Implement comprehensive team management and fix critical bugs

Team Management Features:
- Added 6 new IRC commands: \!teamlist, \!activeteam, \!teamname, \!teamswap, \!heal, \!verifyteamswap
- \!teamlist shows teams with pet names in format "Team name - pet1 - pet2 - pet3"
- \!teamname redirects to web interface for secure PIN-based renaming
- \!teamswap enables team switching with PIN verification via IRC
- \!activeteam displays current team with health status indicators
- \!heal command with 1-hour cooldown for pet health restoration

Critical Bug Fixes:
- Fixed \!teamlist SQL binding error - handled new team data format correctly
- Fixed \!wild command duplicates - now shows unique species types only
- Removed all debug print statements and implemented proper logging
- Fixed data format inconsistencies in team management system

Production Improvements:
- Added logging infrastructure to BaseModule and core components
- Converted 45+ print statements to professional logging calls
- Database query optimization with DISTINCT for spawn deduplication
- Enhanced error handling and user feedback messages

Cross-platform Integration:
- Seamless sync between IRC commands and web interface
- PIN authentication leverages existing secure infrastructure
- Team operations maintain consistency across all interfaces

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-08-01 15:53:26 +00:00
parent e920503dbd
commit e17705dc63
10 changed files with 2012 additions and 384 deletions

View file

@ -2,6 +2,7 @@
"""Base module class for PetBot command modules"""
import asyncio
import logging
from abc import ABC, abstractmethod
class BaseModule(ABC):
@ -11,6 +12,7 @@ class BaseModule(ABC):
self.bot = bot
self.database = database
self.game_engine = game_engine
self.logger = logging.getLogger(self.__class__.__name__)
@staticmethod
def normalize_input(user_input):

View file

@ -134,7 +134,6 @@ class BattleSystem(BaseModule):
gym_battle = await self.database.get_active_gym_battle(player["id"])
if gym_battle:
print(f"DEBUG: Gym battle completion - player: {player['id']}, result: {result.get('winner')}")
await self.handle_gym_battle_completion(channel, nickname, player, result, gym_battle)
else:
# Regular wild battle
@ -312,9 +311,9 @@ class BattleSystem(BaseModule):
except Exception as e:
self.send_message(channel, f"{nickname}: Gym battle error occurred - please !forfeit and try again")
print(f"Gym battle completion error: {e}")
self.logger.error(f"Gym battle completion error: {e}")
import traceback
traceback.print_exc()
self.logger.error(traceback.format_exc())
async def award_battle_experience(self, channel, nickname, player, defeated_pet, battle_type="wild"):
"""Award experience to active pets for battle victory"""

View file

@ -106,7 +106,6 @@ class Exploration(BaseModule):
# CRITICAL FIX: Check and award any outstanding achievements before checking travel requirements
# This ensures players get credit for achievements they've earned but haven't been awarded yet
print(f"🔄 Checking all achievements for {nickname} before travel...")
# Check ALL possible achievements comprehensively
all_new_achievements = await self.game_engine.check_all_achievements(player["id"])

View file

