diff --git a/CHANGELOG.md b/CHANGELOG.md index 5077ca2..84e2dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +99,6 @@ 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 a74469f..ff24e94 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, swap team members +- **Team Management**: Activate/deactivate pets, manage team composition - **Achievement System**: Unlock new areas by completing challenges - **Item Collection**: Discover and collect useful items during exploration @@ -78,7 +78,6 @@ 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 0d39133..534717a 100644 --- a/help.html +++ b/help.html @@ -430,11 +430,6 @@
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 7b4f805..98c5ccf 100644 --- a/modules/achievements.py +++ b/modules/achievements.py @@ -19,14 +19,12 @@ class Achievements(BaseModule): if not player: return - achievements = await self.database.get_player_achievements(player["id"]) + # 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"🏆 {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!") + self.send_message(channel, f"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.") else: - self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!") \ No newline at end of file + self.send_message(channel, f"💡 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 8a4854d..e539a1a 100644 --- a/modules/base_module.py +++ b/modules/base_module.py @@ -12,6 +12,15 @@ 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 d50abeb..911392f 100644 --- a/modules/battle_system.py +++ b/modules/battle_system.py @@ -52,6 +52,12 @@ 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: @@ -87,7 +93,7 @@ class BattleSystem(BaseModule): if not player: return - move_name = " ".join(args).title() # Normalize to Title Case + move_name = " ".join(self.normalize_input(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 c723d4b..ec4c497 100644 --- a/modules/core_commands.py +++ b/modules/core_commands.py @@ -40,5 +40,8 @@ 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']}") \ No newline at end of file + 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 diff --git a/modules/exploration.py b/modules/exploration.py index 485a292..c221cf8 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"] + return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture", "flee"] async def handle_command(self, channel, nickname, command, args): if command == "explore": @@ -22,6 +22,8 @@ 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""" @@ -29,6 +31,18 @@ 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": @@ -51,7 +65,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, or !catch to try catching it directly!") + self.send_message(channel, f"Choose your action: !battle to fight it, !catch to try catching it directly, or !flee to escape!") async def cmd_travel(self, channel, nickname, args): """Travel to a different location""" @@ -64,7 +78,7 @@ class Exploration(BaseModule): return # Handle various input formats and normalize location names - destination_input = " ".join(args).lower() + destination_input = self.normalize_input(" ".join(args)) # Map common variations to exact location names location_mappings = { @@ -82,7 +96,7 @@ class Exploration(BaseModule): destination = location_mappings.get(destination_input) if not destination: # Fall back to title case if no mapping found - destination = " ".join(args).title() + destination = " ".join(self.normalize_input(args)).title() location = await self.database.get_location_by_name(destination) @@ -171,7 +185,7 @@ class Exploration(BaseModule): if args: # Specific location requested - location_name = " ".join(args).title() + location_name = " ".join(self.normalize_input(args)).title() else: # Default to current location current_location = await self.database.get_player_location(player["id"]) @@ -196,6 +210,13 @@ 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"] @@ -297,4 +318,36 @@ 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) \ No newline at end of file + 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 diff --git a/modules/gym_battles.py b/modules/gym_battles.py index 57665c5..0fbd481 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 args[0] == "list": + elif self.normalize_input(args[0]) == "list": await self.cmd_gym_list_all(channel, nickname) - elif args[0] == "challenge": + elif self.normalize_input(args[0]) == "challenge": await self.cmd_gym_challenge(channel, nickname, args[1:]) - elif args[0] == "info": + elif self.normalize_input(args[0]) == "info": await self.cmd_gym_info(channel, nickname, args[1:]) - elif args[0] == "status": + elif self.normalize_input(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!") + f"💡 Use '!gym challenge' to battle (gym name optional if only one gym in location)!") async def cmd_gym_list_all(self, channel, nickname): """List all gyms across all locations""" @@ -97,10 +97,6 @@ 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 @@ -111,19 +107,37 @@ class GymBattles(BaseModule): self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.") return - gym_name = " ".join(args).strip('"') + # 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 - # 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 = 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 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}") - else: - self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.") - return + return # Check if player has active pets active_pets = await self.database.get_active_pets(player["id"]) @@ -266,7 +280,7 @@ class GymBattles(BaseModule): if not player: return - gym_name = " ".join(args).strip('"') + gym_name = " ".join(self.normalize_input(args)).strip('"') # First try to find gym in player's current location location = await self.database.get_player_location(player["id"]) @@ -311,7 +325,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}") + 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") async def cmd_forfeit(self, channel, nickname): """Forfeit the current gym battle""" diff --git a/modules/inventory.py b/modules/inventory.py index 1fe60ef..9994ad6 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -16,51 +16,14 @@ class Inventory(BaseModule): await self.cmd_use_item(channel, nickname, args) async def cmd_inventory(self, channel, nickname): - """Display player's inventory""" + """Redirect player to their web profile for inventory management""" player = await self.require_player(channel, nickname) if not player: return - 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!") + # 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!") async def cmd_use_item(self, channel, nickname, args): """Use an item from inventory""" @@ -72,7 +35,7 @@ class Inventory(BaseModule): if not player: return - item_name = " ".join(args) + item_name = " ".join(self.normalize_input(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 26ffb25..911bef5 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", "swap", "nickname"] + return ["team", "pets", "activate", "deactivate", "nickname"] async def handle_command(self, channel, nickname, command, args): if command == "team": @@ -18,8 +18,6 @@ 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) @@ -40,7 +38,7 @@ class PetManagement(BaseModule): team_info = [] - # Active pets with star + # Active pets with star and team position for pet in active_pets: name = pet["nickname"] or pet["species_name"] @@ -55,7 +53,9 @@ class PetManagement(BaseModule): else: exp_display = f"{exp_needed} to next" - team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}") + # 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}") # Inactive pets for pet in inactive_pets[:5]: # Show max 5 inactive @@ -66,6 +66,7 @@ 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""" @@ -74,7 +75,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}") + self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}#pets") async def cmd_activate(self, channel, nickname, args): """Activate a pet for battle (PM only)""" @@ -88,13 +89,14 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(args) + pet_name = " ".join(self.normalize_input(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"] - self.send_pm(nickname, f"✅ {display_name} is now active for battle!") + position = result.get("team_position", "?") + self.send_pm(nickname, f"✅ {display_name} is now active for battle! Team position: {position}") self.send_message(channel, f"{nickname}: Pet activated successfully!") else: self.send_pm(nickname, f"❌ {result['error']}") @@ -112,7 +114,7 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(args) + pet_name = " ".join(self.normalize_input(args)) result = await self.database.deactivate_pet(player["id"], pet_name) if result["success"]: @@ -124,44 +126,6 @@ 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: @@ -174,7 +138,7 @@ class PetManagement(BaseModule): return # Split args into pet identifier and new nickname - pet_identifier = args[0] + pet_identifier = self.normalize_input(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 5e79860..03d9982 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) + self.web_server = PetBotWebServer(self.database, port=8080, bot=self) self.web_server.start_in_thread() print("✅ Web server started") @@ -303,12 +303,14 @@ 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 = command_parts[0].lower() - args = command_parts[1:] + command = BaseModule.normalize_input(command_parts[0]) + args = BaseModule.normalize_input(command_parts[1:]) try: if command in self.command_map: diff --git a/src/database.py b/src/database.py index 1b89401..52eb5bd 100644 --- a/src/database.py +++ b/src/database.py @@ -120,6 +120,46 @@ 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, @@ -408,6 +448,9 @@ 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] @@ -638,11 +681,16 @@ class Database: if not pet: return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"} - # Activate the pet - await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],)) + # 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"])) await db.commit() - return {"success": True, "pet": dict(pet)} + return {"success": True, "pet": dict(pet), "team_position": next_slot} async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict: """Deactivate a pet by name or species name. Returns result dict.""" @@ -670,58 +718,122 @@ class Database: if active_count["count"] <= 1: return {"success": False, "error": "You must have at least one active pet!"} - # Deactivate the pet - await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],)) + # Deactivate the pet and clear team order + await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],)) await db.commit() return {"success": True, "pet": dict(pet)} - 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.""" + # Team Order Methods + async def get_next_available_team_slot(self, player_id: int) -> int: + """Get the next available team slot (1-6)""" async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Find both pets cursor = await db.execute(""" - 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() + 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()] + # 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 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() + 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() - 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"} + if existing_pet: + return {"success": False, "error": f"Position {position} is already taken"} - if pet1["id"] == pet2["id"]: - return {"success": False, "error": "Cannot swap a pet with itself"} + # 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)) - # 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"} - 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" - } + # 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"} # Item and Inventory Methods async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool: @@ -873,7 +985,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.id ASC + ORDER BY p.team_order ASC, p.id ASC """, (player_id,)) rows = await cursor.fetchall() return [dict(row) for row in rows] @@ -1601,12 +1713,18 @@ class Database: # Begin transaction await db.execute("BEGIN TRANSACTION") - # 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)) + # 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)) # Mark any pending change as verified await db.execute(""" @@ -1713,18 +1831,28 @@ class Database: # Get current pet states cursor = await db.execute(""" - SELECT id, is_active FROM pets WHERE player_id = ? + SELECT id, is_active, team_order FROM pets WHERE player_id = ? """, (player_id,)) - current_pets = {str(row["id"]): bool(row["is_active"]) for row in await cursor.fetchall()} + current_pets = {str(row["id"]): row["team_order"] if row["is_active"] else False for row in await cursor.fetchall()} # Apply proposed changes to current state new_state = current_pets.copy() - for pet_id, new_active_state in proposed_changes.items(): + for pet_id, new_position in proposed_changes.items(): if pet_id in new_state: - new_state[pet_id] = new_active_state + new_state[pet_id] = new_position - # Count active pets in new state - active_count = sum(1 for is_active in new_state.values() if is_active) + # 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"} # Validate constraints if active_count < 1: diff --git a/src/game_engine.py b/src/game_engine.py index 59af9af..1906fd0 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + attack, defense, speed, is_active, team_order) + 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)) + pet_data["defense"], pet_data["speed"], True, 1)) await db.commit() diff --git a/webserver.py b/webserver.py index 1d83c2a..709bd1f 100644 --- a/webserver.py +++ b/webserver.py @@ -25,6 +25,529 @@ 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) @@ -68,139 +591,501 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): def serve_index(self): """Serve the main index page""" - 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

