Compare commits

..

10 commits

Author SHA1 Message Date
megaproxy
c8cb99a4d0 Update project documentation for web interface enhancements
- Updated CHANGELOG.md with comprehensive list of new features and fixes
- Enhanced README.md with updated feature descriptions and web interface capabilities
- Documented team builder functionality, navigation improvements, and bug fixes
- Added clear descriptions of IRC command redirects and web integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:59:47 +01:00
megaproxy
5ac3e36f0c Add unified navigation bar to all webserver pages
- Implemented comprehensive navigation system with hover dropdowns
- Added navigation to all webserver pages for consistent user experience
- Enhanced page templates with unified styling and active page highlighting
- Improved accessibility and discoverability of all bot features through web interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:59:16 +01:00
megaproxy
f7fe4ce034 Update help documentation to reflect web interface changes
- Updated command descriptions to reflect inventory redirect to web interface
- Improved documentation for web-based team building and inventory management
- Added clearer explanations of web interface features and navigation
- Maintained consistency between IRC commands and web functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:58:53 +01:00
megaproxy
ac655b07e6 Remove \!swap command and redirect pet management to web interface
- Removed \!swap command as team management moved to website
- Added redirect messages pointing users to team builder web interface
- Streamlined pet management workflow through unified web experience
- Maintained existing \!pets command functionality with web redirect

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:58:35 +01:00
megaproxy
d05b2ead53 Fix exploration and battle system state management bugs
- Fixed exploration bug: prevent multiple \!explore when encounter is active
- Fixed battle bug: prevent starting multiple battles from exploration encounters
- Enforced exploration encounter workflow: must choose fight/capture/flee before exploring again
- Fixed \!gym challenge to use player's current location instead of requiring location parameter
- Added proper state management to prevent race conditions
- Improved user experience with clear error messages for active encounters

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:58:18 +01:00
megaproxy
3c628c7f51 Implement team order persistence and validation system
- Added team_order column migration for numbered team slots (1-6)
- Implemented get_next_available_team_slot() method
- Added team composition validation for team builder
- Created pending team change system with PIN verification
- Added apply_team_change() for secure team updates
- Enhanced team management with proper ordering and constraints

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:57:54 +01:00
megaproxy
61463267c8 Redirect inventory commands to web interface with jump points
- Updated \!inventory, \!inv, and \!items commands to redirect to player profile
- Added #inventory jump point for direct section navigation
- Improved user experience with web-based inventory management
- Enhanced UX with helpful explanatory messages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:57:27 +01:00
megaproxy
30dcb7e4bc Update bot initialization to pass bot instance to webserver
- Modified PetBotWebServer instantiation to include bot parameter
- Enables IRC PIN delivery for team builder functionality
- Maintains existing webserver functionality while adding IRC integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:56:55 +01:00
megaproxy
ff14710987 Fix team builder interface and implement working drag-and-drop functionality
- Completely rewrote team builder with unified template system
- Fixed center alignment issues with proper CSS layout (max-width: 1200px, margin: 0 auto)
- Implemented working drag-and-drop between storage and numbered team slots (1-6)
- Added double-click backup method for moving pets
- Fixed JavaScript initialization and DOM loading issues
- Added proper visual feedback during drag operations
- Fixed CSS syntax errors that were breaking f-string templates
- Added missing send_json_response method for AJAX requests
- Integrated IRC PIN delivery system for secure team changes
- Updated PetBotWebServer constructor to accept bot instance for IRC messaging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 16:55:58 +01:00
megaproxy
8e9ff2960f Implement case-insensitive command processing across all bot modules
Added normalize_input() function to BaseModule for consistent lowercase conversion of user input. Updated all command modules to use normalization for commands, arguments, pet names, location names, gym names, and item names. Players can now use any capitalization for commands and arguments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 21:57:51 +01:00
15 changed files with 3145 additions and 1407 deletions

View file

@ -99,7 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `!weather` - Check current weather - `!weather` - Check current weather
- `!achievements` - View progress - `!achievements` - View progress
- `!activate/deactivate <pet>` - Manage team - `!activate/deactivate <pet>` - Manage team
- `!swap <pet1> <pet2>` - Reorganize team
- `!moves` - View pet abilities - `!moves` - View pet abilities
- `!flee` - Escape battles - `!flee` - Escape battles

