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
- `!achievements` - View progress
- `!activate/deactivate <pet>` - Manage team
- `!swap <pet1> <pet2>` - Reorganize team
- `!moves` - View pet abilities
- `!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
- **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 <pet>` - Activate a pet for battle
- `!deactivate <pet>` - Move a pet to storage
- `!swap <pet1> <pet2>` - Swap two pets' active status
### Inventory Commands
- `!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-example">Example: !deactivate aqua</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>

View file

@ -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!")
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.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"""

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.")
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:

View file

@ -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']}")
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"""
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)
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 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 \"<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):
"""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 \"<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)
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"""

View file

@ -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 <item name>' 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"]:

View file

@ -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 <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):
"""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)

View file

@ -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:

View file

@ -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:

View file

@ -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()

File diff suppressed because it is too large Load diff