diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e2dff..5077ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `!weather` - Check current weather - `!achievements` - View progress - `!activate/deactivate ` - Manage team +- `!swap ` - Reorganize team - `!moves` - View pet abilities - `!flee` - Escape battles diff --git a/README.md b/README.md index ff24e94..a74469f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to - **Pet Collection**: Catch and collect different species of pets - **Exploration**: Travel between various themed locations - **Battle System**: Engage in turn-based battles with wild pets -- **Team Management**: Activate/deactivate pets, manage team composition +- **Team Management**: Activate/deactivate pets, swap team members - **Achievement System**: Unlock new areas by completing challenges - **Item Collection**: Discover and collect useful items during exploration @@ -78,6 +78,7 @@ A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to ### Pet Management - `!activate ` - Activate a pet for battle - `!deactivate ` - Move a pet to storage +- `!swap ` - Swap two pets' active status ### Inventory Commands - `!inventory` / `!inv` / `!items` - View your collected items diff --git a/help.html b/help.html index 534717a..0d39133 100644 --- a/help.html +++ b/help.html @@ -430,6 +430,11 @@
Remove a pet from your active team and put it in storage.
Example: !deactivate aqua
+
+
!swap <pet1> <pet2>
+
Swap the active status of two pets - one becomes active, the other goes to storage.
+
Example: !swap leafy flamey
+
diff --git a/modules/achievements.py b/modules/achievements.py index 98c5ccf..7b4f805 100644 --- a/modules/achievements.py +++ b/modules/achievements.py @@ -19,12 +19,14 @@ class Achievements(BaseModule): if not player: return - # Redirect to web interface for better achievements display - self.send_message(channel, f"🏆 {nickname}: View your complete achievements at: http://petz.rdx4.com/player/{nickname}#achievements") - - # Show quick summary in channel achievements = await self.database.get_player_achievements(player["id"]) + if achievements: - self.send_message(channel, f"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.") + self.send_message(channel, f"🏆 {nickname}'s Achievements:") + for achievement in achievements[:5]: # Show last 5 achievements + self.send_message(channel, f"• {achievement['name']}: {achievement['description']}") + + if len(achievements) > 5: + self.send_message(channel, f"... and {len(achievements) - 5} more!") else: - self.send_message(channel, f"💡 No achievements yet! Keep exploring and catching pets to unlock new areas!") \ No newline at end of file + self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!") \ No newline at end of file diff --git a/modules/base_module.py b/modules/base_module.py index e539a1a..8a4854d 100644 --- a/modules/base_module.py +++ b/modules/base_module.py @@ -12,15 +12,6 @@ class BaseModule(ABC): self.database = database self.game_engine = game_engine - @staticmethod - def normalize_input(user_input): - """Normalize user input by converting to lowercase for case-insensitive command processing""" - if isinstance(user_input, str): - return user_input.lower() - elif isinstance(user_input, list): - return [item.lower() if isinstance(item, str) else item for item in user_input] - return user_input - @abstractmethod def get_commands(self): """Return list of commands this module handles""" diff --git a/modules/battle_system.py b/modules/battle_system.py index 911392f..d50abeb 100644 --- a/modules/battle_system.py +++ b/modules/battle_system.py @@ -52,12 +52,6 @@ class BattleSystem(BaseModule): self.send_message(channel, f"{nickname}: You're already in battle! Use !attack or !flee.") return - # Check if already in gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - if gym_battle: - self.send_message(channel, f"{nickname}: You're already in a gym battle! Finish your gym battle first.") - return - # Get player's active pet pets = await self.database.get_player_pets(player["id"], active_only=True) if not pets: @@ -93,7 +87,7 @@ class BattleSystem(BaseModule): if not player: return - move_name = " ".join(self.normalize_input(args)).title() # Normalize to Title Case + move_name = " ".join(args).title() # Normalize to Title Case result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name) if "error" in result: diff --git a/modules/core_commands.py b/modules/core_commands.py index ec4c497..c723d4b 100644 --- a/modules/core_commands.py +++ b/modules/core_commands.py @@ -40,8 +40,5 @@ class CoreCommands(BaseModule): if not player: return - # Show quick summary and direct to web interface for detailed stats self.send_message(channel, - f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") - self.send_message(channel, - f"🌐 View detailed statistics at: http://petz.rdx4.com/player/{nickname}#stats") \ No newline at end of file + f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") \ No newline at end of file diff --git a/modules/exploration.py b/modules/exploration.py index c221cf8..485a292 100644 --- a/modules/exploration.py +++ b/modules/exploration.py @@ -7,7 +7,7 @@ class Exploration(BaseModule): """Handles exploration, travel, location, weather, and wild commands""" def get_commands(self): - return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture", "flee"] + return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"] async def handle_command(self, channel, nickname, command, args): if command == "explore": @@ -22,8 +22,6 @@ class Exploration(BaseModule): await self.cmd_wild(channel, nickname, args) elif command in ["catch", "capture"]: await self.cmd_catch(channel, nickname) - elif command == "flee": - await self.cmd_flee_encounter(channel, nickname) async def cmd_explore(self, channel, nickname): """Explore current location""" @@ -31,18 +29,6 @@ class Exploration(BaseModule): if not player: return - # Check if player has an active encounter that must be resolved first - if player["id"] in self.bot.active_encounters: - current_encounter = self.bot.active_encounters[player["id"]] - self.send_message(channel, f"{nickname}: You already have an active encounter with a wild {current_encounter['species_name']}! You must choose to !battle, !catch, or !flee before exploring again.") - return - - # Check if player is in an active battle - active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - if active_battle: - self.send_message(channel, f"{nickname}: You're currently in battle! Finish your battle before exploring.") - return - encounter = await self.game_engine.explore_location(player["id"]) if encounter["type"] == "error": @@ -65,7 +51,7 @@ class Exploration(BaseModule): self.send_message(channel, f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!") - self.send_message(channel, f"Choose your action: !battle to fight it, !catch to try catching it directly, or !flee to escape!") + self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!") async def cmd_travel(self, channel, nickname, args): """Travel to a different location""" @@ -78,7 +64,7 @@ class Exploration(BaseModule): return # Handle various input formats and normalize location names - destination_input = self.normalize_input(" ".join(args)) + destination_input = " ".join(args).lower() # Map common variations to exact location names location_mappings = { @@ -96,7 +82,7 @@ class Exploration(BaseModule): destination = location_mappings.get(destination_input) if not destination: # Fall back to title case if no mapping found - destination = " ".join(self.normalize_input(args)).title() + destination = " ".join(args).title() location = await self.database.get_location_by_name(destination) @@ -185,7 +171,7 @@ class Exploration(BaseModule): if args: # Specific location requested - location_name = " ".join(self.normalize_input(args)).title() + location_name = " ".join(args).title() else: # Default to current location current_location = await self.database.get_player_location(player["id"]) @@ -210,13 +196,6 @@ class Exploration(BaseModule): # Check if player is in an active battle active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - gym_battle = await self.database.get_active_gym_battle(player["id"]) - - if gym_battle: - # Can't catch pets during gym battles - self.send_message(channel, f"{nickname}: You can't catch pets during gym battles! Focus on the challenge!") - return - if active_battle: # Catching during battle wild_pet = active_battle["wild_pet"] @@ -318,36 +297,4 @@ class Exploration(BaseModule): """Display level up information (shared with battle system)""" from .battle_system import BattleSystem battle_system = BattleSystem(self.bot, self.database, self.game_engine) - await battle_system.handle_level_up_display(channel, nickname, exp_result) - - async def cmd_flee_encounter(self, channel, nickname): - """Flee from an active encounter without battling""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Check if player has an active encounter to flee from - if player["id"] not in self.bot.active_encounters: - self.send_message(channel, f"{nickname}: You don't have an active encounter to flee from!") - return - - # Check if player is in an active battle - can't flee from exploration if in battle - active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - if active_battle: - self.send_message(channel, f"{nickname}: You're in battle! Use the battle system's !flee command to escape combat.") - return - - # Check if player is in a gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - if gym_battle: - self.send_message(channel, f"{nickname}: You're in a gym battle! Use !forfeit to leave the gym challenge.") - return - - # Get encounter details for message - encounter = self.bot.active_encounters[player["id"]] - - # Remove the encounter - del self.bot.active_encounters[player["id"]] - - self.send_message(channel, f"💨 {nickname}: You fled from the wild {encounter['species_name']}! You can now explore again.") - self.send_message(channel, f"💡 Use !explore to search for another encounter!") \ No newline at end of file + await battle_system.handle_level_up_display(channel, nickname, exp_result) \ No newline at end of file diff --git a/modules/gym_battles.py b/modules/gym_battles.py index 0fbd481..57665c5 100644 --- a/modules/gym_battles.py +++ b/modules/gym_battles.py @@ -13,13 +13,13 @@ class GymBattles(BaseModule): if command == "gym": if not args: await self.cmd_gym_list(channel, nickname) - elif self.normalize_input(args[0]) == "list": + elif args[0] == "list": await self.cmd_gym_list_all(channel, nickname) - elif self.normalize_input(args[0]) == "challenge": + elif args[0] == "challenge": await self.cmd_gym_challenge(channel, nickname, args[1:]) - elif self.normalize_input(args[0]) == "info": + elif args[0] == "info": await self.cmd_gym_info(channel, nickname, args[1:]) - elif self.normalize_input(args[0]) == "status": + elif args[0] == "status": await self.cmd_gym_status(channel, nickname) else: await self.cmd_gym_list(channel, nickname) @@ -66,7 +66,7 @@ class GymBattles(BaseModule): f" Status: {status} | Next difficulty: {difficulty}") self.send_message(channel, - f"💡 Use '!gym challenge' to battle (gym name optional if only one gym in location)!") + f"💡 Use '!gym challenge \"\"' to battle!") async def cmd_gym_list_all(self, channel, nickname): """List all gyms across all locations""" @@ -97,6 +97,10 @@ class GymBattles(BaseModule): async def cmd_gym_challenge(self, channel, nickname, args): """Challenge a gym""" + if not args: + self.send_message(channel, f"{nickname}: Specify a gym to challenge! Example: !gym challenge \"Forest Guardian\"") + return + player = await self.require_player(channel, nickname) if not player: return @@ -107,37 +111,19 @@ class GymBattles(BaseModule): self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.") return - # Get available gyms in current location - available_gyms = await self.database.get_gyms_in_location(location["id"]) - if not available_gyms: - self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.") - return + gym_name = " ".join(args).strip('"') - gym = None - - if not args: - # No gym name provided - auto-challenge if only one gym, otherwise list options - if len(available_gyms) == 1: - gym = available_gyms[0] - self.send_message(channel, f"🏛️ {nickname}: Challenging the {gym['name']} gym in {location['name']}!") - else: - # Multiple gyms - show list and ask user to specify - gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms]) - self.send_message(channel, f"{nickname}: Multiple gyms found in {location['name']}! Specify which gym to challenge:") - self.send_message(channel, f"Available gyms: {gym_list}") - self.send_message(channel, f"💡 Use: !gym challenge \"\"") - return - else: - # Gym name provided - find specific gym - gym_name = " ".join(self.normalize_input(args)).strip('"') - - # Look for gym in player's current location (case-insensitive) - gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"]) - if not gym: - # List available gyms in current location for helpful error message + # Look for gym in player's current location (case-insensitive) + gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"]) + if not gym: + # List available gyms in current location for helpful error message + available_gyms = await self.database.get_gyms_in_location(location["id"]) + if available_gyms: gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms]) self.send_message(channel, f"{nickname}: No gym named '{gym_name}' found in {location['name']}! Available gyms: {gym_list}") - return + else: + self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.") + return # Check if player has active pets active_pets = await self.database.get_active_pets(player["id"]) @@ -280,7 +266,7 @@ class GymBattles(BaseModule): if not player: return - gym_name = " ".join(self.normalize_input(args)).strip('"') + gym_name = " ".join(args).strip('"') # First try to find gym in player's current location location = await self.database.get_player_location(player["id"]) @@ -325,7 +311,7 @@ class GymBattles(BaseModule): return # This will show a summary - for detailed view they can use !gym list - self.send_message(channel, f"🏆 {nickname}: Use !gym list to see all gym progress, or check your profile at: http://petz.rdx4.com/player/{nickname}#gym-badges") + self.send_message(channel, f"🏆 {nickname}: Use !gym list to see all gym progress, or check your profile at: http://petz.rdx4.com/player/{nickname}") async def cmd_forfeit(self, channel, nickname): """Forfeit the current gym battle""" diff --git a/modules/inventory.py b/modules/inventory.py index 9994ad6..1fe60ef 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -16,14 +16,51 @@ class Inventory(BaseModule): await self.cmd_use_item(channel, nickname, args) async def cmd_inventory(self, channel, nickname): - """Redirect player to their web profile for inventory management""" + """Display player's inventory""" player = await self.require_player(channel, nickname) if not player: return - # Redirect to web interface for better inventory management - self.send_message(channel, f"🎒 {nickname}: View your complete inventory at: http://petz.rdx4.com/player/{nickname}#inventory") - self.send_message(channel, f"💡 The web interface shows detailed item information, categories, and usage options!") + inventory = await self.database.get_player_inventory(player["id"]) + + if not inventory: + self.send_message(channel, f"🎒 {nickname}: Your inventory is empty! Try exploring to find items.") + return + + # Group items by category + categories = {} + for item in inventory: + category = item["category"] + if category not in categories: + categories[category] = [] + categories[category].append(item) + + # Send inventory summary first + total_items = sum(item["quantity"] for item in inventory) + self.send_message(channel, f"🎒 {nickname}'s Inventory ({total_items} items):") + + # Display items by category + rarity_symbols = { + "common": "○", + "uncommon": "◇", + "rare": "◆", + "epic": "★", + "legendary": "✦" + } + + for category, items in categories.items(): + category_display = category.replace("_", " ").title() + self.send_message(channel, f"📦 {category_display}:") + + for item in items[:5]: # Limit to 5 items per category to avoid spam + symbol = rarity_symbols.get(item["rarity"], "○") + quantity_str = f" x{item['quantity']}" if item["quantity"] > 1 else "" + self.send_message(channel, f" {symbol} {item['name']}{quantity_str} - {item['description']}") + + if len(items) > 5: + self.send_message(channel, f" ... and {len(items) - 5} more items") + + self.send_message(channel, f"💡 Use '!use ' to use consumable items!") async def cmd_use_item(self, channel, nickname, args): """Use an item from inventory""" @@ -35,7 +72,7 @@ class Inventory(BaseModule): if not player: return - item_name = " ".join(self.normalize_input(args)) + item_name = " ".join(args) result = await self.database.use_item(player["id"], item_name) if not result["success"]: diff --git a/modules/pet_management.py b/modules/pet_management.py index 911bef5..26ffb25 100644 --- a/modules/pet_management.py +++ b/modules/pet_management.py @@ -7,7 +7,7 @@ class PetManagement(BaseModule): """Handles team, pets, and future pet management commands""" def get_commands(self): - return ["team", "pets", "activate", "deactivate", "nickname"] + return ["team", "pets", "activate", "deactivate", "swap", "nickname"] async def handle_command(self, channel, nickname, command, args): if command == "team": @@ -18,6 +18,8 @@ class PetManagement(BaseModule): await self.cmd_activate(channel, nickname, args) elif command == "deactivate": await self.cmd_deactivate(channel, nickname, args) + elif command == "swap": + await self.cmd_swap(channel, nickname, args) elif command == "nickname": await self.cmd_nickname(channel, nickname, args) @@ -38,7 +40,7 @@ class PetManagement(BaseModule): team_info = [] - # Active pets with star and team position + # Active pets with star for pet in active_pets: name = pet["nickname"] or pet["species_name"] @@ -53,9 +55,7 @@ class PetManagement(BaseModule): else: exp_display = f"{exp_needed} to next" - # Show team position - position = pet.get("team_order", "?") - team_info.append(f"[{position}]⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}") + team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}") # Inactive pets for pet in inactive_pets[:5]: # Show max 5 inactive @@ -66,7 +66,6 @@ class PetManagement(BaseModule): team_info.append(f"... and {len(inactive_pets) - 5} more in storage") self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info)) - self.send_message(channel, f"🌐 View detailed pet collection at: http://petz.rdx4.com/player/{nickname}#pets") async def cmd_pets(self, channel, nickname): """Show link to pet collection web page""" @@ -75,7 +74,7 @@ class PetManagement(BaseModule): return # Send URL to player's profile page instead of PM spam - self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}#pets") + self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}") async def cmd_activate(self, channel, nickname, args): """Activate a pet for battle (PM only)""" @@ -89,14 +88,13 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(self.normalize_input(args)) + pet_name = " ".join(args) result = await self.database.activate_pet(player["id"], pet_name) if result["success"]: pet = result["pet"] display_name = pet["nickname"] or pet["species_name"] - position = result.get("team_position", "?") - self.send_pm(nickname, f"✅ {display_name} is now active for battle! Team position: {position}") + self.send_pm(nickname, f"✅ {display_name} is now active for battle!") self.send_message(channel, f"{nickname}: Pet activated successfully!") else: self.send_pm(nickname, f"❌ {result['error']}") @@ -114,7 +112,7 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(self.normalize_input(args)) + pet_name = " ".join(args) result = await self.database.deactivate_pet(player["id"], pet_name) if result["success"]: @@ -126,6 +124,44 @@ class PetManagement(BaseModule): self.send_pm(nickname, f"❌ {result['error']}") self.send_message(channel, f"{nickname}: Pet deactivation failed - check PM for details!") + async def cmd_swap(self, channel, nickname, args): + """Swap active/storage status of two pets (PM only)""" + # Redirect to PM for privacy + if len(args) < 2: + self.send_pm(nickname, "Usage: !swap ") + self.send_pm(nickname, "Example: !swap Flamey Aqua") + self.send_message(channel, f"{nickname}: Pet swap instructions sent via PM!") + return + + player = await self.require_player(channel, nickname) + if not player: + return + + # Handle multi-word pet names by splitting on first space vs last space + if len(args) == 2: + pet1_name, pet2_name = args + else: + # For more complex parsing, assume equal split + mid_point = len(args) // 2 + pet1_name = " ".join(args[:mid_point]) + pet2_name = " ".join(args[mid_point:]) + + result = await self.database.swap_pets(player["id"], pet1_name, pet2_name) + + if result["success"]: + pet1 = result["pet1"] + pet2 = result["pet2"] + pet1_display = pet1["nickname"] or pet1["species_name"] + pet2_display = pet2["nickname"] or pet2["species_name"] + + self.send_pm(nickname, f"🔄 Swap complete!") + self.send_pm(nickname, f" • {pet1_display} → {result['pet1_now']}") + self.send_pm(nickname, f" • {pet2_display} → {result['pet2_now']}") + self.send_message(channel, f"{nickname}: Pet swap completed!") + else: + self.send_pm(nickname, f"❌ {result['error']}") + self.send_message(channel, f"{nickname}: Pet swap failed - check PM for details!") + async def cmd_nickname(self, channel, nickname, args): """Set a nickname for a pet""" if len(args) < 2: @@ -138,7 +174,7 @@ class PetManagement(BaseModule): return # Split args into pet identifier and new nickname - pet_identifier = self.normalize_input(args[0]) + pet_identifier = args[0] new_nickname = " ".join(args[1:]) result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname) diff --git a/run_bot_debug.py b/run_bot_debug.py index 03d9982..5e79860 100644 --- a/run_bot_debug.py +++ b/run_bot_debug.py @@ -62,7 +62,7 @@ class PetBotDebug: print("✅ Background validation started") print("🔄 Starting web server...") - self.web_server = PetBotWebServer(self.database, port=8080, bot=self) + self.web_server = PetBotWebServer(self.database, port=8080) self.web_server.start_in_thread() print("✅ Web server started") @@ -303,14 +303,12 @@ class PetBotDebug: self.handle_command(channel, nickname, message) def handle_command(self, channel, nickname, message): - from modules.base_module import BaseModule - command_parts = message[1:].split() if not command_parts: return - command = BaseModule.normalize_input(command_parts[0]) - args = BaseModule.normalize_input(command_parts[1:]) + command = command_parts[0].lower() + args = command_parts[1:] try: if command in self.command_map: diff --git a/src/database.py b/src/database.py index 52eb5bd..1b89401 100644 --- a/src/database.py +++ b/src/database.py @@ -120,46 +120,6 @@ class Database: except: pass # Column already exists - # Add team_order column if it doesn't exist - try: - await db.execute("ALTER TABLE pets ADD COLUMN team_order INTEGER DEFAULT NULL") - await db.commit() - print("Added team_order column to pets table") - except: - pass # Column already exists - - # Migrate existing active pets to have team_order values - try: - # Find active pets without team_order - cursor = await db.execute(""" - SELECT id, player_id FROM pets - WHERE is_active = TRUE AND team_order IS NULL - ORDER BY player_id, id - """) - pets_to_migrate = await cursor.fetchall() - - if pets_to_migrate: - print(f"Migrating {len(pets_to_migrate)} active pets to have team_order values...") - - # Group pets by player - from collections import defaultdict - pets_by_player = defaultdict(list) - for pet in pets_to_migrate: - pets_by_player[pet[1]].append(pet[0]) - - # Assign team_order values for each player - for player_id, pet_ids in pets_by_player.items(): - for i, pet_id in enumerate(pet_ids[:6]): # Max 6 pets per team - await db.execute(""" - UPDATE pets SET team_order = ? WHERE id = ? - """, (i + 1, pet_id)) - - await db.commit() - print("Migration completed successfully") - except Exception as e: - print(f"Migration warning: {e}") - pass # Don't fail if migration has issues - await db.execute(""" CREATE TABLE IF NOT EXISTS location_spawns ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -448,9 +408,6 @@ class Database: if active_only: query += " AND p.is_active = TRUE" - # Order by team position for active pets, then by id for storage pets - query += " ORDER BY CASE WHEN p.is_active THEN COALESCE(p.team_order, 999) ELSE 999 END ASC, p.id ASC" - cursor = await db.execute(query, params) rows = await cursor.fetchall() return [dict(row) for row in rows] @@ -681,16 +638,11 @@ class Database: if not pet: return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"} - # Get next available team slot - next_slot = await self.get_next_available_team_slot(player_id) - if next_slot is None: - return {"success": False, "error": "Team is full (maximum 6 pets)"} - - # Activate the pet and assign team position - await db.execute("UPDATE pets SET is_active = TRUE, team_order = ? WHERE id = ?", (next_slot, pet["id"])) + # Activate the pet + await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],)) await db.commit() - return {"success": True, "pet": dict(pet), "team_position": next_slot} + return {"success": True, "pet": dict(pet)} async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict: """Deactivate a pet by name or species name. Returns result dict.""" @@ -718,122 +670,58 @@ class Database: if active_count["count"] <= 1: return {"success": False, "error": "You must have at least one active pet!"} - # Deactivate the pet and clear team order - await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],)) + # Deactivate the pet + await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],)) await db.commit() return {"success": True, "pet": dict(pet)} - # Team Order Methods - async def get_next_available_team_slot(self, player_id: int) -> int: - """Get the next available team slot (1-6)""" + async def swap_pets(self, player_id: int, pet1_identifier: str, pet2_identifier: str) -> Dict: + """Swap the active status of two pets. Returns result dict.""" async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Find both pets cursor = await db.execute(""" - SELECT team_order FROM pets - WHERE player_id = ? AND is_active = TRUE AND team_order IS NOT NULL - ORDER BY team_order ASC - """, (player_id,)) - used_slots = [row[0] for row in await cursor.fetchall()] + SELECT p.*, ps.name as species_name + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + AND (p.nickname = ? OR ps.name = ?) + LIMIT 1 + """, (player_id, pet1_identifier, pet1_identifier)) + pet1 = await cursor.fetchone() - # Find first available slot (1-6) - for slot in range(1, 7): - if slot not in used_slots: - return slot - return None # Team is full - - async def set_pet_team_order(self, player_id: int, pet_id: int, position: int) -> Dict: - """Set a pet's team order position (1-6)""" - if position < 1 or position > 6: - return {"success": False, "error": "Team position must be between 1-6"} - - async with aiosqlite.connect(self.db_path) as db: - # Check if pet belongs to player - cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": "Pet not found"} - - # Check if position is already taken cursor = await db.execute(""" - SELECT id FROM pets - WHERE player_id = ? AND team_order = ? AND is_active = TRUE AND id != ? - """, (player_id, position, pet_id)) - existing_pet = await cursor.fetchone() + SELECT p.*, ps.name as species_name + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + AND (p.nickname = ? OR ps.name = ?) + LIMIT 1 + """, (player_id, pet2_identifier, pet2_identifier)) + pet2 = await cursor.fetchone() - if existing_pet: - return {"success": False, "error": f"Position {position} is already taken"} + if not pet1: + return {"success": False, "error": f"Pet '{pet1_identifier}' not found"} + if not pet2: + return {"success": False, "error": f"Pet '{pet2_identifier}' not found"} - # Update pet's team order and make it active - await db.execute(""" - UPDATE pets SET team_order = ?, is_active = TRUE - WHERE id = ? AND player_id = ? - """, (position, pet_id, player_id)) + if pet1["id"] == pet2["id"]: + return {"success": False, "error": "Cannot swap a pet with itself"} + # Swap their active status + await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet1["is_active"], pet1["id"])) + await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet2["is_active"], pet2["id"])) await db.commit() - return {"success": True, "position": position} - - async def reorder_team_positions(self, player_id: int, new_positions: List[Dict]) -> Dict: - """Reorder team positions based on new arrangement""" - async with aiosqlite.connect(self.db_path) as db: - try: - # Validate all positions are 1-6 and no duplicates - positions = [pos["position"] for pos in new_positions] - if len(set(positions)) != len(positions): - return {"success": False, "error": "Duplicate positions detected"} - - for pos_data in new_positions: - position = pos_data["position"] - pet_id = pos_data["pet_id"] - - if position < 1 or position > 6: - return {"success": False, "error": f"Invalid position {position}"} - - # Verify pet belongs to player - cursor = await db.execute("SELECT id FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - if not await cursor.fetchone(): - return {"success": False, "error": f"Pet {pet_id} not found"} - - # Clear all team orders first - await db.execute("UPDATE pets SET team_order = NULL WHERE player_id = ?", (player_id,)) - - # Set new positions - for pos_data in new_positions: - await db.execute(""" - UPDATE pets SET team_order = ?, is_active = TRUE - WHERE id = ? AND player_id = ? - """, (pos_data["position"], pos_data["pet_id"], player_id)) - - await db.commit() - return {"success": True, "message": "Team order updated successfully"} - - except Exception as e: - await db.rollback() - return {"success": False, "error": str(e)} - - async def remove_from_team_position(self, player_id: int, pet_id: int) -> Dict: - """Remove a pet from team (set to inactive and clear team_order)""" - async with aiosqlite.connect(self.db_path) as db: - # Check if pet belongs to player - cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": "Pet not found"} - # Check if this is the only active pet - cursor = await db.execute("SELECT COUNT(*) as count FROM pets WHERE player_id = ? AND is_active = TRUE", (player_id,)) - active_count = await cursor.fetchone() - - if active_count["count"] <= 1: - return {"success": False, "error": "Cannot deactivate your only active pet"} - - # Remove from team - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE id = ? AND player_id = ? - """, (pet_id, player_id)) - - await db.commit() - return {"success": True, "message": "Pet removed from team"} + return { + "success": True, + "pet1": dict(pet1), + "pet2": dict(pet2), + "pet1_now": "active" if not pet1["is_active"] else "storage", + "pet2_now": "active" if not pet2["is_active"] else "storage" + } # Item and Inventory Methods async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool: @@ -985,7 +873,7 @@ class Database: FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? AND p.is_active = 1 - ORDER BY p.team_order ASC, p.id ASC + ORDER BY p.id ASC """, (player_id,)) rows = await cursor.fetchall() return [dict(row) for row in rows] @@ -1713,18 +1601,12 @@ class Database: # Begin transaction await db.execute("BEGIN TRANSACTION") - # Update pet active status and team_order based on new team - for pet_id, position in team_changes.items(): - if position: # If position is a number (1-6), pet is active - await db.execute(""" - UPDATE pets SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (position, int(pet_id), player_id)) - else: # If position is False, pet is inactive - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE id = ? AND player_id = ? - """, (int(pet_id), player_id)) + # Update pet active status based on new team + for pet_id, is_active in team_changes.items(): + await db.execute(""" + UPDATE pets SET is_active = ? + WHERE id = ? AND player_id = ? + """, (is_active, int(pet_id), player_id)) # Mark any pending change as verified await db.execute(""" @@ -1831,28 +1713,18 @@ class Database: # Get current pet states cursor = await db.execute(""" - SELECT id, is_active, team_order FROM pets WHERE player_id = ? + SELECT id, is_active FROM pets WHERE player_id = ? """, (player_id,)) - current_pets = {str(row["id"]): row["team_order"] if row["is_active"] else False for row in await cursor.fetchall()} + current_pets = {str(row["id"]): bool(row["is_active"]) for row in await cursor.fetchall()} # Apply proposed changes to current state new_state = current_pets.copy() - for pet_id, new_position in proposed_changes.items(): + for pet_id, new_active_state in proposed_changes.items(): if pet_id in new_state: - new_state[pet_id] = new_position + new_state[pet_id] = new_active_state - # Count active pets and validate positions - active_positions = [pos for pos in new_state.values() if pos] - active_count = len(active_positions) - - # Check for valid positions (1-6) - for pos in active_positions: - if not isinstance(pos, int) or pos < 1 or pos > 6: - return {"valid": False, "error": f"Invalid team position: {pos}"} - - # Check for duplicate positions - if len(active_positions) != len(set(active_positions)): - return {"valid": False, "error": "Duplicate team positions detected"} + # Count active pets in new state + active_count = sum(1 for is_active in new_state.values() if is_active) # Validate constraints if active_count < 1: diff --git a/src/game_engine.py b/src/game_engine.py index 1906fd0..59af9af 100644 --- a/src/game_engine.py +++ b/src/game_engine.py @@ -196,11 +196,11 @@ class GameEngine: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, - attack, defense, speed, is_active, team_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + attack, defense, speed, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, species["id"], pet_data["level"], 0, pet_data["hp"], pet_data["hp"], pet_data["attack"], - pet_data["defense"], pet_data["speed"], True, 1)) + pet_data["defense"], pet_data["speed"], True)) await db.commit() diff --git a/webserver.py b/webserver.py index 709bd1f..1d83c2a 100644 --- a/webserver.py +++ b/webserver.py @@ -25,529 +25,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """Get database instance from server""" return self.server.database - @property - def bot(self): - """Get bot instance from server""" - return getattr(self.server, 'bot', None) - - def send_json_response(self, data, status_code=200): - """Send a JSON response""" - import json - self.send_response(status_code) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(data).encode()) - - def get_unified_css(self): - """Return unified CSS theme for all pages""" - return """ - :root { - --bg-primary: #0f0f23; - --bg-secondary: #1e1e3f; - --bg-tertiary: #2a2a4a; - --text-primary: #cccccc; - --text-secondary: #aaaaaa; - --text-accent: #66ff66; - --accent-blue: #4dabf7; - --accent-purple: #845ec2; - --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - --gradient-secondary: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); - --border-color: #444466; - --hover-color: #3a3a5a; - --shadow-color: rgba(0, 0, 0, 0.3); - --success-color: #51cf66; - --warning-color: #ffd43b; - --error-color: #ff6b6b; - } - - * { - box-sizing: border-box; - } - - body { - font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif; - max-width: 1200px; - margin: 0 auto; - padding: 0; - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.6; - min-height: 100vh; - } - - .main-container { - padding: 20px; - min-height: calc(100vh - 80px); - } - - /* Navigation Bar */ - .navbar { - background: var(--gradient-primary); - padding: 15px 20px; - box-shadow: 0 2px 10px var(--shadow-color); - margin-bottom: 0; - } - - .nav-content { - display: flex; - justify-content: space-between; - align-items: center; - max-width: 1200px; - margin: 0 auto; - } - - .nav-brand { - font-size: 1.5em; - font-weight: bold; - color: white; - text-decoration: none; - display: flex; - align-items: center; - gap: 10px; - } - - .nav-links { - display: flex; - gap: 20px; - align-items: center; - } - - .nav-link { - color: white; - text-decoration: none; - padding: 8px 16px; - border-radius: 20px; - transition: all 0.3s ease; - font-weight: 500; - } - - .nav-link:hover { - background: rgba(255, 255, 255, 0.2); - transform: translateY(-1px); - } - - .nav-link.active { - background: rgba(255, 255, 255, 0.3); - } - - /* Dropdown Navigation */ - .nav-dropdown { - position: relative; - display: inline-block; - } - - .dropdown-arrow { - font-size: 0.8em; - margin-left: 5px; - transition: transform 0.3s ease; - } - - .nav-dropdown:hover .dropdown-arrow { - transform: rotate(180deg); - } - - .dropdown-content { - display: none; - position: absolute; - top: 100%; - left: 0; - background: var(--bg-secondary); - min-width: 180px; - box-shadow: 0 8px 16px var(--shadow-color); - border-radius: 8px; - z-index: 1000; - border: 1px solid var(--border-color); - overflow: hidden; - margin-top: 5px; - } - - .nav-dropdown:hover .dropdown-content { - display: block; - animation: fadeIn 0.3s ease; - } - - @keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } - } - - .dropdown-item { - color: var(--text-primary); - padding: 12px 16px; - text-decoration: none; - display: block; - transition: background-color 0.3s ease; - border-bottom: 1px solid var(--border-color); - } - - .dropdown-item:last-child { - border-bottom: none; - } - - .dropdown-item:hover { - background: var(--bg-tertiary); - color: var(--text-accent); - } - - @media (max-width: 768px) { - .nav-content { - flex-direction: column; - gap: 15px; - } - - .nav-links { - flex-wrap: wrap; - justify-content: center; - gap: 10px; - } - - .dropdown-content { - position: static; - display: none; - width: 100%; - box-shadow: none; - border: none; - border-radius: 0; - background: var(--bg-tertiary); - margin-top: 0; - } - - .nav-dropdown:hover .dropdown-content { - display: block; - } - } - - /* Header styling */ - .header { - text-align: center; - background: var(--gradient-primary); - color: white; - padding: 40px 20px; - border-radius: 15px; - margin-bottom: 30px; - box-shadow: 0 5px 20px var(--shadow-color); - } - - .header h1 { - margin: 0 0 10px 0; - font-size: 2.5em; - font-weight: bold; - } - - .header p { - margin: 0; - font-size: 1.1em; - opacity: 0.9; - } - - /* Card styling */ - .card { - background: var(--bg-secondary); - border-radius: 15px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 15px var(--shadow-color); - border: 1px solid var(--border-color); - transition: transform 0.3s ease, box-shadow 0.3s ease; - } - - .card:hover { - transform: translateY(-2px); - box-shadow: 0 6px 25px var(--shadow-color); - } - - .card h2 { - margin-top: 0; - color: var(--text-accent); - border-bottom: 2px solid var(--text-accent); - padding-bottom: 10px; - } - - .card h3 { - color: var(--accent-blue); - margin-top: 25px; - } - - /* Grid layouts */ - .grid { - display: grid; - gap: 20px; - margin-bottom: 30px; - } - - .grid-2 { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); } - .grid-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); } - .grid-4 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); } - - /* Buttons */ - .btn { - display: inline-block; - padding: 10px 20px; - border: none; - border-radius: 25px; - text-decoration: none; - font-weight: 500; - text-align: center; - cursor: pointer; - transition: all 0.3s ease; - margin: 5px; - } - - .btn-primary { - background: var(--gradient-primary); - color: white; - } - - .btn-secondary { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border-color); - } - - .btn:hover { - transform: translateY(-2px); - box-shadow: 0 4px 15px var(--shadow-color); - } - - /* Badges and tags */ - .badge { - display: inline-block; - padding: 4px 12px; - border-radius: 15px; - font-size: 0.85em; - font-weight: 500; - margin: 2px; - } - - .badge-primary { background: var(--accent-blue); color: white; } - .badge-secondary { background: var(--bg-tertiary); color: var(--text-primary); } - .badge-success { background: var(--success-color); color: white; } - .badge-warning { background: var(--warning-color); color: #333; } - .badge-error { background: var(--error-color); color: white; } - - /* Tables */ - .table { - width: 100%; - border-collapse: collapse; - margin: 20px 0; - background: var(--bg-secondary); - border-radius: 10px; - overflow: hidden; - } - - .table th, .table td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid var(--border-color); - } - - .table th { - background: var(--bg-tertiary); - font-weight: bold; - color: var(--text-accent); - } - - .table tr:hover { - background: var(--bg-tertiary); - } - - /* Loading and status messages */ - .loading { - text-align: center; - padding: 40px; - color: var(--text-secondary); - } - - .error-message { - background: var(--error-color); - color: white; - padding: 15px; - border-radius: 10px; - margin: 20px 0; - } - - .success-message { - background: var(--success-color); - color: white; - padding: 15px; - border-radius: 10px; - margin: 20px 0; - } - - /* Pet-specific styles for petdex */ - .pets-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 20px; - margin-top: 20px; - } - - .pet-card { - background: var(--bg-secondary); - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 15px var(--shadow-color); - border: 1px solid var(--border-color); - transition: transform 0.3s ease; - } - - .pet-card:hover { - transform: translateY(-3px); - } - - .pet-header { - background: var(--bg-tertiary); - padding: 15px 20px; - display: flex; - justify-content: space-between; - align-items: center; - } - - .pet-header h3 { - margin: 0; - font-size: 1.3em; - } - - .type-badge { - background: var(--gradient-primary); - color: white; - padding: 4px 12px; - border-radius: 15px; - font-size: 0.85em; - font-weight: 500; - } - - .pet-stats { - padding: 15px 20px; - background: var(--bg-secondary); - } - - .stat-row { - display: flex; - justify-content: space-between; - margin-bottom: 8px; - font-size: 0.9em; - } - - .total-stats { - text-align: center; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--border-color); - font-weight: 600; - color: var(--text-accent); - } - - .pet-info { - padding: 15px 20px; - background: var(--bg-tertiary); - font-size: 0.9em; - line-height: 1.5; - } - - .rarity-section { - margin-bottom: 40px; - } - - /* Responsive design */ - @media (max-width: 768px) { - .main-container { - padding: 10px; - } - - .header h1 { - font-size: 2em; - } - - .card { - padding: 15px; - } - - .grid-2, .grid-3, .grid-4 { - grid-template-columns: 1fr; - } - } - """ - - def get_navigation_bar(self, current_page=""): - """Return unified navigation bar HTML with dropdown menus""" - - # Define navigation structure with dropdowns - nav_structure = [ - ("", "🏠 Home", []), - ("players", "👥 Players", [ - ("leaderboard", "🏆 Leaderboard"), - ("players", "📊 Statistics") - ]), - ("locations", "🗺️ Locations", [ - ("locations", "🌤️ Weather"), - ("locations", "🎯 Spawns"), - ("locations", "🏛️ Gyms") - ]), - ("petdex", "📚 Petdex", [ - ("petdex", "🔷 by Type"), - ("petdex", "⭐ by Rarity"), - ("petdex", "🔍 Search") - ]), - ("help", "📖 Help", [ - ("help", "⚡ Commands"), - ("help", "📖 Web Guide"), - ("help", "❓ FAQ") - ]) - ] - - nav_links = "" - for page_path, page_name, subpages in nav_structure: - active_class = " active" if current_page == page_path else "" - href = f"/{page_path}" if page_path else "/" - - if subpages: - # Create dropdown menu - dropdown_items = "" - for sub_path, sub_name in subpages: - sub_href = f"/{sub_path}" if sub_path else "/" - dropdown_items += f'{sub_name}' - - nav_links += f''' - ''' - else: - # Regular nav link - nav_links += f'{page_name}' - - return f""" - - """ - - def get_page_template(self, title, content, current_page=""): - """Return complete page HTML with unified theme""" - return f""" - - - - - {title} - PetBot - - - - {self.get_navigation_bar(current_page)} -
- {content} -
- -""" - def do_GET(self): """Handle GET requests""" parsed_path = urlparse(self.path) @@ -591,501 +68,139 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): def serve_index(self): """Serve the main index page""" - content = """ -
-