-
- -""" + html = self.get_page_template("PetBot Game Hub", content, "") 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""" - 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") + """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()) def serve_players(self): """Serve the players page with real data""" @@ -268,6 +1153,34 @@ 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 = "" @@ -294,175 +1207,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ - 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
-
- + # Build table content + table_content = f""" +
+

🏆 Player Rankings

+
@@ -480,14 +1229,24 @@ 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') @@ -495,76 +1254,49 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.wfile.write(html.encode()) def serve_error_page(self, page_name, error_msg): - """Serve a generic error page""" - html = f""" - - - - - PetBot - Error - - - - ← Back to Game Hub - -
-

⚠️ Error Loading {page_name}

-
- -
-

Unable to load page

-

{error_msg}

-

Please try again later or contact an administrator.

-
- -""" + border: 2px solid var(--error-color); + margin-top: 30px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + } + + .error-message h2 { + color: var(--error-color); + margin-top: 0; + } + """ + + html_content = self.get_page_template("Error", content, "") + html_content = html_content.replace("", additional_css + "") self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html.encode()) + self.wfile.write(html_content.encode()) def serve_leaderboard(self): """Serve the leaderboard page - redirect to players for now""" @@ -635,7 +1367,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return [] def serve_locations_data(self, locations_data): - """Serve locations page with real data""" + """Serve locations page with real data using unified template""" # Build locations HTML locations_html = "" @@ -699,119 +1431,133 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - html = f""" - - - - - PetBot - Locations - - - - ← 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 -

