- Fixed tuple index out of range error in end_gym_battle() - Added proper row factory and named column access in database queries - Added exception handling and bounds checking in gym battle completion - Added debug logging to track gym battle state issues - Improved error messages for gym battle failures Fixes: "tuple index out of range" error when gym battles complete 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
No EOL
14 KiB
Python
294 lines
No EOL
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Battle system commands module for PetBot"""
|
|
|
|
from .base_module import BaseModule
|
|
|
|
class BattleSystem(BaseModule):
|
|
"""Handles battle, attack, flee, and moves commands"""
|
|
|
|
# IRC color codes for different move types
|
|
MOVE_TYPE_COLORS = {
|
|
'Fire': '\x0304', # Red
|
|
'Water': '\x0312', # Light Blue
|
|
'Grass': '\x0303', # Green
|
|
'Electric': '\x0308', # Yellow
|
|
'Rock': '\x0307', # Orange/Brown
|
|
'Normal': '\x0314', # Gray
|
|
'Physical': '\x0313', # Pink/Magenta (fallback for category)
|
|
'Special': '\x0310' # Cyan (fallback for category)
|
|
}
|
|
|
|
def get_commands(self):
|
|
return ["battle", "attack", "flee", "moves"]
|
|
|
|
def get_move_color(self, move_type):
|
|
"""Get IRC color code for a move type"""
|
|
return self.MOVE_TYPE_COLORS.get(move_type, '\x0312') # Default to light blue
|
|
|
|
async def handle_command(self, channel, nickname, command, args):
|
|
if command == "battle":
|
|
await self.cmd_battle(channel, nickname)
|
|
elif command == "attack":
|
|
await self.cmd_attack(channel, nickname, args)
|
|
elif command == "flee":
|
|
await self.cmd_flee(channel, nickname)
|
|
elif command == "moves":
|
|
await self.cmd_moves(channel, nickname)
|
|
|
|
async def cmd_battle(self, channel, nickname):
|
|
"""Start a battle with encountered wild pet"""
|
|
player = await self.require_player(channel, nickname)
|
|
if not player:
|
|
return
|
|
|
|
# Check if player has an active encounter
|
|
if player["id"] not in self.bot.active_encounters:
|
|
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet to battle!")
|
|
return
|
|
|
|
# Check if already 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 already in battle! Use !attack <move> or !flee.")
|
|
return
|
|
|
|
# Get player's active pet
|
|
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
|
if not pets:
|
|
self.send_message(channel, f"{nickname}: You need an active pet to battle! Use !team to check your pets.")
|
|
return
|
|
|
|
player_pet = pets[0] # Use first active pet
|
|
wild_pet = self.bot.active_encounters[player["id"]]
|
|
|
|
# Start battle
|
|
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, wild_pet)
|
|
|
|
# Condensed battle start message with all info
|
|
moves_colored = " | ".join([
|
|
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
|
for move in battle["available_moves"]
|
|
])
|
|
|
|
battle_start_msg = (f"⚔️ {nickname}: Battle! Your {player_pet['species_name']} (Lv.{player_pet['level']}, "
|
|
f"{battle['player_hp']}/{player_pet['max_hp']} HP) vs Wild {wild_pet['species_name']} "
|
|
f"(Lv.{wild_pet['level']}, {battle['wild_hp']}/{wild_pet['max_hp']} HP)")
|
|
|
|
self.send_message(channel, battle_start_msg)
|
|
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move>")
|
|
|
|
async def cmd_attack(self, channel, nickname, args):
|
|
"""Use a move in battle"""
|
|
if not args:
|
|
self.send_message(channel, f"{nickname}: Specify a move to use! Example: !attack Tackle")
|
|
return
|
|
|
|
player = await self.require_player(channel, nickname)
|
|
if not player:
|
|
return
|
|
|
|
move_name = " ".join(args).title() # Normalize to Title Case
|
|
result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name)
|
|
|
|
if "error" in result:
|
|
self.send_message(channel, f"{nickname}: {result['error']}")
|
|
return
|
|
|
|
# Display battle results - condensed with player names for clarity
|
|
for action in result["results"]:
|
|
# Determine attacker and target with player context
|
|
if "Wild" in action['attacker']:
|
|
attacker = f"Wild {action['attacker']}"
|
|
target_context = f"{nickname}"
|
|
else:
|
|
attacker = f"{nickname}'s {action['attacker']}"
|
|
target_context = "wild pet"
|
|
|
|
# Build condensed message with all info on one line
|
|
if action["damage"] > 0:
|
|
effectiveness_msgs = {
|
|
"super_effective": "⚡ Super effective!",
|
|
"not_very_effective": "💫 Not very effective...",
|
|
"no_effect": "❌ No effect!",
|
|
"super_effective_critical": "💥 CRIT! Super effective!",
|
|
"normal_critical": "💥 Critical hit!",
|
|
"not_very_effective_critical": "💥 Crit, not very effective..."
|
|
}
|
|
|
|
effectiveness = effectiveness_msgs.get(action["effectiveness"], "")
|
|
battle_msg = f"⚔️ {attacker} used {action['move']} → {action['damage']} damage to {target_context}! {effectiveness} (HP: {action['target_hp']})"
|
|
else:
|
|
# Status move or no damage
|
|
battle_msg = f"⚔️ {attacker} used {action['move']} on {target_context}! (HP: {action['target_hp']})"
|
|
|
|
self.send_message(channel, battle_msg)
|
|
|
|
if result["battle_over"]:
|
|
# Check if this is a gym battle
|
|
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
|
|
if result["winner"] == "player":
|
|
self.send_message(channel, f"🎉 {nickname}: You won the battle!")
|
|
# Remove encounter since battle is over
|
|
if player["id"] in self.bot.active_encounters:
|
|
del self.bot.active_encounters[player["id"]]
|
|
else:
|
|
self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...")
|
|
# Remove encounter
|
|
if player["id"] in self.bot.active_encounters:
|
|
del self.bot.active_encounters[player["id"]]
|
|
else:
|
|
# Battle continues - show available moves with type-based colors
|
|
moves_colored = " | ".join([
|
|
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
|
for move in result["available_moves"]
|
|
])
|
|
self.send_message(channel, f"🎯 {nickname}'s turn! Moves: {moves_colored}")
|
|
|
|
async def cmd_flee(self, channel, nickname):
|
|
"""Attempt to flee from battle"""
|
|
player = await self.require_player(channel, nickname)
|
|
if not player:
|
|
return
|
|
|
|
# Check if this is a gym battle
|
|
gym_battle = await self.database.get_active_gym_battle(player["id"])
|
|
|
|
if gym_battle:
|
|
# Can't flee from gym battles
|
|
self.send_message(channel, f"❌ {nickname}: You can't flee from a gym battle! Fight or forfeit with your honor intact!")
|
|
return
|
|
|
|
success = await self.game_engine.battle_engine.flee_battle(player["id"])
|
|
|
|
if success:
|
|
self.send_message(channel, f"💨 {nickname} successfully fled from battle!")
|
|
# Remove encounter
|
|
if player["id"] in self.bot.active_encounters:
|
|
del self.bot.active_encounters[player["id"]]
|
|
else:
|
|
self.send_message(channel, f"❌ {nickname} couldn't escape! Battle continues!")
|
|
|
|
async def cmd_moves(self, channel, nickname):
|
|
"""Show active pet's available moves"""
|
|
player = await self.require_player(channel, nickname)
|
|
if not player:
|
|
return
|
|
|
|
# Get player's active pets
|
|
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
|
if not pets:
|
|
self.send_message(channel, f"{nickname}: You don't have any active pets! Use !team to check your pets.")
|
|
return
|
|
|
|
active_pet = pets[0] # Use first active pet
|
|
available_moves = self.game_engine.battle_engine.get_available_moves(active_pet)
|
|
|
|
if not available_moves:
|
|
self.send_message(channel, f"{nickname}: Your {active_pet['species_name']} has no available moves!")
|
|
return
|
|
|
|
# Limit to 4 moves max and format compactly
|
|
moves_to_show = available_moves[:4]
|
|
|
|
move_info = []
|
|
for move in moves_to_show:
|
|
power = move.get('power', 'Status')
|
|
power_str = str(power) if power != 'Status' else 'Stat'
|
|
move_colored = f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
|
move_info.append(f"{move_colored} ({move['type']}, {power_str})")
|
|
|
|
pet_name = active_pet["nickname"] or active_pet["species_name"]
|
|
moves_line = " | ".join(move_info)
|
|
self.send_message(channel, f"🎯 {nickname}'s {pet_name}: {moves_line}")
|
|
|
|
async def handle_gym_battle_completion(self, channel, nickname, player, battle_result, gym_battle):
|
|
"""Handle completion of a gym battle turn"""
|
|
try:
|
|
if battle_result["winner"] == "player":
|
|
# Player won this individual battle
|
|
current_pet_index = gym_battle["current_pet_index"]
|
|
gym_team = gym_battle["gym_team"]
|
|
|
|
# Safety check for index bounds
|
|
if current_pet_index >= len(gym_team):
|
|
self.send_message(channel, f"❌ {nickname}: Gym battle state error - please !forfeit and try again")
|
|
return
|
|
|
|
defeated_pet = gym_team[current_pet_index]
|
|
|
|
self.send_message(channel, f"🎉 {nickname}: You defeated {defeated_pet['species_name']}!")
|
|
|
|
# Check if there are more pets
|
|
if await self.database.advance_gym_battle(player["id"]):
|
|
# More pets to fight
|
|
next_index = current_pet_index + 1
|
|
next_pet = gym_team[next_index]
|
|
|
|
self.send_message(channel,
|
|
f"🥊 {gym_battle['leader_name']} sends out {next_pet['species_name']} (Lv.{next_pet['level']})!")
|
|
|
|
# Start battle with next gym pet
|
|
active_pets = await self.database.get_active_pets(player["id"])
|
|
player_pet = active_pets[0] # Use first active pet
|
|
|
|
# Create gym pet data for battle engine
|
|
next_gym_pet_data = {
|
|
"species_name": next_pet["species_name"],
|
|
"level": next_pet["level"],
|
|
"type1": next_pet["type1"],
|
|
"type2": next_pet["type2"],
|
|
"stats": {
|
|
"hp": next_pet["hp"],
|
|
"attack": next_pet["attack"],
|
|
"defense": next_pet["defense"],
|
|
"speed": next_pet["speed"]
|
|
}
|
|
}
|
|
|
|
# Start next battle
|
|
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, next_gym_pet_data)
|
|
|
|
self.send_message(channel,
|
|
f"⚔️ Your {player_pet['species_name']} (HP: {battle['player_hp']}/{player_pet['max_hp']}) vs {next_pet['species_name']} (HP: {battle['wild_hp']}/{next_pet['hp']})")
|
|
|
|
# Show available moves
|
|
moves_colored = " | ".join([
|
|
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
|
for move in battle["available_moves"]
|
|
])
|
|
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>")
|
|
|
|
else:
|
|
# All gym pets defeated - gym victory!
|
|
result = await self.database.end_gym_battle(player["id"], victory=True)
|
|
|
|
self.send_message(channel, f"🏆 {nickname}: You defeated all of {gym_battle['leader_name']}'s pets!")
|
|
self.send_message(channel,
|
|
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Impressive! You've earned the {gym_battle['gym_name']} badge!\"")
|
|
self.send_message(channel, f"🎉 {nickname} earned the {gym_battle['gym_name']} badge {gym_battle['badge_icon']}!")
|
|
|
|
# Award rewards based on difficulty
|
|
money_reward = 500 + (result["difficulty_level"] * 100)
|
|
self.send_message(channel, f"💰 Rewards: ${money_reward} | 🌟 Gym mastery increased!")
|
|
|
|
else:
|
|
# Player lost gym battle
|
|
result = await self.database.end_gym_battle(player["id"], victory=False)
|
|
|
|
self.send_message(channel, f"💀 {nickname}: Your pet fainted!")
|
|
self.send_message(channel,
|
|
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"")
|
|
self.send_message(channel,
|
|
f"💡 {nickname}: Try leveling up your pets or bringing items to heal during battle!")
|
|
|
|
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}")
|
|
import traceback
|
|
traceback.print_exc() |