🐾 PetBot Game Hub

-

Welcome to the PetBot web interface!

-

Connect to irc.libera.chat #petz to play

-
+ html = """ + + + + + PetBot Game Hub + + + +
+

🐾 PetBot Game Hub

+

Welcome to the PetBot web interface!

+

Connect to irc.libera.chat #petz to play

+
+ + + +
+

🤖 Bot Status: Online and ready for commands!

+

Use !help in #petz for quick command reference

+
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def serve_help(self): - """Serve the help page using unified template""" - content = """ -
-

📚 PetBot Commands

-

Complete guide to Pokemon-style pet collecting in IRC

-
- -
-
🚀 Getting Started
-
-
-
-
!start
-
Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.
-
Example: !start
-
-
-
!help
-
Get a link to this comprehensive command reference page.
-
Example: !help
-
-
-
!stats
-
View your basic trainer information including level, experience, and money.
-
Example: !stats
-
-
-
-
- -
-
🌍 Exploration & Travel
-
-
-
-
!explore
-
Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!
-
Example: !explore
-
-
-
!travel <location>
-
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.
-
Example: !where
-
-
- -
-

🗺️ Available Locations

-
    -
  • Starter Town - Peaceful starting area (Fire/Water/Grass pets)
  • -
  • Whispering Woods - Ancient forest (Grass pets + new species: Vinewrap, Bloomtail)
  • -
  • Electric Canyon - Charged valley (Electric/Rock pets)
  • -
  • Crystal Caves - Underground caverns (Rock/Crystal pets)
  • -
  • Frozen Tundra - Icy wasteland (Ice/Water pets)
  • -
  • Dragon's Peak - Ultimate challenge (Fire/Rock/Ice pets)
  • -