-
- -""" + } + """ + + # Get the unified template with additional CSS + html_content = self.get_page_template("Game Locations", content, "locations") + # 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.encode()) + self.wfile.write(html_content.encode()) def serve_petdex(self): """Serve the petdex page with all pet species data""" @@ -899,9 +1648,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): try: import aiosqlite async with aiosqlite.connect(database.db_path) as db: - # Get all pet species with evolution information + # Get all pet species with evolution information (no duplicates) cursor = await db.execute(""" - SELECT ps.*, + SELECT DISTINCT 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 @@ -952,6 +1701,40 @@ 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'] @@ -1034,207 +1817,24 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):

The petdex appears to be empty. Contact an administrator.

""" - 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
-
+ # Combine all content + content = f""" +
+

📖 Petdex

+

Complete encyclopedia of all available pets

-

🎯 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() @@ -1321,7 +1921,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 - 'species_name': row[14], 'type1': row[15], 'type2': row[16] + 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17] } pets.append(pet_dict) @@ -1426,148 +2026,94 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return None def serve_player_not_found(self, nickname): - """Serve player not found page""" - html = f""" - - - - - PetBot - Player Not Found - - - - ← 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!

-
- -""" + border: 2px solid var(--error-color); + margin-top: 30px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + } + + .error-message h2 { + color: var(--error-color); + margin-top: 0; + } + """ + + html_content = self.get_page_template("Player Not Found", content, "players") + html_content = html_content.replace("", additional_css + "") self.send_response(404) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html.encode()) + self.wfile.write(html_content.encode()) def serve_player_error(self, nickname, error_msg): - """Serve player error page""" - html = f""" - - - - - PetBot - Error - - - - ← Back to Game Hub - -
-