View file

@ -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 - **Pet Collection**: Catch and collect different species of pets
- **Exploration**: Travel between various themed locations - **Exploration**: Travel between various themed locations
- **Battle System**: Engage in turn-based battles with wild pets - **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 - **Achievement System**: Unlock new areas by completing challenges
- **Item Collection**: Discover and collect useful items during exploration - **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 ### Pet Management
- `!activate <pet>` - Activate a pet for battle - `!activate <pet>` - Activate a pet for battle
- `!deactivate <pet>` - Move a pet to storage - `!deactivate <pet>` - Move a pet to storage
- `!swap <pet1> <pet2>` - Swap two pets' active status
### Inventory Commands ### Inventory Commands
- `!inventory` / `!inv` / `!items` - View your collected items - `!inventory` / `!inv` / `!items` - View your collected items

View file

@ -430,11 +430,6 @@
<div class="command-desc">Remove a pet from your active team and put it in storage.</div> <div class="command-desc">Remove a pet from your active team and put it in storage.</div>
<div class="command-example">Example: !deactivate aqua</div> <div class="command-example">Example: !deactivate aqua</div>
</div> </div>
<div class="command">
<div class="command-name">!swap &lt;pet1&gt; &lt;pet2&gt;</div>
<div class="command-desc">Swap the active status of two pets - one becomes active, the other goes to storage.</div>
<div class="command-example">Example: !swap leafy flamey</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -19,14 +19,12 @@ class Achievements(BaseModule):
if not player: if not player:
return return
# Redirect to web interface for better achievements display
self.send_message(channel, f"🏆 {nickname}: View your complete achievements at: http://petz.rdx4.com/player/{nickname}#achievements")
# Show quick summary in channel
achievements = await self.database.get_player_achievements(player["id"]) achievements = await self.database.get_player_achievements(player["id"])
if achievements: if achievements:
self.send_message(channel, f"🏆 {nickname}'s Achievements:") self.send_message(channel, f"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.")
for achievement in achievements[:5]: # Show last 5 achievements
self.send_message(channel, f"{achievement['name']}: {achievement['description']}")
if len(achievements) > 5:
self.send_message(channel, f"... and {len(achievements) - 5} more!")
else: else:
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!") self.send_message(channel, f"💡 No achievements yet! Keep exploring and catching pets to unlock new areas!")

View file

@ -12,6 +12,15 @@ class BaseModule(ABC):
self.database = database self.database = database
self.game_engine = game_engine 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 @abstractmethod
def get_commands(self): def get_commands(self):
"""Return list of commands this module handles""" """Return list of commands this module handles"""

View file

@ -52,6 +52,12 @@ class BattleSystem(BaseModule):
self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.") self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.")
return 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 # Get player's active pet
pets = await self.database.get_player_pets(player["id"], active_only=True) pets = await self.database.get_player_pets(player["id"], active_only=True)
if not pets: if not pets:
@ -87,7 +93,7 @@ class BattleSystem(BaseModule):
if not player: if not player:
return 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) result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name)
if "error" in result: if "error" in result:

View file