-
- -
-

🌤️ Weather Effects

-
    -
  • Sunny - 1.5x Fire/Grass spawns (1-2 hours)
  • -
  • Rainy - 2.0x Water spawns (45-90 minutes)
  • -
  • Thunderstorm - 2.0x Electric spawns (30-60 minutes)
  • -
  • Blizzard - 1.7x Ice/Water spawns (1-2 hours)
  • -
  • Earthquake - 1.8x Rock spawns (30-90 minutes)
  • -
  • Calm - Normal spawns (1.5-3 hours)
  • -
-
-
-
- -
-
⚔️ Battle System
-
-
-
-
!catch / !capture
-
Attempt to catch a wild pet that appeared during exploration. Success depends on the pet's level and rarity.
-
Example: !catch
-
-
-
!battle
-
Start a turn-based battle with a wild pet. Defeat it to gain experience and money for your active pet.
-
Example: !battle
-
-
-
!attack <move>
-
Use a specific move during battle. Each move has different power, type, and effects.
-
Example: !attack flamethrower
-
-
-
!moves
-
View all available moves for your active pet, including their types and power levels.
-
Example: !moves
-
-
-
!flee
-
Attempt to escape from the current battle. Not always successful!
-
Example: !flee
-
-
-
-
- -
-
🏛️ Gym Battles NEW!
-
-
-
-
!gym
-
List all gyms in your current location with your progress. Shows victories and next difficulty level.
-
Example: !gym
-
-
-
!gym list
-
Show all gyms across all locations with your badge collection progress.
-
Example: !gym list
-
-
-
!gym challenge "<name>"
-
Challenge a gym leader! You must be in the same location as the gym. Difficulty increases with each victory.
-
Example: !gym challenge "Forest Guardian"
-
-
-
!gym info "<name>"
-
Get detailed information about a gym including leader, theme, team, and badge details.
-
Example: !gym info "Storm Master"
-
-
- -
- 💡 Gym Strategy: Each gym specializes in a specific type. Bring pets with type advantages! The more you beat a gym, the harder it gets, but the better the rewards! -
- -
-