@ -15,7 +15,7 @@ class NPCEventsModule(BaseModule):
self.events_manager = NPCEventsManager(database)
def get_commands(self):
return ['events', 'event', 'help', 'contribute', 'eventhelp']
return ['events', 'event', 'contribute', 'eventhelp']
async def handle_command(self, command, channel, nickname, args):
"""Handle NPC events commands"""
@ -27,8 +27,6 @@ class NPCEventsModule(BaseModule):
await self.cmd_events(channel, nickname)
elif command == 'event':
await self.cmd_event(channel, nickname, args)
elif command == 'help' and len(args) > 0 and args[0].lower() == 'events':
await self.cmd_event_help(channel, nickname)
elif command == 'contribute':
await self.cmd_contribute(channel, nickname, args)
elif command == 'eventhelp':
@ -74,7 +72,7 @@ class NPCEventsModule(BaseModule):
self.send_message(channel, message)
except Exception as e:
print(f"Error in cmd_events: {e}")
self.logger.error(f"Error in cmd_events: {e}")
self.send_message(channel, f"❌ Error fetching events: {str(e)}")
async def cmd_event(self, channel, nickname, args):
@ -132,7 +130,7 @@ class NPCEventsModule(BaseModule):
except ValueError:
self.send_message(channel, "❌ Invalid event ID. Please use a number.")
except Exception as e:
print(f"Error in cmd_event: {e}")
self.logger.error(f"Error in cmd_event: {e}")
self.send_message(channel, f"❌ Error fetching event details: {str(e)}")
async def cmd_contribute(self, channel, nickname, args):
@ -200,7 +198,7 @@ class NPCEventsModule(BaseModule):
except ValueError:
self.send_message(channel, "❌ Invalid event ID. Please use a number.")
except Exception as e:
print(f"Error in cmd_contribute: {e}")
self.logger.error(f"Error in cmd_contribute: {e}")
self.send_message(channel, f"❌ Error contributing to event: {str(e)}")
async def cmd_event_help(self, channel, nickname):

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", "nickname"]
return ["team", "pets", "activate", "deactivate", "nickname", "heal", "teamname", "teamswap", "teamlist", "activeteam", "verifyteamswap"]
async def handle_command(self, channel, nickname, command, args):
if command == "team":
@ -20,6 +20,18 @@ class PetManagement(BaseModule):
await self.cmd_deactivate(channel, nickname, args)
elif command == "nickname":
await self.cmd_nickname(channel, nickname, args)
elif command == "heal":
await self.cmd_heal(channel, nickname)
elif command == "teamname":
await self.cmd_teamname(channel, nickname, args)
elif command == "teamswap":
await self.cmd_teamswap(channel, nickname, args)
elif command == "teamlist":
await self.cmd_teamlist(channel, nickname)
elif command == "activeteam":
await self.cmd_activeteam(channel, nickname)
elif command == "verifyteamswap":
await self.cmd_verifyteamswap(channel, nickname, args)
async def cmd_team(self, channel, nickname):
"""Redirect player to their team builder page"""
@ -110,4 +122,273 @@ class PetManagement(BaseModule):
new_name = result["new_nickname"]
self.send_message(channel, f"{nickname}: {old_name} is now nicknamed '{new_name}'!")
else:
self.send_message(channel, f"{nickname}: {result['error']}")
self.send_message(channel, f"{nickname}: {result['error']}")
async def cmd_heal(self, channel, nickname):
"""Heal active pets (available to all users with 1-hour cooldown)"""
try:
player = await self.require_player(channel, nickname)
if not player:
return
# Check cooldown
from datetime import datetime, timedelta
last_heal = await self.database.get_last_heal_time(player["id"])
if last_heal:
time_since_heal = datetime.now() - last_heal
if time_since_heal < timedelta(hours=1):
remaining = timedelta(hours=1) - time_since_heal
minutes_remaining = int(remaining.total_seconds() / 60)
self.send_message(channel, f"{nickname}: Heal command is on cooldown! {minutes_remaining} minutes remaining.")
return
# Get active pets
active_pets = await self.database.get_active_pets(player["id"])
if not active_pets:
self.send_message(channel, f"{nickname}: You don't have any active pets to heal!")
return
# Count how many pets need healing
pets_healed = 0
for pet in active_pets:
if pet["hp"] < pet["max_hp"]:
# Heal pet to full HP
await self.database.update_pet_hp(pet["id"], pet["max_hp"])
pets_healed += 1
if pets_healed == 0:
self.send_message(channel, f"{nickname}: All your active pets are already at full health!")
return
# Update cooldown
await self.database.update_last_heal_time(player["id"])
self.send_message(channel, f"💊 {nickname}: Healed {pets_healed} pet{'s' if pets_healed != 1 else ''} to full health! Next heal available in 1 hour.")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error with heal command: {str(e)}")
async def cmd_teamname(self, channel, nickname, args):
"""Redirect player to team builder for team naming"""
player = await self.require_player(channel, nickname)
if not player:
return
# Redirect to web interface for team management
self.send_message(channel, f"🏷️ {nickname}: Rename your teams at: http://petz.rdx4.com/teambuilder/{nickname}")
self.send_message(channel, f"💡 Click on team names to rename them with secure PIN verification!")
async def cmd_teamswap(self, channel, nickname, args):
"""Switch active team to specified slot with PIN verification"""
if not args:
self.send_message(channel, f"{nickname}: Usage: !teamswap <slot>")
self.send_message(channel, f"Example: !teamswap 2")
self.send_message(channel, f"Slots: 1-3")
return
player = await self.require_player(channel, nickname)
if not player:
return
try:
slot = int(args[0])
if slot < 1 or slot > 3:
self.send_message(channel, f"{nickname}: Invalid slot! Must be 1, 2, or 3")
return
except ValueError:
self.send_message(channel, f"{nickname}: Slot must be a number (1-3)")
return
try:
# Check if team slot exists and has pets
config = await self.database.load_team_configuration(player["id"], slot)
if not config:
self.send_message(channel, f"{nickname}: Team slot {slot} is empty! Create a team first via the web interface")
return
# Parse team data to check if it has pets
import json
team_data = json.loads(config["team_data"]) if config["team_data"] else []
if not team_data:
self.send_message(channel, f"{nickname}: Team slot {slot} '{config['config_name']}' has no pets!")
return
# Check if this team is already active
current_active_slot = await self.database.get_active_team_slot(player["id"])
if current_active_slot == slot:
self.send_message(channel, f"{nickname}: Team slot {slot} '{config['config_name']}' is already active!")
return
# Get team management service for PIN-based swap
from src.team_management import TeamManagementService
from src.pin_authentication import PinAuthenticationService
pin_service = PinAuthenticationService(self.database, self.bot)
team_service = TeamManagementService(self.database, pin_service)
# Request team swap with PIN verification
swap_result = await team_service.request_team_swap(player["id"], nickname, slot)
if swap_result["success"]:
pet_count = len(team_data)
self.send_message(channel, f"🔐 {nickname}: PIN sent for swapping to '{config['config_name']}' ({pet_count} pets). Check your PM!")
else:
self.send_message(channel, f"{nickname}: {swap_result.get('error', 'Failed to request team swap')}")
except Exception as e:
self.logger.error(f"Error in teamswap command: {e}")
self.send_message(channel, f"{nickname}: Error processing team swap request")
async def cmd_teamlist(self, channel, nickname):
"""Show all team slots with names and pet names"""
player = await self.require_player(channel, nickname)
if not player:
return
try:
import json
# Get team configurations directly from database
team_configs = await self.database.get_player_team_configurations(player["id"])
current_active_slot = await self.database.get_active_team_slot(player["id"])
# Build team list display
team_lines = [f"📋 {nickname}'s Teams:"]
for slot in range(1, 4):
# Find config for this slot
config = next((c for c in team_configs if c.get("slot") == slot), None)
if config and config.get("team_data"):
team_name = config["name"]
team_data = config["team_data"]
# Get pet names from team data
pet_names = []
if isinstance(team_data, list):
# New format: list of pet objects (already fetched by get_player_team_configurations)
for pet in team_data:
if pet and isinstance(pet, dict):
display_name = pet.get("nickname") or pet.get("species_name", "Unknown")
pet_names.append(display_name)
elif isinstance(team_data, dict):
# Old format: dict with positions containing pet IDs
for pos in sorted(team_data.keys()):
pet_id = team_data[pos]
if pet_id:
pet = await self.database.get_pet_by_id(pet_id)
if pet:
display_name = pet.get("nickname") or pet.get("species_name", "Unknown")
pet_names.append(display_name)
# Mark active team
active_marker = " 🟢" if current_active_slot == slot else ""
if pet_names:
pets_text = " - ".join(pet_names)
team_lines.append(f" {slot}. {team_name} - {pets_text}{active_marker}")
else:
team_lines.append(f" {slot}. {team_name} - empty{active_marker}")
else:
team_lines.append(f" {slot}. Team {slot} - empty")
team_lines.append("")
team_lines.append("Commands: !teamswap <slot> | Web: http://petz.rdx4.com/teambuilder/" + nickname)
# Send each line separately to avoid IRC length limits
for line in team_lines:
self.send_message(channel, line)
except Exception as e:
self.logger.error(f"Error in teamlist command: {e}")
self.send_message(channel, f"{nickname}: Error loading team list")
async def cmd_activeteam(self, channel, nickname):
"""Show current active team details"""
player = await self.require_player(channel, nickname)
if not player:
return
try:
# Get active team
active_pets = await self.database.get_active_team(player["id"])
current_slot = await self.database.get_active_team_slot(player["id"])
if not active_pets:
self.send_message(channel, f"{nickname}: You don't have an active team! Use !teamswap or the web interface to set one")
return
# Get team name if it's from a saved configuration
team_name = "Active Team"
if current_slot:
config = await self.database.load_team_configuration(player["id"], current_slot)
if config:
team_name = config["config_name"]
# Build active team display
team_lines = [f"⚔️ {nickname}'s {team_name}:"]
for i, pet in enumerate(active_pets, 1):
display_name = pet.get("nickname") or pet.get("species_name", "Unknown")
level = pet.get("level", 1)
hp = pet.get("hp", 0)
max_hp = pet.get("max_hp", 100)
# Health status indicator
health_pct = (hp / max_hp * 100) if max_hp > 0 else 0
if health_pct >= 75:
health_icon = "💚"
elif health_pct >= 50:
health_icon = "💛"
elif health_pct >= 25:
health_icon = "🧡"
else:
health_icon = "❤️"
team_lines.append(f" {i}. {display_name} (Lv.{level}) {health_icon} {hp}/{max_hp}")
team_lines.append("")
team_lines.append(f"Use !heal to restore health (1hr cooldown)")
team_lines.append(f"Manage teams: http://petz.rdx4.com/teambuilder/{nickname}")
# Send team info
for line in team_lines:
self.send_message(channel, line)
except Exception as e:
self.logger.error(f"Error in activeteam command: {e}")
self.send_message(channel, f"{nickname}: Error loading active team")
async def cmd_verifyteamswap(self, channel, nickname, args):
"""Verify PIN and execute team swap"""
if not args:
self.send_message(channel, f"{nickname}: Usage: !verifyteamswap <pin>")
return
player = await self.require_player(channel, nickname)
if not player:
return
pin_code = args[0].strip()
try:
# Get team management service
from src.team_management import TeamManagementService
from src.pin_authentication import PinAuthenticationService
pin_service = PinAuthenticationService(self.database, self.bot)
team_service = TeamManagementService(self.database, pin_service)
# Verify PIN and execute team swap
result = await team_service.verify_team_swap(player["id"], pin_code)
if result["success"]:
self.send_message(channel, f"{nickname}: {result['message']}")
if 'pets_applied' in result:
self.send_message(channel, f"🔄 {result['pets_applied']} pets are now active for battle!")
else:
self.send_message(channel, f"{nickname}: {result.get('error', 'Team swap failed')}")
except Exception as e:
self.logger.error(f"Error in verifyteamswap command: {e}")
self.send_message(channel, f"{nickname}: Error processing team swap verification")

