import json import random import aiosqlite import asyncio import logging from typing import Dict, List, Optional from .database import Database from .battle_engine import BattleEngine class GameEngine: def __init__(self, database: Database): self.database = database self.battle_engine = BattleEngine(database) self.pet_species = {} self.locations = {} self.moves = {} self.type_chart = {} self.weather_patterns = {} self.weather_task = None self.pet_recovery_task = None self.shutdown_event = asyncio.Event() self.logger = logging.getLogger(__name__) async def load_game_data(self): await self.load_pet_species() await self.load_locations() await self.load_moves() await self.load_type_chart() await self.load_achievements() await self.database.initialize_items() await self.database.initialize_gyms() await self.init_weather_system() await self.battle_engine.load_battle_data() await self.start_pet_recovery_system() async def load_pet_species(self): try: with open("config/pets.json", "r") as f: species_data = json.load(f) async with aiosqlite.connect(self.database.db_path) as db: # Check if species already exist to avoid re-inserting and changing IDs cursor = await db.execute("SELECT COUNT(*) FROM pet_species") existing_count = (await cursor.fetchone())[0] if existing_count == 0: # Only insert if no species exist to avoid ID conflicts for species in species_data: await db.execute(""" INSERT OR IGNORE INTO pet_species (name, type1, type2, base_hp, base_attack, base_defense, base_speed, evolution_level, rarity, emoji) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( species["name"], species["type1"], species.get("type2"), species["base_hp"], species["base_attack"], species["base_defense"], species["base_speed"], species.get("evolution_level"), species.get("rarity", 1), species.get("emoji", "🐾") )) await db.commit() self.logger.info(f"✅ Loaded {len(species_data)} pet species into database") else: self.logger.info(f"✅ Found {existing_count} existing pet species - skipping reload to preserve IDs") except FileNotFoundError: await self.create_default_species() async def create_default_species(self): default_species = [ { "name": "Flamey", "type1": "Fire", "base_hp": 45, "base_attack": 52, "base_defense": 43, "base_speed": 65, "rarity": 1 }, { "name": "Aqua", "type1": "Water", "base_hp": 44, "base_attack": 48, "base_defense": 65, "base_speed": 43, "rarity": 1 }, { "name": "Leafy", "type1": "Grass", "base_hp": 45, "base_attack": 49, "base_defense": 49, "base_speed": 45, "rarity": 1 }, { "name": "Sparky", "type1": "Electric", "base_hp": 35, "base_attack": 55, "base_defense": 40, "base_speed": 90, "rarity": 2 }, { "name": "Rocky", "type1": "Rock", "base_hp": 40, "base_attack": 80, "base_defense": 100, "base_speed": 25, "rarity": 2 } ] async with aiosqlite.connect(self.database.db_path) as db: for species in default_species: await db.execute(""" INSERT OR IGNORE INTO pet_species (name, type1, type2, base_hp, base_attack, base_defense, base_speed, rarity) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( species["name"], species["type1"], species.get("type2"), species["base_hp"], species["base_attack"], species["base_defense"], species["base_speed"], species["rarity"] )) await db.commit() async def load_locations(self): try: with open("config/locations.json", "r") as f: locations_data = json.load(f) except FileNotFoundError: locations_data = self.get_default_locations() async with aiosqlite.connect(self.database.db_path) as db: for location in locations_data: await db.execute(""" INSERT OR IGNORE INTO locations (name, description, level_min, level_max) VALUES (?, ?, ?, ?) """, (location["name"], location["description"], location["level_min"], location["level_max"])) cursor = await db.execute( "SELECT id FROM locations WHERE name = ?", (location["name"],) ) location_row = await cursor.fetchone() location_id = location_row[0] for spawn in location["spawns"]: species_cursor = await db.execute( "SELECT id FROM pet_species WHERE name = ?", (spawn["species"],) ) species_id = await species_cursor.fetchone() if species_id: await db.execute(""" INSERT OR IGNORE INTO location_spawns (location_id, species_id, spawn_rate, min_level, max_level) VALUES (?, ?, ?, ?, ?) """, (location_id, species_id[0], spawn["spawn_rate"], spawn["min_level"], spawn["max_level"])) await db.commit() def get_default_locations(self): return [ { "name": "Starter Woods", "description": "A peaceful forest perfect for beginners", "level_min": 1, "level_max": 5, "spawns": [ {"species": "Leafy", "spawn_rate": 0.4, "min_level": 1, "max_level": 3}, {"species": "Flamey", "spawn_rate": 0.3, "min_level": 1, "max_level": 3}, {"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 3} ] }, { "name": "Electric Canyon", "description": "A charged valley crackling with energy", "level_min": 3, "level_max": 8, "spawns": [ {"species": "Sparky", "spawn_rate": 0.6, "min_level": 3, "max_level": 6}, {"species": "Rocky", "spawn_rate": 0.4, "min_level": 4, "max_level": 7} ] } ] async def load_moves(self): pass async def load_type_chart(self): pass async def give_starter_pet(self, player_id: int) -> Dict: starters = ["Flamey", "Aqua", "Leafy"] chosen_starter = random.choice(starters) async with aiosqlite.connect(self.database.db_path) as db: db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT * FROM pet_species WHERE name = ?", (chosen_starter,) ) species = await cursor.fetchone() pet_data = self.generate_pet_stats(dict(species), level=5) cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, attack, defense, speed, is_active, team_order, iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, species["id"], pet_data["level"], 0, pet_data["hp"], pet_data["max_hp"], pet_data["attack"], pet_data["defense"], pet_data["speed"], True, 1, pet_data["iv_hp"], pet_data["iv_attack"], pet_data["iv_defense"], pet_data["iv_speed"], player_id)) await db.commit() return {"species_name": chosen_starter, **pet_data} def generate_pet_stats(self, species: Dict, level: int = 1) -> Dict: # Generate individual IVs for each stat (0-31) iv_hp = random.randint(0, 31) iv_attack = random.randint(0, 31) iv_defense = random.randint(0, 31) iv_speed = random.randint(0, 31) # Calculate stats using individual IVs (Pokemon-style formula) hp = int((2 * species["base_hp"] + iv_hp) * level / 100) + level + 10 attack = int((2 * species["base_attack"] + iv_attack) * level / 100) + 5 defense = int((2 * species["base_defense"] + iv_defense) * level / 100) + 5 speed = int((2 * species["base_speed"] + iv_speed) * level / 100) + 5 return { "level": level, "hp": hp, "max_hp": hp, # Initial HP is max HP "attack": attack, "defense": defense, "speed": speed, # Include IVs in the returned data for storage "iv_hp": iv_hp, "iv_attack": iv_attack, "iv_defense": iv_defense, "iv_speed": iv_speed } async def attempt_catch(self, player_id: int, location_name: str) -> str: async with aiosqlite.connect(self.database.db_path) as db: db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT * FROM locations WHERE name = ?", (location_name,) ) location = await cursor.fetchone() if not location: return f"Location '{location_name}' not found!" cursor = await db.execute(""" SELECT ls.*, ps.name as species_name, ps.* FROM location_spawns ls JOIN pet_species ps ON ls.species_id = ps.id WHERE ls.location_id = ? """, (location["id"],)) spawns = await cursor.fetchall() if not spawns: return f"No pets found in {location_name}!" if random.random() > 0.7: return f"You searched {location_name} but found nothing..." chosen_spawn = random.choices( spawns, weights=[spawn["spawn_rate"] for spawn in spawns] )[0] pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"]) pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level) catch_rate = 0.5 + (0.3 / chosen_spawn["rarity"]) if random.random() < catch_rate: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, attack, defense, speed, is_active, iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, chosen_spawn["species_id"], pet_level, 0, pet_stats["hp"], pet_stats["max_hp"], pet_stats["attack"], pet_stats["defense"], pet_stats["speed"], False, pet_stats["iv_hp"], pet_stats["iv_attack"], pet_stats["iv_defense"], pet_stats["iv_speed"], player_id)) await db.commit() return f"Caught a level {pet_level} {chosen_spawn['species_name']}!" else: return f"A wild {chosen_spawn['species_name']} appeared but escaped!" async def get_location_spawns(self, location_name: str) -> List[Dict]: async with aiosqlite.connect(self.database.db_path) as db: db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT * FROM locations WHERE name = ?", (location_name,) ) location = await cursor.fetchone() if not location: return [] cursor = await db.execute(""" SELECT DISTINCT ps.name, ps.type1, ps.type2, MIN(ls.spawn_rate) as spawn_rate FROM location_spawns ls JOIN pet_species ps ON ls.species_id = ps.id WHERE ls.location_id = ? GROUP BY ps.id, ps.name, ps.type1, ps.type2 """, (location["id"],)) spawns = await cursor.fetchall() return [dict(spawn) for spawn in spawns] async def explore_location(self, player_id: int) -> Dict: """Explore the player's current location and return encounter results""" async with aiosqlite.connect(self.database.db_path) as db: db.row_factory = aiosqlite.Row # Get player's current location location = await self.database.get_player_location(player_id) if not location: return {"type": "error", "message": "You are not in a valid location!"} # Check for item discovery first (30% chance) item_result = await self.check_item_discovery(player_id, location) if item_result: return item_result # Get spawns for current location cursor = await db.execute(""" SELECT ls.*, ps.name as species_name, ps.* FROM location_spawns ls JOIN pet_species ps ON ls.species_id = ps.id WHERE ls.location_id = ? """, (location["id"],)) spawns = await cursor.fetchall() if not spawns: return {"type": "empty", "message": f"You explore {location['name']} but find nothing interesting..."} # Apply weather modifiers to spawns modified_spawns = await self.get_weather_modified_spawns(location["id"], spawns) # Random encounter chance (50% chance of finding a pet after item check) if random.random() > 0.5: return {"type": "empty", "message": f"You explore {location['name']} but find nothing this time..."} # Choose random spawn with weather-modified rates chosen_spawn = random.choices( modified_spawns, weights=[spawn["spawn_rate"] for spawn in modified_spawns] )[0] pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"]) pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level) return { "type": "encounter", "location": location["name"], "pet": { "species_id": chosen_spawn["species_id"], "species_name": chosen_spawn["species_name"], "level": pet_level, "type1": chosen_spawn["type1"], "type2": chosen_spawn["type2"], "stats": pet_stats, # Additional battle-ready stats "hp": pet_stats["hp"], "max_hp": pet_stats["hp"], "attack": pet_stats["attack"], "defense": pet_stats["defense"], "speed": pet_stats["speed"] } } async def check_item_discovery(self, player_id: int, location: Dict) -> Optional[Dict]: """Check if player finds an item during exploration""" import json # Load items config try: with open("config/items.json", "r") as f: items_data = json.load(f) except FileNotFoundError: return None # Get global spawn multiplier from config global_multiplier = items_data.get("_config", {}).get("global_spawn_multiplier", 1.0) # Get all possible items for this location available_items = [] location_name = location["name"].lower().replace(" ", "_") for category_items in items_data.values(): if isinstance(category_items, list): for item in category_items: if "locations" in item: item_locations = item["locations"] if "all" in item_locations or location_name in item_locations: # Apply global multiplier to spawn rate item_copy = item.copy() item_copy["effective_spawn_rate"] = item.get("spawn_rate", 0.1) * global_multiplier available_items.append(item_copy) if not available_items: return None # Calculate total spawn rates for this location (using effective rates) total_rate = sum(item.get("effective_spawn_rate", 0.1) for item in available_items) # 30% base chance of finding an item if random.random() > 0.3: return None # Choose item based on effective spawn rates (with global multiplier applied) chosen_item = random.choices( available_items, weights=[item.get("effective_spawn_rate", 0.1) for item in available_items] )[0] # Add item to player's inventory success = await self.database.add_item_to_inventory(player_id, chosen_item["name"]) if success: # Get rarity info for display rarity_info = items_data.get("rarity_info", {}).get(chosen_item["rarity"], {}) symbol = rarity_info.get("symbol", "○") return { "type": "item_found", "location": location["name"], "item": chosen_item, "symbol": symbol, "message": f"🎒 You found a {symbol} {chosen_item['name']} ({chosen_item['rarity']})! {chosen_item['description']}" } return None async def attempt_catch_current_location(self, player_id: int, target_pet: Dict) -> str: """Attempt to catch a pet during exploration""" catch_rate = 0.5 + (0.3 / target_pet.get("rarity", 1)) if random.random() < catch_rate: # Successfully caught the pet async with aiosqlite.connect(self.database.db_path) as db: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, attack, defense, speed, is_active, iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, target_pet["species_id"], target_pet["level"], 0, target_pet["stats"]["hp"], target_pet["stats"]["max_hp"], target_pet["stats"]["attack"], target_pet["stats"]["defense"], target_pet["stats"]["speed"], False, target_pet["stats"]["iv_hp"], target_pet["stats"]["iv_attack"], target_pet["stats"]["iv_defense"], target_pet["stats"]["iv_speed"], player_id)) await db.commit() return f"Success! You caught the Level {target_pet['level']} {target_pet['species_name']}!" else: return f"The wild {target_pet['species_name']} escaped!" async def load_achievements(self): """Load achievements from config and populate database""" try: with open("config/achievements.json", "r") as f: achievements_data = json.load(f) async with aiosqlite.connect(self.database.db_path) as db: for achievement in achievements_data: # Get location ID if specified location_id = None if achievement.get("unlock_location"): cursor = await db.execute( "SELECT id FROM locations WHERE name = ?", (achievement["unlock_location"],) ) location_row = await cursor.fetchone() if location_row: location_id = location_row[0] # Insert or update achievement await db.execute(""" INSERT OR REPLACE INTO achievements (name, description, requirement_type, requirement_data, unlock_location_id) VALUES (?, ?, ?, ?, ?) """, ( achievement["name"], achievement["description"], achievement["requirement_type"], achievement["requirement_data"], location_id )) await db.commit() except FileNotFoundError: self.logger.warning("No achievements.json found, skipping achievement loading") async def init_weather_system(self): """Initialize random weather for all locations""" try: with open("config/weather_patterns.json", "r") as f: self.weather_patterns = json.load(f) # Set initial weather for all locations await self.update_all_weather() # Start background weather update task await self.start_weather_system() except FileNotFoundError: self.logger.warning("No weather_patterns.json found, skipping weather system") self.weather_patterns = {"weather_types": {}, "location_weather_chances": {}} async def update_all_weather(self): """Update weather for all locations""" import datetime async with aiosqlite.connect(self.database.db_path) as db: # Get all locations cursor = await db.execute("SELECT * FROM locations") locations = await cursor.fetchall() for location in locations: location_name = location[1] # name is second column # Get possible weather for this location possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"]) # Choose random weather weather_type = random.choice(possible_weather) weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, { "spawn_modifier": 1.0, "affected_types": [], "duration_minutes": [90, 180] }) # Calculate end time with random duration duration_range = weather_config.get("duration_minutes", [90, 180]) duration_minutes = random.randint(duration_range[0], duration_range[1]) end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes) # Clear old weather and set new await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location[0],)) await db.execute(""" INSERT INTO location_weather (location_id, weather_type, active_until, spawn_modifier, affected_types) VALUES (?, ?, ?, ?, ?) """, ( location[0], weather_type, end_time.isoformat(), weather_config.get("spawn_modifier", 1.0), ",".join(weather_config.get("affected_types", [])) )) await db.commit() async def check_and_award_achievements(self, player_id: int, action_type: str, data: str = ""): """Check for new achievements after player actions""" return await self.database.check_player_achievements(player_id, action_type, data) async def check_all_achievements(self, player_id: int): """Check and award ALL possible achievements for a player""" return await self.database.check_all_achievements(player_id) async def get_weather_modified_spawns(self, location_id: int, spawns: list) -> list: """Apply weather modifiers to spawn rates""" weather = await self.database.get_location_weather(location_id) if not weather or not weather.get("affected_types"): return spawns # No weather effects affected_types = weather["affected_types"].split(",") if weather["affected_types"] else [] modifier = weather.get("spawn_modifier", 1.0) # Apply weather modifier to matching pet types modified_spawns = [] for spawn in spawns: spawn_dict = dict(spawn) pet_types = [spawn_dict.get("type1")] if spawn_dict.get("type2"): pet_types.append(spawn_dict["type2"]) # Check if this pet type is affected by weather if any(pet_type in affected_types for pet_type in pet_types): spawn_dict["spawn_rate"] *= modifier modified_spawns.append(spawn_dict) return modified_spawns async def start_weather_system(self): """Start the background weather update task""" if self.weather_task is None or self.weather_task.done(): self.logger.info("🌤️ Starting weather update background task...") self.weather_task = asyncio.create_task(self._weather_update_loop()) async def stop_weather_system(self): """Stop the background weather update task""" self.logger.info("🌤️ Stopping weather update background task...") self.shutdown_event.set() if self.weather_task and not self.weather_task.done(): self.weather_task.cancel() try: await self.weather_task except asyncio.CancelledError: pass async def _weather_update_loop(self): """Background task that checks and updates expired weather""" try: while not self.shutdown_event.is_set(): try: # Check every 5 minutes for expired weather await asyncio.sleep(300) # 5 minutes if self.shutdown_event.is_set(): break # Check for locations with expired weather await self._check_and_update_expired_weather() except asyncio.CancelledError: break except Exception as e: self.logger.error(f"Error in weather update loop: {e}") # Continue the loop even if there's an error await asyncio.sleep(60) # Wait a minute before retrying except asyncio.CancelledError: self.logger.info("Weather update task cancelled") async def _check_and_update_expired_weather(self): """Check for expired weather and update it with announcements""" try: async with aiosqlite.connect(self.database.db_path) as db: # Find locations with expired weather and get their current weather cursor = await db.execute(""" SELECT l.id, l.name, lw.weather_type as current_weather FROM locations l LEFT JOIN location_weather lw ON l.id = lw.location_id AND lw.active_until > datetime('now') WHERE lw.location_id IS NULL OR lw.active_until <= datetime('now') """) expired_locations = await cursor.fetchall() if expired_locations: self.logger.info(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather") for location in expired_locations: location_id = location[0] location_name = location[1] previous_weather = location[2] if location[2] else "calm" # Get possible weather for this location possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["calm"]) # Choose random weather weather_type = random.choice(possible_weather) weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, { "spawn_modifier": 1.0, "affected_types": [], "duration_minutes": [90, 180] }) # Calculate end time with random duration duration_range = weather_config.get("duration_minutes", [90, 180]) duration_minutes = random.randint(duration_range[0], duration_range[1]) import datetime end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes) # Clear old weather and set new await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location_id,)) await db.execute(""" INSERT INTO location_weather (location_id, weather_type, active_until, spawn_modifier, affected_types) VALUES (?, ?, ?, ?, ?) """, ( location_id, weather_type, end_time.isoformat(), weather_config.get("spawn_modifier", 1.0), ",".join(weather_config.get("affected_types", [])) )) self.logger.info(f" 🌤️ {location_name}: Weather changed from {previous_weather} to {weather_type} for {duration_minutes} minutes") # Announce weather change to IRC await self.announce_weather_change(location_name, previous_weather, weather_type, "auto") await db.commit() except Exception as e: self.logger.error(f"Error checking expired weather: {e}") async def announce_weather_change(self, location_name: str, previous_weather: str, new_weather: str, source: str = "auto"): """Announce weather changes to IRC channel""" try: # Get weather emojis weather_emojis = { "sunny": "☀️", "rainy": "🌧️", "storm": "⛈️", "blizzard": "❄️", "earthquake": "🌍", "calm": "🌤️" } prev_emoji = weather_emojis.get(previous_weather, "🌤️") new_emoji = weather_emojis.get(new_weather, "🌤️") # Create announcement message if previous_weather == new_weather: return # No change, no announcement if source == "admin": message = f"🌤️ Weather Update: {location_name} weather has been changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather} by admin command!" elif source == "web": message = f"🌤️ Weather Update: {location_name} weather has been changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather} via web interface!" else: message = f"🌤️ Weather Update: {location_name} weather has naturally changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather}!" # Send to IRC channel via bot instance if hasattr(self, 'bot') and self.bot: from config import IRC_CONFIG channel = IRC_CONFIG.get("channel", "#petz") if hasattr(self.bot, 'send_message_sync'): self.bot.send_message_sync(channel, message) elif hasattr(self.bot, 'send_message'): import asyncio if hasattr(self.bot, 'main_loop') and self.bot.main_loop: future = asyncio.run_coroutine_threadsafe( self.bot.send_message(channel, message), self.bot.main_loop ) else: asyncio.create_task(self.bot.send_message(channel, message)) except Exception as e: self.logger.error(f"Error announcing weather change: {e}") async def get_pet_emoji(self, species_name: str) -> str: """Get emoji for a pet species""" try: async with aiosqlite.connect(self.database.db_path) as db: cursor = await db.execute( "SELECT emoji FROM pet_species WHERE name = ?", (species_name,) ) row = await cursor.fetchone() return row[0] if row and row[0] else "🐾" except Exception: return "🐾" # Default emoji if something goes wrong def format_pet_name_with_emoji(self, pet_name: str, emoji: str = None, species_name: str = None) -> str: """Format pet name with emoji for display in IRC or web Args: pet_name: The pet's nickname or species name emoji: Optional emoji to use (if None, will look up by species_name) species_name: Species name for emoji lookup if emoji not provided Returns: Formatted string like "🔥 Flamey" or "🐉 Infernowyrm" """ if emoji: return f"{emoji} {pet_name}" elif species_name: # This would need to be async, but for IRC we can use sync fallback return f"🐾 {pet_name}" # Use default for now, can be enhanced later else: return f"🐾 {pet_name}" async def start_pet_recovery_system(self): """Start the background pet recovery task""" if self.pet_recovery_task is None or self.pet_recovery_task.done(): self.logger.info("🏥 Starting pet recovery background task...") self.pet_recovery_task = asyncio.create_task(self._pet_recovery_loop()) async def stop_pet_recovery_system(self): """Stop the background pet recovery task""" self.logger.info("🏥 Stopping pet recovery background task...") if self.pet_recovery_task and not self.pet_recovery_task.done(): self.pet_recovery_task.cancel() try: await self.pet_recovery_task except asyncio.CancelledError: pass async def _pet_recovery_loop(self): """Background task that checks for pets eligible for auto-recovery""" try: while not self.shutdown_event.is_set(): try: # Check every 5 minutes for pets that need recovery await asyncio.sleep(300) # 5 minutes if self.shutdown_event.is_set(): break # Get pets eligible for auto-recovery (fainted for 30+ minutes) eligible_pets = await self.database.get_pets_for_auto_recovery() if eligible_pets: self.logger.info(f"🏥 Auto-recovering {len(eligible_pets)} pet(s) after 30 minutes...") for pet in eligible_pets: success = await self.database.auto_recover_pet(pet["id"]) if success: self.logger.info(f" 🏥 Auto-recovered {pet['nickname'] or pet['species_name']} (ID: {pet['id']}) to 1 HP") else: self.logger.error(f" ❌ Failed to auto-recover pet ID: {pet['id']}") except asyncio.CancelledError: break except Exception as e: self.logger.error(f"Error in pet recovery loop: {e}") # Continue the loop even if there's an error await asyncio.sleep(60) # Wait a minute before retrying except asyncio.CancelledError: self.logger.info("Pet recovery task cancelled") async def shutdown(self): """Gracefully shutdown the game engine""" self.logger.info("🔄 Shutting down game engine...") await self.stop_weather_system() await self.stop_pet_recovery_system()