🏆 Gym Leaders & Badges

-
-
- 🍃 Forest Guardian
- Location: Starter Town
- Leader: Trainer Verde
- Theme: Grass-type -
-
- 🌳 Nature's Haven
- Location: Whispering Woods
- Leader: Elder Sage
- Theme: Grass-type -
-
- ⚡ Storm Master
- Location: Electric Canyon
- Leader: Captain Volt
- Theme: Electric-type -
-
- 💎 Stone Crusher
- Location: Crystal Caves
- Leader: Miner Magnus
- Theme: Rock-type -
-
- ❄️ Ice Breaker
- Location: Frozen Tundra
- Leader: Arctic Queen
- Theme: Ice/Water-type -
-
- 🐉 Dragon Slayer
- Location: Dragon's Peak
- Leader: Champion Drake
- Theme: Fire-type -
-
-
-
-
- -
-
🐾 Pet Management
-
-
-
-
!team
-
View your active team of pets with their levels, HP, and status.
-
Example: !team
-
-
-
!pets
-
View your complete pet collection with detailed stats and information via web interface.
-
Example: !pets
-
-
-
!activate <pet>
-
Add a pet to your active battle team. You can have multiple active pets for different situations.
-
Example: !activate flamey
-
-
-
!deactivate <pet>
-
Remove a pet from your active team and put it in storage.
-
Example: !deactivate aqua
-
-
-
-
- -
-
🎒 Inventory System NEW!
-
-
-
-
!inventory / !inv / !items
-
View all items in your inventory organized by category. Shows quantities and item descriptions.
-
Example: !inventory
-
-
-
!use <item name>
-
Use a consumable item from your inventory. Items can heal pets, boost stats, or provide other benefits.
-
Example: !use Small Potion
-
-
- -
-