View file

@ -13,9 +13,10 @@ class TeamBuilder(BaseModule):
# No direct commands handled by this module
pass
async def send_team_builder_pin(self, nickname, pin_code):
async def send_team_builder_pin(self, nickname, pin_code, team_name=None):
"""Send PIN to player via private message"""
message = f"""🔐 Team Builder Verification PIN: {pin_code}
team_text = f" for {team_name}" if team_name else ""
message = f"""🔐 Team Builder Verification PIN{team_text}: {pin_code}
This PIN will expire in 10 minutes.
Enter this PIN on the team builder web page to confirm your team changes.
@ -23,13 +24,13 @@ Enter this PIN on the team builder web page to confirm your team changes.
Keep this PIN private! Do not share it with anyone."""
self.send_pm(nickname, message)
print(f"🔐 Sent team builder PIN to {nickname}: {pin_code}")
self.logger.info(f"🔐 Sent team builder PIN to {nickname}: {pin_code}{team_text}")
async def cleanup_expired_data(self):
"""Clean up expired PINs and pending requests"""
try:
result = await self.database.cleanup_expired_pins()
if result["success"] and (result["pins_cleaned"] > 0 or result["changes_cleaned"] > 0):
print(f"🧹 Cleaned up {result['pins_cleaned']} expired PINs and {result['changes_cleaned']} pending changes")
self.logger.info(f"🧹 Cleaned up {result['pins_cleaned']} expired PINs and {result['changes_cleaned']} pending changes")
except Exception as e:
print(f"Error during cleanup: {e}")
self.logger.error(f"Error during cleanup: {e}")