@ -40,5 +40,8 @@ class CoreCommands(BaseModule):
if not player: if not player:
return return
# Show quick summary and direct to web interface for detailed stats
self.send_message(channel, self.send_message(channel,
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") 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")

View file

@ -7,7 +7,7 @@ class Exploration(BaseModule):
"""Handles exploration, travel, location, weather, and wild commands""" """Handles exploration, travel, location, weather, and wild commands"""
def get_commands(self): 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): async def handle_command(self, channel, nickname, command, args):
if command == "explore": if command == "explore":
@ -22,6 +22,8 @@ class Exploration(BaseModule):
await self.cmd_wild(channel, nickname, args) await self.cmd_wild(channel, nickname, args)
elif command in ["catch", "capture"]: elif command in ["catch", "capture"]:
await self.cmd_catch(channel, nickname) await self.cmd_catch(channel, nickname)
elif command == "flee":
await self.cmd_flee_encounter(channel, nickname)
async def cmd_explore(self, channel, nickname): async def cmd_explore(self, channel, nickname):
"""Explore current location""" """Explore current location"""
@ -29,6 +31,18 @@ class Exploration(BaseModule):
if not player: if not player:
return 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"]) encounter = await self.game_engine.explore_location(player["id"])
if encounter["type"] == "error": if encounter["type"] == "error":
@ -51,7 +65,7 @@ class Exploration(BaseModule):
self.send_message(channel, self.send_message(channel,
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!") 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): async def cmd_travel(self, channel, nickname, args):
"""Travel to a different location""" """Travel to a different location"""
@ -64,7 +78,7 @@ class Exploration(BaseModule):
return return
# Handle various input formats and normalize location names # 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 # Map common variations to exact location names
location_mappings = { location_mappings = {
@ -82,7 +96,7 @@ class Exploration(BaseModule):
destination = location_mappings.get(destination_input) destination = location_mappings.get(destination_input)
if not destination: if not destination:
# Fall back to title case if no mapping found # 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) location = await self.database.get_location_by_name(destination)
@ -171,7 +185,7 @@ class Exploration(BaseModule):
if args: if args:
# Specific location requested # Specific location requested
location_name = " ".join(args).title() location_name = " ".join(self.normalize_input(args)).title()
else: else:
# Default to current location # Default to current location
current_location = await self.database.get_player_location(player["id"]) 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 # Check if player is in an active battle
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) 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: if active_battle:
# Catching during battle # Catching during battle
wild_pet = active_battle["wild_pet"] wild_pet = active_battle["wild_pet"]
@ -298,3 +319,35 @@ class Exploration(BaseModule):
from .battle_system import BattleSystem from .battle_system import BattleSystem
battle_system = BattleSystem(self.bot, self.database, self.game_engine) battle_system = BattleSystem(self.bot, self.database, self.game_engine)
await battle_system.handle_level_up_display(channel, nickname, exp_result) 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!")

View file

@ -13,13 +13,13 @@ class GymBattles(BaseModule):
if command == "gym": if command == "gym":
if not args: if not args:
await self.cmd_gym_list(channel, nickname) 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) 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:]) 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:]) 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) await self.cmd_gym_status(channel, nickname)
else: else:
await self.cmd_gym_list(channel, nickname) await self.cmd_gym_list(channel, nickname)
@ -66,7 +66,7 @@ class GymBattles(BaseModule):
f" Status: {status} | Next difficulty: {difficulty}") f" Status: {status} | Next difficulty: {difficulty}")
self.send_message(channel, self.send_message(channel,
f"💡 Use '!gym challenge \"<gym name>\"' 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): async def cmd_gym_list_all(self, channel, nickname):
"""List all gyms across all locations""" """List all gyms across all locations"""
@ -97,10 +97,6 @@ class GymBattles(BaseModule):
async def cmd_gym_challenge(self, channel, nickname, args): async def cmd_gym_challenge(self, channel, nickname, args):
"""Challenge a gym""" """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) player = await self.require_player(channel, nickname)
if not player: if not player:
return return
@ -111,18 +107,36 @@ class GymBattles(BaseModule):
self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.") self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.")
return 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
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 \"<gym name>\"")
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) # Look for gym in player's current location (case-insensitive)
gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"]) gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"])
if not gym: if not gym:
# List available gyms in current location for helpful error message # List available gyms in current location for helpful error message
available_gyms = await self.database.get_gyms_in_location(location["id"])
if available_gyms:
gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms]) 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}") 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 # Check if player has active pets
@ -266,7 +280,7 @@ class GymBattles(BaseModule):
if not player: if not player:
return return
gym_name = " ".join(args).strip('"') gym_name = " ".join(self.normalize_input(args)).strip('"')
# First try to find gym in player's current location # First try to find gym in player's current location
location = await self.database.get_player_location(player["id"]) location = await self.database.get_player_location(player["id"])
@ -311,7 +325,7 @@ class GymBattles(BaseModule):
return return
# This will show a summary - for detailed view they can use !gym list # 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): async def cmd_forfeit(self, channel, nickname):
"""Forfeit the current gym battle""" """Forfeit the current gym battle"""

View file

@ -16,51 +16,14 @@ class Inventory(BaseModule):
await self.cmd_use_item(channel, nickname, args) await self.cmd_use_item(channel, nickname, args)
async def cmd_inventory(self, channel, nickname): 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) player = await self.require_player(channel, nickname)
if not player: if not player:
return return
inventory = await self.database.get_player_inventory(player["id"]) # 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")
if not inventory: self.send_message(channel, f"💡 The web interface shows detailed item information, categories, and usage options!")
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 <item name>' to use consumable items!")
async def cmd_use_item(self, channel, nickname, args): async def cmd_use_item(self, channel, nickname, args):
"""Use an item from inventory""" """Use an item from inventory"""
@ -72,7 +35,7 @@ class Inventory(BaseModule):
if not player: if not player:
return return
item_name = " ".join(args) item_name = " ".join(self.normalize_input(args))
result = await self.database.use_item(player["id"], item_name) result = await self.database.use_item(player["id"], item_name)
if not result["success"]: if not result["success"]:

View file

@ -7,7 +7,7 @@ class PetManagement(BaseModule):
"""Handles team, pets, and future pet management commands""" """Handles team, pets, and future pet management commands"""
def get_commands(self): 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): async def handle_command(self, channel, nickname, command, args):
if command == "team": if command == "team":
@ -18,8 +18,6 @@ class PetManagement(BaseModule):
await self.cmd_activate(channel, nickname, args) await self.cmd_activate(channel, nickname, args)
elif command == "deactivate": elif command == "deactivate":
await self.cmd_deactivate(channel, nickname, args) await self.cmd_deactivate(channel, nickname, args)
elif command == "swap":
await self.cmd_swap(channel, nickname, args)
elif command == "nickname": elif command == "nickname":
await self.cmd_nickname(channel, nickname, args) await self.cmd_nickname(channel, nickname, args)
@ -40,7 +38,7 @@ class PetManagement(BaseModule):
team_info = [] team_info = []
# Active pets with star # Active pets with star and team position
for pet in active_pets: for pet in active_pets:
name = pet["nickname"] or pet["species_name"] name = pet["nickname"] or pet["species_name"]
@ -55,7 +53,9 @@ class PetManagement(BaseModule):
else: else:
exp_display = f"{exp_needed} to next" 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 # Inactive pets
for pet in inactive_pets[:5]: # Show max 5 inactive 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") 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"🐾 {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): async def cmd_pets(self, channel, nickname):
"""Show link to pet collection web page""" """Show link to pet collection web page"""
@ -74,7 +75,7 @@ class PetManagement(BaseModule):
return return
# Send URL to player's profile page instead of PM spam # 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): async def cmd_activate(self, channel, nickname, args):
"""Activate a pet for battle (PM only)""" """Activate a pet for battle (PM only)"""
@ -88,13 +89,14 @@ class PetManagement(BaseModule):
if not player: if not player:
return return
pet_name = " ".join(args) pet_name = " ".join(self.normalize_input(args))
result = await self.database.activate_pet(player["id"], pet_name) result = await self.database.activate_pet(player["id"], pet_name)
if result["success"]: if result["success"]:
pet = result["pet"] pet = result["pet"]
display_name = pet["nickname"] or pet["species_name"] 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!") self.send_message(channel, f"{nickname}: Pet activated successfully!")
else: else:
self.send_pm(nickname, f"{result['error']}") self.send_pm(nickname, f"{result['error']}")
@ -112,7 +114,7 @@ class PetManagement(BaseModule):
if not player: if not player:
return return
pet_name = " ".join(args) pet_name = " ".join(self.normalize_input(args))
result = await self.database.deactivate_pet(player["id"], pet_name) result = await self.database.deactivate_pet(player["id"], pet_name)
if result["success"]: if result["success"]:
@ -124,44 +126,6 @@ class PetManagement(BaseModule):
self.send_pm(nickname, f"{result['error']}") self.send_pm(nickname, f"{result['error']}")
self.send_message(channel, f"{nickname}: Pet deactivation failed - check PM for details!") 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 <pet1> <pet2>")
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): async def cmd_nickname(self, channel, nickname, args):
"""Set a nickname for a pet""" """Set a nickname for a pet"""
if len(args) < 2: if len(args) < 2:
@ -174,7 +138,7 @@ class PetManagement(BaseModule):
return return
# Split args into pet identifier and new nickname # Split args into pet identifier and new nickname
pet_identifier = args[0] pet_identifier = self.normalize_input(args[0])
new_nickname = " ".join(args[1:]) new_nickname = " ".join(args[1:])
result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname) result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname)

View file

@ -62,7 +62,7 @@ class PetBotDebug:
print("✅ Background validation started") print("✅ Background validation started")
print("🔄 Starting web server...") 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() self.web_server.start_in_thread()
print("✅ Web server started") print("✅ Web server started")
@ -303,12 +303,14 @@ class PetBotDebug:
self.handle_command(channel, nickname, message) self.handle_command(channel, nickname, message)
def handle_command(self, channel, nickname, message): def handle_command(self, channel, nickname, message):
from modules.base_module import BaseModule
command_parts = message[1:].split() command_parts = message[1:].split()
if not command_parts: if not command_parts:
return return
command = command_parts[0].lower() command = BaseModule.normalize_input(command_parts[0])
args = command_parts[1:] args = BaseModule.normalize_input(command_parts[1:])
try: try:
if command in self.command_map: if command in self.command_map:

View file

@ -120,6 +120,46 @@ class Database:
except: except:
pass # Column already exists 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(""" await db.execute("""
CREATE TABLE IF NOT EXISTS location_spawns ( CREATE TABLE IF NOT EXISTS location_spawns (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -408,6 +448,9 @@ class Database:
if active_only: if active_only:
query += " AND p.is_active = TRUE" 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) cursor = await db.execute(query, params)
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [dict(row) for row in rows] return [dict(row) for row in rows]
@ -638,11 +681,16 @@ class Database:
if not pet: if not pet:
return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"} return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"}
# Activate the pet # Get next available team slot
await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],)) 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() 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: async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict:
"""Deactivate a pet by name or species name. Returns result dict.""" """Deactivate a pet by name or species name. Returns result dict."""
@ -670,58 +718,122 @@ class Database:
if active_count["count"] <= 1: if active_count["count"] <= 1:
return {"success": False, "error": "You must have at least one active pet!"} return {"success": False, "error": "You must have at least one active pet!"}
# Deactivate the pet # Deactivate the pet and clear team order
await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],)) await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],))
await db.commit() await db.commit()
return {"success": True, "pet": dict(pet)} return {"success": True, "pet": dict(pet)}
async def swap_pets(self, player_id: int, pet1_identifier: str, pet2_identifier: str) -> Dict: # Team Order Methods
"""Swap the active status of two pets. Returns result dict.""" 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: async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Find both pets
cursor = await db.execute(""" cursor = await db.execute("""
SELECT p.*, ps.name as species_name SELECT team_order FROM pets
FROM pets p WHERE player_id = ? AND is_active = TRUE AND team_order IS NOT NULL
JOIN pet_species ps ON p.species_id = ps.id ORDER BY team_order ASC
WHERE p.player_id = ? """, (player_id,))
AND (p.nickname = ? OR ps.name = ?) used_slots = [row[0] for row in await cursor.fetchall()]
LIMIT 1
""", (player_id, pet1_identifier, pet1_identifier))
pet1 = await cursor.fetchone()
# Find first available slot (1-6)
for slot in range(1, 7):
if slot not in used_slots:
return slot
return None # Team is full
async def set_pet_team_order(self, player_id: int, pet_id: int, position: int) -> Dict:
"""Set a pet's team order position (1-6)"""
if position < 1 or position > 6:
return {"success": False, "error": "Team position must be between 1-6"}
async with aiosqlite.connect(self.db_path) as db:
# Check if pet belongs to player
cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
pet = await cursor.fetchone()
if not pet:
return {"success": False, "error": "Pet not found"}
# Check if position is already taken
cursor = await db.execute(""" cursor = await db.execute("""
SELECT p.*, ps.name as species_name SELECT id FROM pets
FROM pets p WHERE player_id = ? AND team_order = ? AND is_active = TRUE AND id != ?
JOIN pet_species ps ON p.species_id = ps.id """, (player_id, position, pet_id))
WHERE p.player_id = ? existing_pet = await cursor.fetchone()
AND (p.nickname = ? OR ps.name = ?)
LIMIT 1
""", (player_id, pet2_identifier, pet2_identifier))
pet2 = await cursor.fetchone()
if not pet1: if existing_pet:
return {"success": False, "error": f"Pet '{pet1_identifier}' not found"} return {"success": False, "error": f"Position {position} is already taken"}
if not pet2:
return {"success": False, "error": f"Pet '{pet2_identifier}' not found"}
if pet1["id"] == pet2["id"]: # Update pet's team order and make it active
return {"success": False, "error": "Cannot swap a pet with itself"} 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() await db.commit()
return {"success": True, "position": position}
return { async def reorder_team_positions(self, player_id: int, new_positions: List[Dict]) -> Dict:
"success": True, """Reorder team positions based on new arrangement"""
"pet1": dict(pet1), async with aiosqlite.connect(self.db_path) as db:
"pet2": dict(pet2), try:
"pet1_now": "active" if not pet1["is_active"] else "storage", # Validate all positions are 1-6 and no duplicates
"pet2_now": "active" if not pet2["is_active"] else "storage" positions = [pos["position"] for pos in new_positions]
} if len(set(positions)) != len(positions):
return {"success": False, "error": "Duplicate positions detected"}
for pos_data in new_positions:
position = pos_data["position"]
pet_id = pos_data["pet_id"]
if position < 1 or position > 6:
return {"success": False, "error": f"Invalid position {position}"}
# Verify pet belongs to player
cursor = await db.execute("SELECT id FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
if not await cursor.fetchone():
return {"success": False, "error": f"Pet {pet_id} not found"}
# Clear all team orders first
await db.execute("UPDATE pets SET team_order = NULL WHERE player_id = ?", (player_id,))
# Set new positions
for pos_data in new_positions:
await db.execute("""
UPDATE pets SET team_order = ?, is_active = TRUE
WHERE id = ? AND player_id = ?
""", (pos_data["position"], pos_data["pet_id"], player_id))
await db.commit()
return {"success": True, "message": "Team order updated successfully"}
except Exception as e:
await db.rollback()
return {"success": False, "error": str(e)}
async def remove_from_team_position(self, player_id: int, pet_id: int) -> Dict:
"""Remove a pet from team (set to inactive and clear team_order)"""
async with aiosqlite.connect(self.db_path) as db:
# Check if pet belongs to player
cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
pet = await cursor.fetchone()
if not pet:
return {"success": False, "error": "Pet not found"}
# Check if this is the only active pet
cursor = await db.execute("SELECT COUNT(*) as count FROM pets WHERE player_id = ? AND is_active = TRUE", (player_id,))
active_count = await cursor.fetchone()
if active_count["count"] <= 1:
return {"success": False, "error": "Cannot deactivate your only active pet"}
# Remove from team
await db.execute("""
UPDATE pets SET is_active = FALSE, team_order = NULL
WHERE id = ? AND player_id = ?
""", (pet_id, player_id))
await db.commit()
return {"success": True, "message": "Pet removed from team"}
# Item and Inventory Methods # Item and Inventory Methods
async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool: 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 FROM pets p
JOIN pet_species ps ON p.species_id = ps.id JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ? AND p.is_active = 1 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,)) """, (player_id,))
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [dict(row) for row in rows] return [dict(row) for row in rows]
@ -1601,12 +1713,18 @@ class Database:
# Begin transaction # Begin transaction
await db.execute("BEGIN TRANSACTION") await db.execute("BEGIN TRANSACTION")
# Update pet active status based on new team # Update pet active status and team_order based on new team
for pet_id, is_active in team_changes.items(): for pet_id, position in team_changes.items():
if position: # If position is a number (1-6), pet is active
await db.execute(""" await db.execute("""
UPDATE pets SET is_active = ? UPDATE pets SET is_active = TRUE, team_order = ?
WHERE id = ? AND player_id = ? WHERE id = ? AND player_id = ?
""", (is_active, int(pet_id), 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 # Mark any pending change as verified
await db.execute(""" await db.execute("""
@ -1713,18 +1831,28 @@ class Database:
# Get current pet states # Get current pet states
cursor = await db.execute(""" 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,)) """, (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 # Apply proposed changes to current state
new_state = current_pets.copy() 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: 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 # Count active pets and validate positions
active_count = sum(1 for is_active in new_state.values() if is_active) 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 # Validate constraints
if active_count < 1: if active_count < 1:

View file

@ -196,11 +196,11 @@ class GameEngine:
cursor = await db.execute(""" cursor = await db.execute("""
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
attack, defense, speed, is_active) attack, defense, speed, is_active, team_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (player_id, species["id"], pet_data["level"], 0, """, (player_id, species["id"], pet_data["level"], 0,
pet_data["hp"], pet_data["hp"], pet_data["attack"], 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() await db.commit()

File diff suppressed because it is too large Load diff