🎯 Item Categories & Rarities

-
    -
  • ○ Common (15%) - Small Potions, basic healing items
  • -
  • ◇ Uncommon (8-12%) - Large Potions, battle boosters, special berries
  • -
  • ◆ Rare (3-6%) - Super Potions, speed elixirs, location treasures
  • -
  • ★ Epic (2-3%) - Evolution stones, rare crystals, ancient artifacts
  • -
  • ✦ Legendary (1%) - Lucky charms, ancient fossils, ultimate items
  • -
-
- -
- 💡 Item Discovery: Find items while exploring! Each location has unique treasures. Items stack in your inventory and can be used anytime. -
-
- -
-
🏆 Achievements & Progress
-
-
-
-
!achievements
-
View your achievement progress and see which new locations you've unlocked.
-
Example: !achievements
-
-
- -
-

🎯 Location Unlock Requirements

-
    -
  • Pet Collector (5 pets) → Unlocks Whispering Woods
  • -
  • Spark Collector (2 Electric species) → Unlocks Electric Canyon
  • -
  • Rock Hound (3 Rock species) → Unlocks Crystal Caves
  • -
  • Ice Breaker (5 Water/Ice species) → Unlocks Frozen Tundra
  • -
  • Dragon Tamer (15 pets + 3 Fire species) → Unlocks Dragon's Peak
  • -
-
-
- -
-
🌐 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
  • -
-
-
-
- - - """ - - # Add command-specific CSS to the unified styles - additional_css = """ - .section { - background: var(--bg-secondary); - border-radius: 15px; - margin-bottom: 30px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - border: 1px solid var(--border-color); - overflow: hidden; - } - - .section-header { - background: var(--gradient-primary); - color: white; - padding: 20px 25px; - font-size: 1.3em; - font-weight: 700; - } - - .section-content { - padding: 25px; - } - - .command-grid { - display: grid; - gap: 20px; - } - - .command { - border: 1px solid var(--border-color); - border-radius: 12px; - overflow: hidden; - transition: all 0.3s ease; - background: var(--bg-tertiary); - } - - .command:hover { - transform: translateY(-3px); - box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15); - border-color: var(--text-accent); - } - - .command-name { - background: var(--bg-primary); - padding: 15px 20px; - font-family: 'Fira Code', 'Courier New', monospace; - font-weight: bold; - color: var(--text-accent); - border-bottom: 1px solid var(--border-color); - font-size: 1.2em; - text-shadow: 0 0 10px rgba(102, 255, 102, 0.3); - } - - .command-desc { - padding: 20px; - line-height: 1.7; - color: var(--text-primary); - } - - .command-example { - background: var(--bg-primary); - padding: 12px 20px; - font-family: 'Fira Code', 'Courier New', monospace; - color: var(--text-secondary); - border-top: 1px solid var(--border-color); - font-size: 0.95em; - } - - .info-box { - background: var(--bg-tertiary); - padding: 20px; - border-radius: 12px; - margin: 20px 0; - border: 1px solid var(--border-color); - } - - .info-box h4 { - margin: 0 0 15px 0; - color: var(--text-accent); - font-size: 1.1em; - font-weight: 600; - } - - .info-box ul { - margin: 0; - padding-left: 25px; - } - - .info-box li { - margin: 8px 0; - color: var(--text-primary); - } - - .info-box strong { - color: var(--text-accent); - } - - .footer { - text-align: center; - margin-top: 50px; - padding: 30px; - background: var(--bg-secondary); - border-radius: 15px; - color: var(--text-secondary); - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - border: 1px solid var(--border-color); - } - - .tip { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 20px; - border-radius: 12px; - margin: 20px 0; - font-weight: 500; - text-shadow: 0 2px 4px rgba(0,0,0,0.3); - } - - .gym-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 15px; - margin: 15px 0; - } - - .gym-card { - background: var(--bg-primary); - padding: 15px; - border-radius: 8px; - border: 1px solid var(--border-color); - } - - .gym-card strong { - color: var(--text-accent); - } - """ - - # Get the unified template with additional CSS - html_content = self.get_page_template("Command Help", content, "help") - # Insert additional CSS before closing tag - html_content = html_content.replace("", additional_css + "") - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_content.encode()) + """Serve the help page""" + try: + with open('help.html', 'r') as f: + content = f.read() + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(content.encode()) + except FileNotFoundError: + self.send_error(404, "Help file not found") def serve_players(self): """Serve the players page with real data""" @@ -1153,34 +268,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): def serve_players_data(self, players_data): """Serve players page with real data""" - # Calculate statistics - total_players = len(players_data) - total_pets = sum(p['pet_count'] for p in players_data) if players_data else 0 - total_achievements = sum(p['achievement_count'] for p in players_data) if players_data else 0 - highest_level = max((p['level'] for p in players_data), default=0) if players_data else 0 - - # Build statistics cards - stats_content = f""" -
-
-

📊 Total Players

-
{total_players}
-
-
-

🐾 Total Pets

-
{total_pets}
-
-
-

🏆 Achievements

-
{total_achievements}
-
-
-

⭐ Highest Level

-
{highest_level}
-
-
- """ - # Build players table HTML if players_data: players_html = "" @@ -1207,11 +294,175 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ - # Build table content - table_content = f""" -
-

🏆 Player Rankings