⚠️ Error

-
- -
-

Unable to load player data

-

{error_msg}

-

Please try again later or contact an administrator.

-
- -""" + border: 2px solid var(--error-color); + margin-top: 30px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + } + + .error-message h2 { + color: var(--error-color); + margin-top: 0; + } + """ + + html_content = self.get_page_template("Player Error", content, "players") + html_content = html_content.replace("", additional_css + "") self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html.encode()) + self.wfile.write(html_content.encode()) def serve_player_data(self, nickname, player_data): """Serve player profile page with real data""" @@ -1619,15 +2165,20 @@ 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 @@ -1654,15 +2205,19 @@ 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 @@ -1678,15 +2233,24 @@ 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 @@ -1709,283 +2273,495 @@ 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!

""" - html = f""" - - - - - PetBot - {nickname}'s Profile - - - - ← 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} -
-
- -""" + } + + .achievements-grid, .badges-grid, .encounters-grid, .inventory-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 15px; + } + + .achievement-card, .badge-card, .encounter-card, .inventory-item { + background: var(--bg-tertiary); + padding: 15px; + border-radius: 8px; + border-left: 4px solid var(--text-accent); + transition: transform 0.3s ease; + } + + .achievement-card:hover, .badge-card:hover, .encounter-card:hover, .inventory-item:hover { + transform: translateY(-3px); + } + + .achievement-card { + display: flex; + align-items: flex-start; + gap: 15px; + } + + .achievement-icon { + font-size: 1.5em; + flex-shrink: 0; + } + + .achievement-content h4 { + margin: 0 0 8px 0; + color: var(--text-accent); + } + + .achievement-content p { + margin: 0 0 8px 0; + color: var(--text-primary); + } + + .achievement-date { + color: var(--text-secondary); + font-size: 0.9em; + } + + .badge-card { + display: flex; + align-items: flex-start; + gap: 15px; + border-left-color: gold; + } + + .badge-icon { + font-size: 1.5em; + flex-shrink: 0; + } + + .badge-content h4 { + margin: 0 0 8px 0; + color: gold; + } + + .badge-content p { + margin: 0 0 10px 0; + color: var(--text-primary); + } + + .badge-stats { + display: flex; + flex-direction: column; + gap: 4px; + } + + .badge-stats span { + color: var(--text-secondary); + font-size: 0.9em; + } + + .encounter-card { + border-left: 4px solid var(--text-accent); + } + + .encounter-header { + margin-bottom: 10px; + } + + .encounter-stats { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 8px; + } + + .encounter-stats span { + color: var(--text-primary); + font-size: 0.9em; + } + + .encounter-date { + color: var(--text-secondary); + font-size: 0.9em; + } + + .inventory-item { + border-left: 4px solid var(--text-accent); + } + + .item-header { + margin-bottom: 8px; + } + + .item-description { + color: var(--text-primary); + margin-bottom: 8px; + } + + .item-meta { + color: var(--text-secondary); + font-size: 0.9em; + } + + .empty-state { + text-align: center; + padding: 40px; + color: var(--text-secondary); + } + + .empty-icon { + font-size: 3em; + margin-bottom: 15px; + } + + .empty-state h3 { + margin: 0 0 10px 0; + color: var(--text-primary); + } + + .empty-state p { + margin: 0; + font-size: 1.1em; + } + + .encounters-summary { + text-align: center; + margin-bottom: 20px; + padding: 15px; + background: var(--bg-tertiary); + border-radius: 8px; + } + + .encounters-summary p { + margin: 5px 0; + color: var(--text-secondary); + } + + .btn { + display: inline-block; + padding: 12px 24px; + border-radius: 6px; + text-decoration: none; + font-weight: 600; + text-align: center; + transition: all 0.3s ease; + border: none; + cursor: pointer; + } + + .btn-primary { + background: var(--gradient-primary); + color: white; + } + + .btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + } + + /* Mobile Responsive */ + @media (max-width: 768px) { + .stats-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + } + + .stat-card { + padding: 15px; + } + + .stat-value { + font-size: 1.5em; + } + + .achievements-grid, .badges-grid, .encounters-grid, .inventory-grid { + grid-template-columns: 1fr; + } + + .nav-pill { + padding: 6px 12px; + font-size: 0.8em; + margin: 3px; + } + + .pets-table { + min-width: 500px; + } + + .pets-table th, .pets-table td { + padding: 8px 10px; + font-size: 0.9em; + } + } + """ + + # Get the unified template with the additional CSS + html_content = self.get_page_template(f"{nickname}'s Profile", content, "players") + 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.encode()) + self.wfile.write(html_content.encode()) def log_message(self, format, *args): """Override to reduce logging noise""" @@ -2025,31 +2801,51 @@ 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""" - html = f""" - - - - - Team Builder - {nickname} - - - -
-

