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:
parent
e920503dbd
commit
e17705dc63
10 changed files with 2012 additions and 384 deletions
|
|
@ -2,6 +2,7 @@ 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
|
||||
|
|
@ -18,6 +19,7 @@ class GameEngine:
|
|||
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()
|
||||
|
|
@ -56,9 +58,9 @@ class GameEngine:
|
|||
species.get("rarity", 1), species.get("emoji", "🐾")
|
||||
))
|
||||
await db.commit()
|
||||
print(f"✅ Loaded {len(species_data)} pet species into database")
|
||||
self.logger.info(f"✅ Loaded {len(species_data)} pet species into database")
|
||||
else:
|
||||
print(f"✅ Found {existing_count} existing pet species - skipping reload to preserve IDs")
|
||||
self.logger.info(f"✅ Found {existing_count} existing pet species - skipping reload to preserve IDs")
|
||||
|
||||
except FileNotFoundError:
|
||||
await self.create_default_species()
|
||||
|
|
@ -207,30 +209,44 @@ class GameEngine:
|
|||
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
|
||||
attack, defense, speed, is_active, team_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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["hp"], pet_data["attack"],
|
||||
pet_data["defense"], pet_data["speed"], True, 1))
|
||||
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:
|
||||
iv_bonus = random.randint(0, 31)
|
||||
# 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)
|
||||
|
||||
hp = int((2 * species["base_hp"] + iv_bonus) * level / 100) + level + 10
|
||||
attack = int((2 * species["base_attack"] + iv_bonus) * level / 100) + 5
|
||||
defense = int((2 * species["base_defense"] + iv_bonus) * level / 100) + 5
|
||||
speed = int((2 * species["base_speed"] + iv_bonus) * level / 100) + 5
|
||||
# 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
|
||||
"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:
|
||||
|
|
@ -270,11 +286,14 @@ class GameEngine:
|
|||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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["hp"], pet_stats["attack"],
|
||||
pet_stats["defense"], pet_stats["speed"], False))
|
||||
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']}!"
|
||||
|
|
@ -293,10 +312,11 @@ class GameEngine:
|
|||
return []
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT ps.name, ps.type1, ps.type2, ls.spawn_rate
|
||||
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()
|
||||
|
||||
|
|
@ -437,12 +457,15 @@ class GameEngine:
|
|||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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"]["hp"],
|
||||
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"]["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']}!"
|
||||
|
|
@ -482,7 +505,7 @@ class GameEngine:
|
|||
await db.commit()
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No achievements.json found, skipping achievement loading")
|
||||
self.logger.warning("No achievements.json found, skipping achievement loading")
|
||||
|
||||
async def init_weather_system(self):
|
||||
"""Initialize random weather for all locations"""
|
||||
|
|
@ -497,7 +520,7 @@ class GameEngine:
|
|||
await self.start_weather_system()
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No weather_patterns.json found, skipping weather system")
|
||||
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):
|
||||
|
|
@ -579,12 +602,12 @@ class GameEngine:
|
|||
async def start_weather_system(self):
|
||||
"""Start the background weather update task"""
|
||||
if self.weather_task is None or self.weather_task.done():
|
||||
print("🌤️ Starting weather update background task...")
|
||||
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"""
|
||||
print("🌤️ Stopping weather update background 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()
|
||||
|
|
@ -610,37 +633,37 @@ class GameEngine:
|
|||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in weather update loop: {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:
|
||||
print("Weather update task cancelled")
|
||||
self.logger.info("Weather update task cancelled")
|
||||
|
||||
async def _check_and_update_expired_weather(self):
|
||||
"""Check for expired weather and update it"""
|
||||
"""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
|
||||
# Find locations with expired weather and get their current weather
|
||||
cursor = await db.execute("""
|
||||
SELECT l.id, l.name
|
||||
SELECT l.id, l.name, lw.weather_type as current_weather
|
||||
FROM locations l
|
||||
WHERE l.id NOT IN (
|
||||
SELECT location_id FROM location_weather
|
||||
WHERE active_until > datetime('now')
|
||||
)
|
||||
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:
|
||||
print(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather")
|
||||
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"])
|
||||
possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["calm"])
|
||||
|
||||
# Choose random weather
|
||||
weather_type = random.choice(possible_weather)
|
||||
|
|
@ -668,12 +691,61 @@ class GameEngine:
|
|||
",".join(weather_config.get("affected_types", []))
|
||||
))
|
||||
|
||||
print(f" 🌤️ {location_name}: New {weather_type} weather for {duration_minutes} minutes")
|
||||
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:
|
||||
print(f"Error checking expired weather: {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"""
|
||||
|
|
@ -710,12 +782,12 @@ class GameEngine:
|
|||
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():
|
||||
print("🏥 Starting pet recovery background task...")
|
||||
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"""
|
||||
print("🏥 Stopping pet recovery background 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:
|
||||
|
|
@ -738,27 +810,27 @@ class GameEngine:
|
|||
eligible_pets = await self.database.get_pets_for_auto_recovery()
|
||||
|
||||
if eligible_pets:
|
||||
print(f"🏥 Auto-recovering {len(eligible_pets)} pet(s) after 30 minutes...")
|
||||
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:
|
||||
print(f" 🏥 Auto-recovered {pet['nickname'] or pet['species_name']} (ID: {pet['id']}) to 1 HP")
|
||||
self.logger.info(f" 🏥 Auto-recovered {pet['nickname'] or pet['species_name']} (ID: {pet['id']}) to 1 HP")
|
||||
else:
|
||||
print(f" ❌ Failed to auto-recover pet ID: {pet['id']}")
|
||||
self.logger.error(f" ❌ Failed to auto-recover pet ID: {pet['id']}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in pet recovery loop: {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:
|
||||
print("Pet recovery task cancelled")
|
||||
self.logger.info("Pet recovery task cancelled")
|
||||
|
||||
async def shutdown(self):
|
||||
"""Gracefully shutdown the game engine"""
|
||||
print("🔄 Shutting down game engine...")
|
||||
self.logger.info("🔄 Shutting down game engine...")
|
||||
await self.stop_weather_system()
|
||||
await self.stop_pet_recovery_system()
|
||||
Loading…
Add table
Add a link
Reference in a new issue