- + html = f""" + + + + + PetBot - Players + + + + ← Back to Game Hub + +
+

👥 Registered Players

+

All trainers on their pet collection journey

+
+ +
+
📊 Server Statistics
+
+
+
+
{len(players_data)}
+
Total Players
+
+
+
{sum(p['pet_count'] for p in players_data)}
+
Total Pets Caught
+
+
+
{sum(p['achievement_count'] for p in players_data)}
+
Total Achievements
+
+
+
{max((p['level'] for p in players_data), default=0)}
+
Highest Level
+
+
+
+
+ +
+
🏆 Player Rankings
+
+
@@ -1229,24 +480,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): {players_html}
Rank
+

💡 Click on any player name to view their detailed profile

- """ - - # Combine all content - content = f""" -
-

👥 Registered Players

-

All trainers on their pet collection journey

-
- - {stats_content} - {table_content} - """ - - html = self.get_page_template("Players", content, "players") +
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') @@ -1254,49 +495,76 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.wfile.write(html.encode()) def serve_error_page(self, page_name, error_msg): - """Serve a generic error page using unified template""" - content = f""" -
-

⚠️ Error Loading {page_name}

-
+ """Serve a generic error page""" + html = f""" + + + + + PetBot - Error + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← Back to Game Hub + +
+

⚠️ Error Loading {page_name}

+
+ +
+

Unable to load page

+

{error_msg}

+

Please try again later or contact an administrator.

+
+ +""" self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_leaderboard(self): """Serve the leaderboard page - redirect to players for now""" @@ -1367,7 +635,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return [] def serve_locations_data(self, locations_data): - """Serve locations page with real data using unified template""" + """Serve locations page with real data""" # Build locations HTML locations_html = "" @@ -1431,133 +699,119 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - content = f""" -
-

🗺️ Game Locations

-

Explore all areas and discover what pets await you!

-
+ html = f""" + + + + + PetBot - Locations + tag - html_content = html_content.replace("", additional_css + "") + }} + + + + ← Back to Game Hub + +
+

🗺️ Game Locations

+

Explore all areas and discover what pets await you!

+
+ +
+

🎯 How Locations Work

+

Travel: Use !travel <location> to move between areas

+

Explore: Use !explore to find wild pets in your current location

+

Unlock: Some locations require achievements - catch specific pet types to unlock new areas!

+

Weather: Check !weather for conditions that boost certain pet spawn rates

+
+ +
+ {locations_html} +
+ +
+

+ 💡 Use !wild <location> in #petz to see what pets spawn in a specific area +

+
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_petdex(self): """Serve the petdex page with all pet species data""" @@ -1648,9 +899,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): try: import aiosqlite async with aiosqlite.connect(database.db_path) as db: - # Get all pet species with evolution information (no duplicates) + # Get all pet species with evolution information cursor = await db.execute(""" - SELECT DISTINCT ps.*, + SELECT ps.*, evolve_to.name as evolves_to_name, (SELECT COUNT(*) FROM location_spawns ls WHERE ls.species_id = ps.id) as location_count FROM pet_species ps @@ -1701,40 +952,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"} rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"} - # Calculate statistics - total_species = len(petdex_data) - type_counts = {} - for pet in petdex_data: - if pet['type1'] not in type_counts: - type_counts[pet['type1']] = 0 - type_counts[pet['type1']] += 1 - if pet['type2'] and pet['type2'] not in type_counts: - type_counts[pet['type2']] = 0 - if pet['type2']: - type_counts[pet['type2']] += 1 - - # Build statistics section - stats_content = f""" -
-
-

📊 Total Species

-
{total_species}
-
-
-

🎨 Types

-
{len(type_counts)}
-
-
-

⭐ Rarities

-
{len(set(pet['rarity'] for pet in petdex_data))}
-
-
-

🧬 Evolutions

-
{len([p for p in petdex_data if p['evolution_level']])}
-
-
- """ - pets_by_rarity = {} for pet in petdex_data: rarity = pet['rarity'] @@ -1817,24 +1034,207 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):

The petdex appears to be empty. Contact an administrator.

""" - # Combine all content - content = f""" -
-

📖 Petdex

-

Complete encyclopedia of all available pets

+ html = f""" + + + + + PetBot - Petdex + + + + ← Back to Game Hub + +
+

📖 Petdex - Complete Pet Encyclopedia

+

Comprehensive guide to all available pet species

+
+ +
+
+
+
{total_species}
+
Total Species
+
+
+
{len([p for p in petdex_data if p['type1'] == 'Fire' or p['type2'] == 'Fire'])}
+
Fire Types
+
+
+
{len([p for p in petdex_data if p['type1'] == 'Water' or p['type2'] == 'Water'])}
+
Water Types
+
+
+
{len([p for p in petdex_data if p['evolution_level']])}
+
Can Evolve
+
+

🎯 Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!

+
+ + {petdex_html} + + +""" - {stats_content} - -
-

📊 Pet Collection by Rarity

-

🎯 Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!

- - {petdex_html} -
- """ - - html = self.get_page_template("Petdex", content, "petdex") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() @@ -1921,7 +1321,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 'hp': row[6], 'max_hp': row[7], 'attack': row[8], 'defense': row[9], 'speed': row[10], 'happiness': row[11], 'caught_at': row[12], 'is_active': bool(row[13]), # Convert to proper boolean - 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17] + 'species_name': row[14], 'type1': row[15], 'type2': row[16] } pets.append(pet_dict) @@ -2026,94 +1426,148 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return None def serve_player_not_found(self, nickname): - """Serve player not found page using unified template""" - content = f""" -
-

🚫 Player Not Found

-
+ """Serve player not found page""" + html = f""" + + + + + PetBot - Player Not Found + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← Back to Game Hub + +
+

🚫 Player Not Found

+
+ +
+

Player "{nickname}" not found

+

This player hasn't started their journey yet or doesn't exist.

+

Players can use !start in #petz to begin their adventure!

+
+ +""" self.send_response(404) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_player_error(self, nickname, error_msg): - """Serve player error page using unified template""" - content = f""" -
-

⚠️ Error

-
+ """Serve player error page""" + html = f""" + + + + + PetBot - Error + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← Back to Game Hub + +
+

⚠️ Error

+
+ +
+

Unable to load player data

+

{error_msg}

+

Please try again later or contact an administrator.

+
+ +""" self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_player_data(self, nickname, player_data): """Serve player profile page with real data""" @@ -2165,20 +1619,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if achievements: for achievement in achievements: achievements_html += f""" -
-
🏆
-
-

{achievement['achievement_name']}

-

{achievement['achievement_desc']}

- Earned: {achievement['completed_at']} -
+
+ 🏆 {achievement['achievement_name']}
+ {achievement['achievement_desc']}
+ Earned: {achievement['completed_at']}
""" else: achievements_html = """ -
-
🏆
-

No achievements yet

-

Keep exploring and catching pets to earn achievements!

+
+ No achievements yet. Keep exploring and catching pets to earn achievements!
""" # Build inventory HTML @@ -2205,19 +1654,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): quantity_str = f" x{item['quantity']}" if item['quantity'] > 1 else "" inventory_html += f""" -
-
- {symbol} {item['name']}{quantity_str} -
-
{item['description']}
-
Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}
+
+ {symbol} {item['name']}{quantity_str}
+ {item['description']}
+ Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}
""" else: inventory_html = """ -
-
🎒
-

No items yet

-

Try exploring to find useful items!

+
+ No items yet. Try exploring to find useful items!
""" # Build gym badges HTML @@ -2233,24 +1678,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): except (AttributeError, IndexError): badge_date = 'Unknown' badges_html += f""" -
-
{badge['badge_icon']}
-
-

{badge['badge_name']}

-

Earned from {badge['gym_name']} ({badge['location_name']})

-
- First victory: {badge_date} - Total victories: {badge['victories']} - Highest difficulty: Level {badge['highest_difficulty']} -
-
+
+ {badge['badge_icon']} {badge['badge_name']}
+ Earned from {badge['gym_name']} ({badge['location_name']})
+ First victory: {badge_date} | Total victories: {badge['victories']} | Highest difficulty: Level {badge['highest_difficulty']}
""" else: badges_html = """ -
-
🏆
-

No gym badges yet

-

Challenge gyms to earn badges and prove your training skills!

+
+ No gym badges yet. Challenge gyms to earn badges and prove your training skills!
""" # Build encounters HTML @@ -2273,495 +1709,283 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): encounter_date = 'Unknown' encounters_html += f""" -
-
- {encounter['species_name']} - {type_str} -
-
- Encountered {encounter['total_encounters']} times - Caught {encounter['caught_count']} times -
-
First seen: {encounter_date}
+
+ {encounter['species_name']} {type_str}
+ Encountered {encounter['total_encounters']} times | Caught {encounter['caught_count']} times
+ First seen: {encounter_date}
""" else: encounters_html = """ -
-
👁️
-

No pets encountered yet

-

Use !explore to discover wild pets!

+
+ No pets encountered yet. Use !explore to discover wild pets!
""" - # Build content for the unified template - content = f""" -
-

🐾 {nickname}'s Profile

-

Level {player['level']} Trainer

-

Currently in {player.get('location_name', 'Unknown Location')}

- -
+ html = f""" + + + + + PetBot - {nickname}'s Profile + ", additional_css + "") + }} + + + + ← Back to Game Hub + +
+

🐾 {nickname}'s Profile

+

Level {player['level']} Trainer

+

Currently in {player.get('location_name', 'Unknown Location')}

+ +
+ +
+
📊 Player Statistics
+
+
+
+
{player['level']}
+
Level
+
+
+
{player['experience']}
+
Experience
+
+
+
${player['money']}
+
Money
+
+
+
{total_pets}
+
Pets Caught
+
+
+
{active_count}
+
Active Pets
+
+
+
{len(achievements)}
+
Achievements
+
+
+
{encounter_stats.get('species_encountered', 0)}
+
Species Seen
+
+
+
{encounter_stats.get('completion_percentage', 0)}%
+
Petdex Complete
+
+
+
+
+ +
+
🐾 Pet Collection
+
+ + + + + + + + + + + + + + {pets_html} + +
StatusNameSpeciesTypeLevelHPStats
+
+
+ +
+
🏆 Achievements
+
+ {achievements_html} +
+
+ +
+
🎒 Inventory
+
+ {inventory_html} +
+
+ +
+
🏆 Gym Badges
+
+ {badges_html} +
+
+ +
+
👁️ Pet Encounters
+
+
+

Species discovered: {encounter_stats.get('species_encountered', 0)}/{encounter_stats.get('total_species', 0)} + ({encounter_stats.get('completion_percentage', 0)}% complete)

+

Total encounters: {encounter_stats.get('total_encounters', 0)}

+
+ {encounters_html} +
+
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def log_message(self, format, *args): """Override to reduce logging noise""" @@ -2801,51 +2025,31 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.serve_player_error(nickname, f"Error loading team builder: {str(e)}") def serve_teambuilder_no_pets(self, nickname): - """Show message when player has no pets using unified template""" - content = f""" -
-