🐾 No Pets Found

-

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

-

← Back to Profile

-
- -""" + """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 + "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() - self.wfile.write(html.encode()) + self.wfile.write(html_content.encode()) def serve_teambuilder_interface(self, nickname, pets): """Serve the full interactive team builder interface""" @@ -2074,14 +2870,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}") + 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')}") # 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}
@@ -2121,7 +2917,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - active_cards = ''.join(make_pet_card(pet, True) for pet in active_pets) + # 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) + storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets) html = f""" @@ -2203,6 +3007,75 @@ 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; @@ -2499,11 +3372,31 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
⭐ Active Team
-
- {active_cards} -
-
- Drop pets here to add to your 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]}
+
@@ -2578,14 +3471,23 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Add double-click handler card.addEventListener('dblclick', function() {{ const petId = this.dataset.petId; - const isActive = currentTeam[petId]; + const currentPosition = currentTeam[petId]; - console.log(`Double-click: Moving pet ${{petId}} from ${{isActive ? 'active' : 'storage'}} to ${{isActive ? 'storage' : 'active'}}`); + console.log(`Double-click: Moving pet ${{petId}} from ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); - if (isActive) {{ + if (currentPosition) {{ movePetToStorage(petId); }} else {{ - movePetToActive(petId); + // 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'); }} }}); @@ -2602,49 +3504,42 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} // Declare container variables once at the top level - const activeContainer = document.getElementById('active-container'); + const teamSlotsContainer = document.getElementById('team-slots-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(`Active container has ${{activeContainer.children.length}} pets initially`); + console.log(`Team slots container:`, teamSlotsContainer); 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 currentContainer = card.parentElement.id; + const teamOrder = card.dataset.teamOrder; - console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, currentContainer=${{currentContainer}}`); + console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, team_order=${{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); + if (isActive && teamOrder) {{ + teamPositions[petId] = parseInt(teamOrder); + originalTeam[petId] = parseInt(teamOrder); + currentTeam[petId] = parseInt(teamOrder); }} else {{ - console.log(`Pet ${{petId}} correctly placed in ${{currentContainer}}`); + originalTeam[petId] = false; + currentTeam[petId] = false; }} }}); - 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('Team positions:', teamPositions); console.log('Original team state:', originalTeam); console.log('Current team state:', currentTeam); - // Completely rewritten drag and drop - simpler approach + // Completely rewritten drag and drop for slot system function initializeDragAndDrop() {{ console.log('Initializing drag and drop...'); @@ -2678,43 +3573,43 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }}); - // 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'; - }} - }}); - - 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'); + // 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'); - - if (draggedElement) {{ - const petId = draggedElement.dataset.petId; - console.log('Moving pet', petId, 'to active'); - movePetToActive(petId); - }} - }}); - }} + }} + }}); + + slot.addEventListener('drop', function(e) {{ + e.preventDefault(); + console.log(`DROP: Team slot ${{position}}`); + this.classList.remove('drag-over'); + + if (draggedElement) {{ + const petId = draggedElement.dataset.petId; + console.log(`Moving pet ${{petId}} to team slot ${{position}}`); + movePetToTeamSlot(petId, position); + }} + }}); }}); + // Set up storage drop zones [storageContainer, storageDrop].forEach(zone => {{ if (zone) {{ zone.addEventListener('dragover', function(e) {{ @@ -2753,39 +3648,48 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): console.log('Drag and drop initialization complete'); }} - function movePetToActive(petId) {{ - console.log(`movePetToActive called for pet ${{petId}}`); + function movePetToTeamSlot(petId, position) {{ + console.log(`movePetToTeamSlot called for pet ${{petId}}, position ${{position}}`); const card = document.querySelector(`[data-pet-id="${{petId}}"]`); if (!card) {{ console.log(`No card found for pet ${{petId}}`); 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 slot = document.getElementById(`slot-${{position}}`); + if (!slot) {{ + console.log(`No slot found for position ${{position}}`); + return; }} + + 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) {{ @@ -2796,11 +3700,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return; }} - const currentIsActive = currentTeam[petId]; + const currentPosition = currentTeam[petId]; - console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); + console.log(`Pet ${{petId}} current state: ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); - if (currentIsActive) {{ + if (currentPosition) {{ console.log(`Moving pet ${{petId}} to storage...`); // Update state @@ -2810,6 +3714,7 @@ 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); @@ -2825,16 +3730,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): function updateDropZoneVisibility() {{ - // 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'); - }} - + // Update storage drop zone visibility if (storageContainer && storageContainer.children.length > 0) {{ if (storageDrop) storageDrop.classList.add('has-pets'); }} else {{ @@ -2842,7 +3738,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} console.log('Drop zone visibility updated:', {{ - activeContainerPets: activeContainer ? activeContainer.children.length : 0, storageContainerPets: storageContainer ? storageContainer.children.length : 0 }}); }} @@ -3006,10 +3901,716 @@ 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.encode()) + self.wfile.write(html_content.encode()) def handle_team_save(self, nickname): """Handle team save request and generate PIN""" @@ -3145,90 +4746,94 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"🔐 PIN for {nickname}: {pin_code}") # Try to send via IRC bot if available - 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()) + 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}") + class PetBotWebServer: - def __init__(self, database, port=8080): - self.database = database + """Standalone web server for PetBot""" + + def __init__(self, database=None, port=8080, bot=None): + self.database = database or Database() self.port = port + self.bot = bot self.server = None - + def run(self): - """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}") + """Start the web server""" 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""" - 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 + 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() def run_standalone(): - """Run the web server standalone for testing""" - print("🐾 PetBot Web Server (Standalone Mode)") - print("=" * 40) + """Run the web server in standalone mode""" + import sys - # Initialize database - database = Database() - # Note: In standalone mode, we can't easily run async init - # This is mainly for testing the web routes + port = 8080 + if len(sys.argv) > 1: + try: + port = int(sys.argv[1]) + except ValueError: + print('Usage: python webserver.py [port]') + sys.exit(1) - # 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") + 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') try: server.run() except KeyboardInterrupt: - print("\n✅ Web server stopped") + print('\n✅ Web server stopped') + +if __name__ == '__main__': + run_standalone() -if __name__ == "__main__": - run_standalone() \ No newline at end of file