Initial commit: Complete PetBot IRC Game
🎮 Features implemented: - Pokemon-style pet collection and battles - Multi-location exploration system - Dynamic weather with background updates - Achievement system with location unlocks - Web dashboard for player stats - Modular command system - Async database with SQLite - PM flood prevention - Persistent player data 🌤️ Weather System: - 6 weather types with spawn modifiers - 30min-3hour dynamic durations - Background task for automatic updates - Location-specific weather patterns 🐛 Recent Bug Fixes: - Database persistence on restart - Player page SQLite row conversion - Achievement count calculations - Travel requirement messages - Battle move color coding - Locations page display 🔧 Generated with Claude Code 🤖 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
47f160a295
31 changed files with 6235 additions and 0 deletions
18
modules/__init__.py
Normal file
18
modules/__init__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env python3
|
||||
"""PetBot modules package"""
|
||||
|
||||
from .core_commands import CoreCommands
|
||||
from .exploration import Exploration
|
||||
from .battle_system import BattleSystem
|
||||
from .pet_management import PetManagement
|
||||
from .achievements import Achievements
|
||||
from .admin import Admin
|
||||
|
||||
__all__ = [
|
||||
'CoreCommands',
|
||||
'Exploration',
|
||||
'BattleSystem',
|
||||
'PetManagement',
|
||||
'Achievements',
|
||||
'Admin'
|
||||
]
|
||||
32
modules/achievements.py
Normal file
32
modules/achievements.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Achievements commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Achievements(BaseModule):
|
||||
"""Handles achievements display and tracking"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["achievements"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "achievements":
|
||||
await self.cmd_achievements(channel, nickname)
|
||||
|
||||
async def cmd_achievements(self, channel, nickname):
|
||||
"""Show player achievements"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
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!")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")
|
||||
30
modules/admin.py
Normal file
30
modules/admin.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Admin commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Admin(BaseModule):
|
||||
"""Handles admin-only commands like reload"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["reload"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "reload":
|
||||
await self.cmd_reload(channel, nickname)
|
||||
|
||||
async def cmd_reload(self, channel, nickname):
|
||||
"""Reload bot modules (megasconed only)"""
|
||||
if nickname.lower() != "megasconed":
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Trigger module reload in main bot
|
||||
success = await self.bot.reload_modules()
|
||||
if success:
|
||||
self.send_message(channel, f"{nickname}: ✅ Modules reloaded successfully!")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: ❌ Module reload failed!")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}")
|
||||
43
modules/base_module.py
Normal file
43
modules/base_module.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Base module class for PetBot command modules"""
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class BaseModule(ABC):
|
||||
"""Base class for all PetBot modules"""
|
||||
|
||||
def __init__(self, bot, database, game_engine):
|
||||
self.bot = bot
|
||||
self.database = database
|
||||
self.game_engine = game_engine
|
||||
|
||||
@abstractmethod
|
||||
def get_commands(self):
|
||||
"""Return list of commands this module handles"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
"""Handle a command for this module"""
|
||||
pass
|
||||
|
||||
def send_message(self, target, message):
|
||||
"""Send message through the bot"""
|
||||
self.bot.send_message(target, message)
|
||||
|
||||
def send_pm(self, nickname, message):
|
||||
"""Send private message to user"""
|
||||
self.bot.send_message(nickname, message)
|
||||
|
||||
async def get_player(self, nickname):
|
||||
"""Get player from database"""
|
||||
return await self.database.get_player(nickname)
|
||||
|
||||
async def require_player(self, channel, nickname):
|
||||
"""Get player or send start message if not found"""
|
||||
player = await self.get_player(nickname)
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return None
|
||||
return player
|
||||
192
modules/battle_system.py
Normal file
192
modules/battle_system.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
#!/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"]:
|
||||
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
|
||||
|
||||
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}")
|
||||
44
modules/core_commands.py
Normal file
44
modules/core_commands.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Core commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class CoreCommands(BaseModule):
|
||||
"""Handles basic bot commands like start, help, stats"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["help", "start", "stats"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "help":
|
||||
await self.cmd_help(channel, nickname)
|
||||
elif command == "start":
|
||||
await self.cmd_start(channel, nickname)
|
||||
elif command == "stats":
|
||||
await self.cmd_stats(channel, nickname, args)
|
||||
|
||||
async def cmd_help(self, channel, nickname):
|
||||
"""Send help URL to prevent rate limiting"""
|
||||
self.send_message(channel, f"{nickname}: Complete command reference available at: http://localhost:8080/help")
|
||||
|
||||
async def cmd_start(self, channel, nickname):
|
||||
"""Start a new player"""
|
||||
player = await self.get_player(nickname)
|
||||
if player:
|
||||
self.send_message(channel, f"{nickname}: You already have an account! Use !team to see your pets.")
|
||||
return
|
||||
|
||||
player_id = await self.database.create_player(nickname)
|
||||
starter_pet = await self.game_engine.give_starter_pet(player_id)
|
||||
|
||||
self.send_message(channel,
|
||||
f"🎉 {nickname}: Welcome to the world of pets! You received a Level {starter_pet['level']} {starter_pet['species_name']}! You are now in Starter Town.")
|
||||
|
||||
async def cmd_stats(self, channel, nickname, args):
|
||||
"""Show player statistics"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
self.send_message(channel,
|
||||
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
225
modules/exploration.py
Normal file
225
modules/exploration.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Exploration commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Exploration(BaseModule):
|
||||
"""Handles exploration, travel, location, weather, and wild commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "explore":
|
||||
await self.cmd_explore(channel, nickname)
|
||||
elif command == "travel":
|
||||
await self.cmd_travel(channel, nickname, args)
|
||||
elif command in ["location", "where"]:
|
||||
await self.cmd_location(channel, nickname)
|
||||
elif command == "weather":
|
||||
await self.cmd_weather(channel, nickname)
|
||||
elif command == "wild":
|
||||
await self.cmd_wild(channel, nickname, args)
|
||||
elif command in ["catch", "capture"]:
|
||||
await self.cmd_catch(channel, nickname)
|
||||
|
||||
async def cmd_explore(self, channel, nickname):
|
||||
"""Explore current location"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
encounter = await self.game_engine.explore_location(player["id"])
|
||||
|
||||
if encounter["type"] == "error":
|
||||
self.send_message(channel, f"{nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "empty":
|
||||
self.send_message(channel, f"🔍 {nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "encounter":
|
||||
# Store the encounter for potential catching
|
||||
self.bot.active_encounters[player["id"]] = encounter["pet"]
|
||||
|
||||
pet = encounter["pet"]
|
||||
type_str = pet["type1"]
|
||||
if pet["type2"]:
|
||||
type_str += f"/{pet['type2']}"
|
||||
|
||||
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!")
|
||||
|
||||
async def cmd_travel(self, channel, nickname, args):
|
||||
"""Travel to a different location"""
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify where to travel! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
destination = " ".join(args).title() # Normalize to Title Case
|
||||
location = await self.database.get_location_by_name(destination)
|
||||
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!")
|
||||
return
|
||||
|
||||
# Check if player can access this location
|
||||
missing_requirements = await self.database.get_missing_location_requirements(player["id"], location["id"])
|
||||
if missing_requirements:
|
||||
# Build specific message about required achievements
|
||||
if len(missing_requirements) == 1:
|
||||
achievement = missing_requirements[0]
|
||||
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievement: '{achievement['name']}' - {achievement['description']}")
|
||||
else:
|
||||
achievement_names = [f"'{req['name']}'" for req in missing_requirements]
|
||||
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievements: {', '.join(achievement_names)}. Use !achievements to see progress.")
|
||||
return
|
||||
|
||||
# Clear any active encounters when traveling
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
await self.database.update_player_location(player["id"], location["id"])
|
||||
|
||||
# Show weather info
|
||||
weather = await self.database.get_location_weather(location["id"])
|
||||
weather_msg = ""
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
weather_msg = f" Weather: {weather['weather_type']} - {weather_desc}"
|
||||
|
||||
self.send_message(channel, f"🗺️ {nickname}: You traveled to {destination}. {location['description']}{weather_msg}")
|
||||
|
||||
async def cmd_location(self, channel, nickname):
|
||||
"""Show current location"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
location = await self.database.get_player_location(player["id"])
|
||||
if location:
|
||||
self.send_message(channel, f"📍 {nickname}: You are currently in {location['name']}. {location['description']}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost! Contact an admin.")
|
||||
|
||||
async def cmd_weather(self, channel, nickname):
|
||||
"""Show current weather"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
location = await self.database.get_player_location(player["id"])
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost!")
|
||||
return
|
||||
|
||||
weather = await self.database.get_location_weather(location["id"])
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
|
||||
self.send_message(channel, f"🌤️ {nickname}: Current weather in {location['name']}: {weather['weather_type']}")
|
||||
self.send_message(channel, f"Effect: {weather_desc}")
|
||||
else:
|
||||
self.send_message(channel, f"🌤️ {nickname}: The weather in {location['name']} is calm with no special effects.")
|
||||
|
||||
async def cmd_wild(self, channel, nickname, args):
|
||||
"""Show wild pets in location (defaults to current)"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
if args:
|
||||
# Specific location requested
|
||||
location_name = " ".join(args).title()
|
||||
else:
|
||||
# Default to current location
|
||||
current_location = await self.database.get_player_location(player["id"])
|
||||
if not current_location:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost!")
|
||||
return
|
||||
location_name = current_location["name"]
|
||||
|
||||
wild_pets = await self.game_engine.get_location_spawns(location_name)
|
||||
|
||||
if wild_pets:
|
||||
pet_list = ", ".join([pet["name"] for pet in wild_pets])
|
||||
self.send_message(channel, f"🌿 Wild pets in {location_name}: {pet_list}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No location found called '{location_name}'")
|
||||
|
||||
async def cmd_catch(self, channel, nickname):
|
||||
"""Catch a pet during exploration or battle"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
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:
|
||||
# Catching during battle
|
||||
wild_pet = active_battle["wild_pet"]
|
||||
current_hp = active_battle["wild_hp"]
|
||||
max_hp = wild_pet["max_hp"]
|
||||
|
||||
# Calculate catch rate based on remaining HP (lower HP = higher catch rate)
|
||||
hp_percentage = current_hp / max_hp
|
||||
base_catch_rate = 0.3 # Lower base rate than direct catch
|
||||
hp_bonus = (1.0 - hp_percentage) * 0.5 # Up to 50% bonus for low HP
|
||||
final_catch_rate = min(0.9, base_catch_rate + hp_bonus) # Cap at 90%
|
||||
|
||||
import random
|
||||
if random.random() < final_catch_rate:
|
||||
# Successful catch during battle
|
||||
result = await self.game_engine.attempt_catch_current_location(player["id"], wild_pet)
|
||||
|
||||
# End the battle
|
||||
await_result = await self.game_engine.battle_engine.end_battle(player["id"], "caught")
|
||||
|
||||
# Check for achievements
|
||||
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
|
||||
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
|
||||
|
||||
all_achievements = type_achievements + total_achievements
|
||||
if all_achievements:
|
||||
for achievement in all_achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove encounter
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: Caught the {wild_pet['species_name']} during battle! (HP: {current_hp}/{max_hp})")
|
||||
else:
|
||||
# Failed catch - battle continues
|
||||
catch_percentage = int(final_catch_rate * 100)
|
||||
self.send_message(channel, f"🎯 {nickname}: The {wild_pet['species_name']} broke free! ({catch_percentage}% catch rate) Battle continues!")
|
||||
return
|
||||
|
||||
# Regular exploration catch (not in battle)
|
||||
if player["id"] not in self.bot.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet, or be in battle to catch!")
|
||||
return
|
||||
|
||||
target_pet = self.bot.active_encounters[player["id"]]
|
||||
result = await self.game_engine.attempt_catch_current_location(player["id"], target_pet)
|
||||
|
||||
# Check for achievements after successful catch
|
||||
if "Success!" in result:
|
||||
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
|
||||
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
|
||||
|
||||
all_achievements = type_achievements + total_achievements
|
||||
if all_achievements:
|
||||
for achievement in all_achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove the encounter regardless of success
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: {result}")
|
||||
149
modules/pet_management.py
Normal file
149
modules/pet_management.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Pet management commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class PetManagement(BaseModule):
|
||||
"""Handles team, pets, and future pet management commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["team", "pets", "activate", "deactivate", "swap"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "team":
|
||||
await self.cmd_team(channel, nickname)
|
||||
elif command == "pets":
|
||||
await self.cmd_pets(channel, nickname)
|
||||
elif command == "activate":
|
||||
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)
|
||||
|
||||
async def cmd_team(self, channel, nickname):
|
||||
"""Show active pets (channel display)"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=False)
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.")
|
||||
return
|
||||
|
||||
# Show active pets first, then others
|
||||
active_pets = [pet for pet in pets if pet.get("is_active")]
|
||||
inactive_pets = [pet for pet in pets if not pet.get("is_active")]
|
||||
|
||||
team_info = []
|
||||
|
||||
# Active pets with star
|
||||
for pet in active_pets:
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
# Inactive pets
|
||||
for pet in inactive_pets[:5]: # Show max 5 inactive
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
if len(inactive_pets) > 5:
|
||||
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
|
||||
|
||||
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
|
||||
|
||||
async def cmd_pets(self, channel, nickname):
|
||||
"""Show link to pet collection web page"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
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://localhost:8080/player/{nickname}")
|
||||
|
||||
async def cmd_activate(self, channel, nickname, args):
|
||||
"""Activate a pet for battle (PM only)"""
|
||||
# Redirect to PM for privacy
|
||||
if not args:
|
||||
self.send_pm(nickname, "Usage: !activate <pet_name>")
|
||||
self.send_message(channel, f"{nickname}: Pet activation instructions sent via PM!")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
pet_name = " ".join(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!")
|
||||
self.send_message(channel, f"{nickname}: Pet activated successfully!")
|
||||
else:
|
||||
self.send_pm(nickname, f"❌ {result['error']}")
|
||||
self.send_message(channel, f"{nickname}: Pet activation failed - check PM for details!")
|
||||
|
||||
async def cmd_deactivate(self, channel, nickname, args):
|
||||
"""Deactivate a pet to storage (PM only)"""
|
||||
# Redirect to PM for privacy
|
||||
if not args:
|
||||
self.send_pm(nickname, "Usage: !deactivate <pet_name>")
|
||||
self.send_message(channel, f"{nickname}: Pet deactivation instructions sent via PM!")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
pet_name = " ".join(args)
|
||||
result = await self.database.deactivate_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} moved to storage!")
|
||||
self.send_message(channel, f"{nickname}: Pet deactivated successfully!")
|
||||
else:
|
||||
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!")
|
||||
Loading…
Add table
Add a link
Reference in a new issue