🐾 Team Builder

-

Build your perfect team for battles and adventures

-
- -
-

🐾 No Pets Found

-

{nickname}, you need to catch some pets before using the team builder!

-

Head to the IRC channel and use !explore to find wild pets!

- ← Back to Profile -
- """ - - # Add no-pets-specific CSS - additional_css = """ - .main-container { - text-align: center; - max-width: 800px; - margin: 0 auto; - } - - .no-pets-message { - background: var(--bg-secondary); - padding: 40px; - border-radius: 15px; - border: 2px solid var(--warning-color); - margin-top: 30px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - } - - .no-pets-message h2 { - color: var(--warning-color); - margin-top: 0; - } - """ - - html_content = self.get_page_template(f"Team Builder - {nickname}", content, "players") - html_content = html_content.replace("", additional_css + "") + """Show message when player has no pets""" + html = f""" + + + + + Team Builder - {nickname} + + + +
+

🐾 No Pets Found

+

{nickname}, you need to catch some pets before using the team builder!

+

← Back to Profile

+
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_teambuilder_interface(self, nickname, pets): """Serve the full interactive team builder interface""" @@ -2870,14 +2074,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): type_str += f"/{pet['type2']}" # Debug logging - print(f"Making pet card for {name} (ID: {pet['id']}): is_active={pet['is_active']}, passed_is_active={is_active}, status_class={status_class}, team_order={pet.get('team_order', 'None')}") + print(f"Making pet card for {name} (ID: {pet['id']}): is_active={pet['is_active']}, passed_is_active={is_active}, status_class={status_class}") # Calculate HP percentage for health bar hp_percent = (pet['hp'] / pet['max_hp']) * 100 if pet['max_hp'] > 0 else 0 hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336" return f""" -
+

{name}

{status}
@@ -2917,15 +2121,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - # Create 6 numbered slots and place pets in their positions - team_slots = [''] * 6 # Initialize 6 empty slots - - # Place active pets in their team_order positions - for pet in active_pets: - team_order = pet.get('team_order') - if team_order and 1 <= team_order <= 6: - team_slots[team_order - 1] = make_pet_card(pet, True) - + active_cards = ''.join(make_pet_card(pet, True) for pet in active_pets) storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets) html = f""" @@ -3007,75 +2203,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): min-height: 200px; }} - .team-slots-container {{ - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 15px; - min-height: 400px; - }} - - .team-slot {{ - background: var(--bg-tertiary); - border: 2px dashed #666; - border-radius: 12px; - padding: 10px; - position: relative; - min-height: 120px; - transition: all 0.3s ease; - display: flex; - flex-direction: column; - }} - - .team-slot:hover {{ - border-color: var(--text-accent); - background: var(--drag-hover); - }} - - .team-slot.drag-over {{ - border-color: var(--text-accent); - background: var(--drag-hover); - border-style: solid; - transform: scale(1.02); - }} - - .slot-number {{ - position: absolute; - top: 5px; - left: 5px; - background: var(--active-color); - color: white; - width: 24px; - height: 24px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.9em; - font-weight: bold; - z-index: 10; - }} - - .slot-content {{ - flex: 1; - display: flex; - align-items: center; - justify-content: center; - position: relative; - }} - - .slot-content:empty::before {{ - content: "Empty Slot"; - color: var(--text-secondary); - font-style: italic; - opacity: 0.7; - }} - - .slot-content .pet-card {{ - margin: 0; - width: 100%; - max-width: none; - }} - .pet-card {{ background: var(--bg-tertiary); border-radius: 12px; @@ -3372,31 +2499,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
⭐ Active Team
-
-
-
1
-
{team_slots[0]}
-
-
-
2
-
{team_slots[1]}
-
-
-
3
-
{team_slots[2]}
-
-
-
4
-
{team_slots[3]}
-
-
-
5
-
{team_slots[4]}
-
-
-
6
-
{team_slots[5]}
-
+
+ {active_cards} +
+
+ Drop pets here to add to your active team
@@ -3471,23 +2578,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Add double-click handler card.addEventListener('dblclick', function() {{ const petId = this.dataset.petId; - const currentPosition = currentTeam[petId]; + const isActive = currentTeam[petId]; - console.log(`Double-click: Moving pet ${{petId}} from ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); + console.log(`Double-click: Moving pet ${{petId}} from ${{isActive ? 'active' : 'storage'}} to ${{isActive ? 'storage' : 'active'}}`); - if (currentPosition) {{ + if (isActive) {{ movePetToStorage(petId); }} else {{ - // Find first empty slot - for (let i = 1; i <= 6; i++) {{ - const slot = document.getElementById(`slot-${{i}}`); - const slotContent = slot.querySelector('.slot-content'); - if (slotContent.children.length === 0) {{ - movePetToTeamSlot(petId, i); - return; - }} - }} - console.log('No empty slots available'); + movePetToActive(petId); }} }}); @@ -3504,42 +2602,49 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} // Declare container variables once at the top level - const teamSlotsContainer = document.getElementById('team-slots-container'); + const activeContainer = document.getElementById('active-container'); const storageContainer = document.getElementById('storage-container'); + const activeDrop = document.getElementById('active-drop'); const storageDrop = document.getElementById('storage-drop'); - const teamSlots = Array.from({{length: 6}}, (_, i) => document.getElementById(`slot-${{i + 1}}`)); // Initialize team state with detailed debugging console.log('=== TEAM STATE INITIALIZATION ==='); const allCards = document.querySelectorAll('.pet-card'); console.log(`Found ${{allCards.length}} pet cards total`); - console.log(`Team slots container:`, teamSlotsContainer); + console.log(`Active container has ${{activeContainer.children.length}} pets initially`); console.log(`Storage container has ${{storageContainer.children.length}} pets initially`); - let teamPositions = {{}}; // Track pet positions in team slots - allCards.forEach((card, index) => {{ const petId = card.dataset.petId; const isActive = card.dataset.active === 'true'; - const teamOrder = card.dataset.teamOrder; + const currentContainer = card.parentElement.id; - console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, team_order=${{teamOrder}}`); + console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, currentContainer=${{currentContainer}}`); - if (isActive && teamOrder) {{ - teamPositions[petId] = parseInt(teamOrder); - originalTeam[petId] = parseInt(teamOrder); - currentTeam[petId] = parseInt(teamOrder); + originalTeam[petId] = isActive; + currentTeam[petId] = isActive; + + // CRITICAL: Verify container placement is correct - DO NOT MOVE unless absolutely necessary + const expectedContainer = isActive ? activeContainer : storageContainer; + const expectedContainerId = isActive ? 'active-container' : 'storage-container'; + + if (currentContainer !== expectedContainerId) {{ + console.error(`MISMATCH! Pet ${{petId}} is in ${{currentContainer}} but should be in ${{expectedContainerId}} based on data-active=${{card.dataset.active}}`); + console.log(`Moving pet ${{petId}} to correct container...`); + expectedContainer.appendChild(card); }} else {{ - originalTeam[petId] = false; - currentTeam[petId] = false; + console.log(`Pet ${{petId}} correctly placed in ${{currentContainer}}`); }} }}); - console.log('Team positions:', teamPositions); + console.log('After initialization:'); + console.log(`Active container now has ${{activeContainer.children.length}} pets`); + console.log(`Storage container now has ${{storageContainer.children.length}} pets`); + console.log('Original team state:', originalTeam); console.log('Current team state:', currentTeam); - // Completely rewritten drag and drop for slot system + // Completely rewritten drag and drop - simpler approach function initializeDragAndDrop() {{ console.log('Initializing drag and drop...'); @@ -3573,43 +2678,43 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }}); - // Set up team slot drop zones - teamSlots.forEach((slot, index) => {{ - const position = index + 1; - - slot.addEventListener('dragover', function(e) {{ - e.preventDefault(); - if (e.dataTransfer) {{ - e.dataTransfer.dropEffect = 'move'; - }} - }}); - - slot.addEventListener('dragenter', function(e) {{ - e.preventDefault(); - this.classList.add('drag-over'); - console.log(`DRAGENTER: Team slot ${{position}}`); - }}); - - slot.addEventListener('dragleave', function(e) {{ - if (!this.contains(e.relatedTarget)) {{ - this.classList.remove('drag-over'); - }} - }}); - - slot.addEventListener('drop', function(e) {{ - e.preventDefault(); - console.log(`DROP: Team slot ${{position}}`); - this.classList.remove('drag-over'); + // Set up drop zones (using previously declared variables) + + [activeContainer, activeDrop].forEach(zone => {{ + if (zone) {{ + zone.addEventListener('dragover', function(e) {{ + e.preventDefault(); + if (e.dataTransfer) {{ + e.dataTransfer.dropEffect = 'move'; + }} + }}); - if (draggedElement) {{ - const petId = draggedElement.dataset.petId; - console.log(`Moving pet ${{petId}} to team slot ${{position}}`); - movePetToTeamSlot(petId, position); - }} - }}); + zone.addEventListener('dragenter', function(e) {{ + e.preventDefault(); + this.classList.add('drag-over'); + console.log('DRAGENTER: Active zone'); + }}); + + zone.addEventListener('dragleave', function(e) {{ + if (!this.contains(e.relatedTarget)) {{ + this.classList.remove('drag-over'); + }} + }}); + + zone.addEventListener('drop', function(e) {{ + e.preventDefault(); + console.log('DROP: Active zone'); + this.classList.remove('drag-over'); + + if (draggedElement) {{ + const petId = draggedElement.dataset.petId; + console.log('Moving pet', petId, 'to active'); + movePetToActive(petId); + }} + }}); + }} }}); - // Set up storage drop zones [storageContainer, storageDrop].forEach(zone => {{ if (zone) {{ zone.addEventListener('dragover', function(e) {{ @@ -3648,48 +2753,39 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): console.log('Drag and drop initialization complete'); }} - function movePetToTeamSlot(petId, position) {{ - console.log(`movePetToTeamSlot called for pet ${{petId}}, position ${{position}}`); + function movePetToActive(petId) {{ + console.log(`movePetToActive called for pet ${{petId}}`); const card = document.querySelector(`[data-pet-id="${{petId}}"]`); if (!card) {{ console.log(`No card found for pet ${{petId}}`); return; }} - const slot = document.getElementById(`slot-${{position}}`); - if (!slot) {{ - console.log(`No slot found for position ${{position}}`); - return; + const currentIsActive = currentTeam[petId]; + + console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); + + if (!currentIsActive) {{ + console.log(`Moving pet ${{petId}} to active...`); + + // Update state + currentTeam[petId] = true; + + // Move DOM element + card.classList.remove('storage'); + card.classList.add('active'); + card.dataset.active = 'true'; + card.querySelector('.status-badge').textContent = 'Active'; + activeContainer.appendChild(card); + + // Update interface + updateSaveButton(); + updateDropZoneVisibility(); + + console.log('Pet moved to active successfully'); + }} else {{ + console.log(`Pet ${{petId}} is already active, no move needed`); }} - - const slotContent = slot.querySelector('.slot-content'); - - // Check if slot is already occupied - if (slotContent.children.length > 0) {{ - console.log(`Slot ${{position}} is already occupied, swapping pets`); - const existingCard = slotContent.querySelector('.pet-card'); - if (existingCard) {{ - const existingPetId = existingCard.dataset.petId; - // Move existing pet to storage - movePetToStorage(existingPetId); - }} - }} - - // Update state - currentTeam[petId] = position; - - // Move DOM element - card.classList.remove('storage'); - card.classList.add('active'); - card.dataset.active = 'true'; - card.dataset.teamOrder = position; - card.querySelector('.status-badge').textContent = 'Active'; - slotContent.appendChild(card); - - // Update interface - updateSaveButton(); - - console.log(`Pet moved to team slot ${{position}} successfully`); }} function movePetToStorage(petId) {{ @@ -3700,11 +2796,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return; }} - const currentPosition = currentTeam[petId]; + const currentIsActive = currentTeam[petId]; - console.log(`Pet ${{petId}} current state: ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); + console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); - if (currentPosition) {{ + if (currentIsActive) {{ console.log(`Moving pet ${{petId}} to storage...`); // Update state @@ -3714,7 +2810,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): card.classList.remove('active'); card.classList.add('storage'); card.dataset.active = 'false'; - card.dataset.teamOrder = ''; card.querySelector('.status-badge').textContent = 'Storage'; storageContainer.appendChild(card); @@ -3730,7 +2825,16 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): function updateDropZoneVisibility() {{ - // Update storage drop zone visibility + // Using previously declared container variables + + // CRITICAL: Only update visual indicators, never move pets + // Use CSS classes instead of direct style manipulation + if (activeContainer && activeContainer.children.length > 0) {{ + if (activeDrop) activeDrop.classList.add('has-pets'); + }} else {{ + if (activeDrop) activeDrop.classList.remove('has-pets'); + }} + if (storageContainer && storageContainer.children.length > 0) {{ if (storageDrop) storageDrop.classList.add('has-pets'); }} else {{ @@ -3738,6 +2842,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} console.log('Drop zone visibility updated:', {{ + activeContainerPets: activeContainer ? activeContainer.children.length : 0, storageContainerPets: storageContainer ? storageContainer.children.length : 0 }}); }} @@ -3901,716 +3006,10 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ - # Generate storage pets HTML first - storage_pets_html = "" - for pet in inactive_pets: - storage_pets_html += make_pet_card(pet, False) - - # Generate active pets HTML for team slots - active_pets_html = "" - for pet in active_pets: - if pet.get('team_order'): - active_pets_html += make_pet_card(pet, True) - - # Create content using string concatenation instead of f-strings to avoid CSS brace issues - team_builder_content = """ - - -
-
-

🐾 Team Builder

-

Drag pets between Active Team and Storage. Double-click as backup.

-
- -
-
-

⚔️ Active Team (1-6 pets)

-
-
-
Slot 1 (Leader)
-
-
Drop pet here
-
-
-
-
Slot 2
-
-
Drop pet here
-
-
-
-
Slot 3
-
-
Drop pet here
-
-
-
-
Slot 4
-
-
Drop pet here
-
-
-
-
Slot 5
-
-
Drop pet here
-
-
-
-
Slot 6
-
-
Drop pet here
-
-
-
-
- -
-

📦 Storage

-
- """ + storage_pets_html + active_pets_html + """ -
-
-
- -
- - ← Back to Profile -
- Changes are saved securely with PIN verification via IRC -
-
- -
-

🔐 PIN Verification Required

-

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

-

Enter the PIN below to confirm your team changes:

- - -
-
-
- -
- 💡 How to use:
- • Drag pets to team slots
- • Double-click to move pets
- • Empty slots show placeholders -
- - - """ - - # Get the unified template - html_content = self.get_page_template(f"Team Builder - {nickname}", team_builder_content, "") - self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def handle_team_save(self, nickname): """Handle team save request and generate PIN""" @@ -4746,94 +3145,90 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"🔐 PIN for {nickname}: {pin_code}") # Try to send via IRC bot if available - if self.bot and hasattr(self.bot, 'send_message'): - try: - # Send PIN via private message - self.bot.send_message(nickname, f"🔐 Team Builder PIN: {pin_code}") - self.bot.send_message(nickname, f"💡 Enter this PIN on the web page to confirm your team changes. PIN expires in 10 minutes.") - print(f"✅ PIN sent to {nickname} via IRC") - except Exception as e: - print(f"❌ Failed to send PIN via IRC: {e}") - else: - print(f"❌ No IRC bot available to send PIN to {nickname}") - print(f"💡 Manual PIN for {nickname}: {pin_code}") - + try: + # Check if the bot instance is accessible via global state + import sys + if hasattr(sys.modules.get('__main__'), 'bot_instance'): + bot = sys.modules['__main__'].bot_instance + if hasattr(bot, 'send_message'): + # Send directly via bot's send_message method (non-async) + message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)" + bot.send_message(nickname, message) + print(f"✅ PIN sent to {nickname} via IRC") + return + except Exception as e: + print(f"Could not send PIN via IRC bot: {e}") + + # Fallback: just print to console for now + print(f"⚠️ IRC bot not available - PIN displayed in console only") + + def send_json_response(self, data, status_code=200): + """Send JSON response""" + import json + response = json.dumps(data) + + self.send_response(status_code) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(response.encode()) class PetBotWebServer: - """Standalone web server for PetBot""" - - def __init__(self, database=None, port=8080, bot=None): - self.database = database or Database() + def __init__(self, database, port=8080): + self.database = database self.port = port - self.bot = bot self.server = None - + def run(self): - """Start the web server""" + """Start the HTTP web server""" + print(f"🌐 Starting PetBot web server on http://0.0.0.0:{self.port}") + print(f"📡 Accessible from WSL at: http://172.27.217.61:{self.port}") + print(f"📡 Accessible from Windows at: http://localhost:{self.port}") self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler) + # Pass database to the server so handlers can access it self.server.database = self.database - self.server.bot = self.bot - - print(f'🌐 Starting PetBot web server on http://0.0.0.0:{self.port}') - print(f'📡 Accessible from WSL at: http://172.27.217.61:{self.port}') - print(f'📡 Accessible from Windows at: http://localhost:{self.port}') - print('') - print('🌐 Public access at: http://petz.rdx4.com/') - print('') - self.server.serve_forever() def start_in_thread(self): """Start the web server in a background thread""" - import threading - self.thread = threading.Thread(target=self.run, daemon=True) - self.thread.start() - - def stop(self): - """Stop the web server""" - if self.server: - self.server.shutdown() - self.server.server_close() + thread = Thread(target=self.run, daemon=True) + thread.start() + print(f"✅ Web server started at http://localhost:{self.port}") + print(f"🌐 Public access at: http://petz.rdx4.com/") + return thread def run_standalone(): - """Run the web server in standalone mode""" - import sys + """Run the web server standalone for testing""" + print("🐾 PetBot Web Server (Standalone Mode)") + print("=" * 40) - port = 8080 - if len(sys.argv) > 1: - try: - port = int(sys.argv[1]) - except ValueError: - print('Usage: python webserver.py [port]') - sys.exit(1) + # Initialize database + database = Database() + # Note: In standalone mode, we can't easily run async init + # This is mainly for testing the web routes - server = PetBotWebServer(port) - - print('🌐 PetBot Web Server') - print('=' * 50) - print(f'Port: {port}') - print('') - print('🔗 Local URLs:') - print(f' http://localhost:{port}/ - Game Hub (local)') - print(f' http://localhost:{port}/help - Command Help (local)') - print(f' http://localhost:{port}/players - Player List (local)') - print(f' http://localhost:{port}/leaderboard - Leaderboard (local)') - print(f' http://localhost:{port}/locations - Locations (local)') - print('') - print('🌐 Public URLs:') - print(' http://petz.rdx4.com/ - Game Hub') - print(' http://petz.rdx4.com/help - Command Help') - print(' http://petz.rdx4.com/players - Player List') - print(' http://petz.rdx4.com/leaderboard - Leaderboard') - print(' http://petz.rdx4.com/locations - Locations') - print('') - print('Press Ctrl+C to stop') + # Start web server + server = PetBotWebServer(database) + print("🚀 Starting web server...") + print("📝 Available routes:") + print(" http://localhost:8080/ - Game Hub (local)") + print(" http://localhost:8080/help - Command Help (local)") + print(" http://localhost:8080/players - Player List (local)") + print(" http://localhost:8080/leaderboard - Leaderboard (local)") + print(" http://localhost:8080/locations - Locations (local)") + print("") + print("🌐 Public URLs:") + print(" http://petz.rdx4.com/ - Game Hub") + print(" http://petz.rdx4.com/help - Command Help") + print(" http://petz.rdx4.com/players - Player List") + print(" http://petz.rdx4.com/leaderboard - Leaderboard") + print(" http://petz.rdx4.com/locations - Locations") + print("") + print("Press Ctrl+C to stop") try: server.run() except KeyboardInterrupt: - print('\n✅ Web server stopped') - -if __name__ == '__main__': - run_standalone() + print("\n✅ Web server stopped") +if __name__ == "__main__": + run_standalone() \ No newline at end of file