diff --git a/modules/exploration.py b/modules/exploration.py index ccbc2e3..485a292 100644 --- a/modules/exploration.py +++ b/modules/exploration.py @@ -90,7 +90,17 @@ class Exploration(BaseModule): self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!") return - # Check if player can access this location + # 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"]) + if all_new_achievements: + for achievement in all_new_achievements: + self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}") + + # Now check if player can access this location (after awarding achievements) missing_requirements = await self.database.get_missing_location_requirements(player["id"], location["id"]) if missing_requirements: # Build specific message about required achievements diff --git a/run_bot_debug.py b/run_bot_debug.py index 67696b4..5e79860 100644 --- a/run_bot_debug.py +++ b/run_bot_debug.py @@ -53,6 +53,14 @@ class PetBotDebug: self.load_modules() print("✅ Modules loaded") + print("🔄 Validating player data and achievements...") + loop.run_until_complete(self.validate_all_player_data()) + print("✅ Player data validation complete") + + print("🔄 Starting background validation task...") + self.start_background_validation(loop) + print("✅ Background validation started") + print("🔄 Starting web server...") self.web_server = PetBotWebServer(self.database, port=8080) self.web_server.start_in_thread() @@ -92,6 +100,64 @@ class PetBotDebug: print(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands") + async def validate_all_player_data(self): + """Validate and refresh all player data on startup to prevent state loss""" + try: + # Get all players from database + import aiosqlite + async with aiosqlite.connect(self.database.db_path) as db: + cursor = await db.execute("SELECT id, nickname FROM players") + players = await cursor.fetchall() + + print(f"🔄 Found {len(players)} players to validate...") + + for player_id, nickname in players: + try: + # Check and award any missing achievements for each player + new_achievements = await self.game_engine.check_all_achievements(player_id) + + if new_achievements: + print(f" 🏆 {nickname}: Restored {len(new_achievements)} missing achievements") + for achievement in new_achievements: + print(f" - {achievement['name']}") + else: + print(f" ✅ {nickname}: All achievements up to date") + + # Validate team composition + team_composition = await self.database.get_team_composition(player_id) + if team_composition["active_pets"] == 0 and team_composition["total_pets"] > 0: + # Player has pets but none active - activate the first one + pets = await self.database.get_player_pets(player_id) + if pets: + first_pet = pets[0] + await self.database.activate_pet(player_id, str(first_pet["id"])) + print(f" 🔧 {nickname}: Auto-activated pet {first_pet['nickname'] or first_pet['species_name']} (no active pets)") + + except Exception as e: + print(f" ❌ Error validating {nickname}: {e}") + + print("✅ All player data validated and updated") + + except Exception as e: + print(f"❌ Error during player data validation: {e}") + # Don't fail startup if validation fails + + def start_background_validation(self, loop): + """Start background task to periodically validate player data""" + import asyncio + + async def periodic_validation(): + while True: + try: + await asyncio.sleep(1800) # Run every 30 minutes + print("🔄 Running periodic player data validation...") + await self.validate_all_player_data() + except Exception as e: + print(f"❌ Error in background validation: {e}") + + # Create background task + loop.create_task(periodic_validation()) + async def reload_modules(self): """Reload all modules (for admin use)""" try: diff --git a/src/database.py b/src/database.py index 6dea100..1b89401 100644 --- a/src/database.py +++ b/src/database.py @@ -515,6 +515,19 @@ class Database: return False + async def check_all_achievements(self, player_id: int) -> List[Dict]: + """Check and award ALL possible achievements for a player""" + all_new_achievements = [] + + # Check all achievement types that might be available + achievement_types = ["catch_type", "catch_total", "explore_count"] + + for achievement_type in achievement_types: + new_achievements = await self.check_player_achievements(player_id, achievement_type, "") + all_new_achievements.extend(new_achievements) + + return all_new_achievements + async def get_player_achievements(self, player_id: int) -> List[Dict]: """Get all achievements earned by player""" async with aiosqlite.connect(self.db_path) as db: diff --git a/src/game_engine.py b/src/game_engine.py index 66cd6a4..59af9af 100644 --- a/src/game_engine.py +++ b/src/game_engine.py @@ -529,6 +529,10 @@ class GameEngine: """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) diff --git a/webserver.py b/webserver.py index 3644896..1d83c2a 100644 --- a/webserver.py +++ b/webserver.py @@ -606,7 +606,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Get all locations cursor = await db.execute(""" SELECT l.*, - GROUP_CONCAT(ps.name || ' (' || ps.type1 || + GROUP_CONCAT(DISTINCT ps.name || ' (' || ps.type1 || CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns FROM locations l LEFT JOIN location_spawns ls ON l.id = ls.location_id @@ -645,13 +645,28 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if not spawns or spawns == 'None': spawns = "No pets spawn here yet" - # Split spawns into a readable list - spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else [] + # Split spawns into a readable list and remove duplicates + if spawns != "No pets spawn here yet": + spawn_list = list(set([spawn.strip() for spawn in spawns.split(',') if spawn.strip()])) + spawn_list.sort() # Sort alphabetically for consistency + else: + spawn_list = [] + spawn_badges = "" - for spawn in spawn_list[:6]: # Limit to first 6 for display - spawn_badges += f'{spawn.strip()}' - if len(spawn_list) > 6: - spawn_badges += f'+{len(spawn_list) - 6} more' + visible_spawns = spawn_list[:6] # Show first 6 + hidden_spawns = spawn_list[6:] # Hide the rest + + # Add visible spawn badges + for spawn in visible_spawns: + spawn_badges += f'{spawn}' + + # Add hidden spawn badges (initially hidden) + if hidden_spawns: + location_id = location['id'] + for spawn in hidden_spawns: + spawn_badges += f'{spawn}' + # Add functional "show more" button + spawn_badges += f'+{len(hidden_spawns)} more' if not spawn_badges: spawn_badges = 'No pets spawn here yet' @@ -1305,7 +1320,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 'nickname': row[3], 'level': row[4], 'experience': row[5], 'hp': row[6], 'max_hp': row[7], 'attack': row[8], 'defense': row[9], 'speed': row[10], 'happiness': row[11], - 'caught_at': row[12], 'is_active': row[13], + 'caught_at': row[12], 'is_active': bool(row[13]), # Convert to proper boolean 'species_name': row[14], 'type1': row[15], 'type2': row[16] } pets.append(pet_dict) @@ -2041,7 +2056,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): active_pets = [pet for pet in pets if pet['is_active']] inactive_pets = [pet for pet in pets if not pet['is_active']] - # Generate detailed pet cards + # Debug logging + print(f"Team Builder Debug for {nickname}:") + print(f"Total pets: {len(pets)}") + active_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in active_pets] + inactive_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in inactive_pets] + print(f"Active pets: {len(active_pets)} - {active_names}") + print(f"Inactive pets: {len(inactive_pets)} - {inactive_names}") + + # Generate detailed pet cards with debugging def make_pet_card(pet, is_active): name = pet['nickname'] or pet['species_name'] status = "Active" if is_active else "Storage" @@ -2050,6 +2073,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if pet['type2']: type_str += f"/{pet['type2']}" + # Debug logging + print(f"Making pet card for {name} (ID: {pet['id']}): is_active={pet['is_active']}, passed_is_active={is_active}, status_class={status_class}") + # Calculate HP percentage for health bar hp_percent = (pet['hp'] / pet['max_hp']) * 100 if pet['max_hp'] > 0 else 0 hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336" @@ -2530,23 +2556,19 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): console.log(`Container ${{id}}: exists=${{!!element}}`); }}); - // Test move functions directly + // Test function availability (non-destructive) if (petCards.length > 0) {{ - console.log('Testing move functions...'); + console.log('Testing function availability...'); const testCard = petCards[0]; const petId = testCard.dataset.petId; const isCurrentlyActive = currentTeam[petId]; console.log(`Test pet ${{petId}} is currently: ${{isCurrentlyActive ? 'active' : 'storage'}}`); - - // Test moving to opposite state - if (isCurrentlyActive) {{ - console.log('Testing move to storage...'); - movePetToStorage(petId); - }} else {{ - console.log('Testing move to active...'); - movePetToActive(petId); - }} + console.log('Move functions available:', {{ + movePetToStorage: typeof movePetToStorage === 'function', + movePetToActive: typeof movePetToActive === 'function' + }}); + console.log('✅ Test complete - no pets were moved'); }} }} @@ -2579,17 +2601,46 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }} - // Initialize team state with debugging - console.log('Initializing team state...'); - document.querySelectorAll('.pet-card').forEach((card, index) => {{ + // Declare container variables once at the top level + const activeContainer = document.getElementById('active-container'); + const storageContainer = document.getElementById('storage-container'); + const activeDrop = document.getElementById('active-drop'); + const storageDrop = document.getElementById('storage-drop'); + + // Initialize team state with detailed debugging + console.log('=== TEAM STATE INITIALIZATION ==='); + const allCards = document.querySelectorAll('.pet-card'); + console.log(`Found ${{allCards.length}} pet cards total`); + console.log(`Active container has ${{activeContainer.children.length}} pets initially`); + console.log(`Storage container has ${{storageContainer.children.length}} pets initially`); + + allCards.forEach((card, index) => {{ const petId = card.dataset.petId; const isActive = card.dataset.active === 'true'; + const currentContainer = card.parentElement.id; + + console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, currentContainer=${{currentContainer}}`); + originalTeam[petId] = isActive; currentTeam[petId] = isActive; - console.log(`Pet ${{index}}: ID=${{petId}}, isActive=${{isActive}}, parentContainer=${{card.parentElement.id}}`); + // CRITICAL: Verify container placement is correct - DO NOT MOVE unless absolutely necessary + const expectedContainer = isActive ? activeContainer : storageContainer; + const expectedContainerId = isActive ? 'active-container' : 'storage-container'; + + if (currentContainer !== expectedContainerId) {{ + console.error(`MISMATCH! Pet ${{petId}} is in ${{currentContainer}} but should be in ${{expectedContainerId}} based on data-active=${{card.dataset.active}}`); + console.log(`Moving pet ${{petId}} to correct container...`); + expectedContainer.appendChild(card); + }} else {{ + console.log(`Pet ${{petId}} correctly placed in ${{currentContainer}}`); + }} }}); + console.log('After initialization:'); + console.log(`Active container now has ${{activeContainer.children.length}} pets`); + console.log(`Storage container now has ${{storageContainer.children.length}} pets`); + console.log('Original team state:', originalTeam); console.log('Current team state:', currentTeam); @@ -2627,11 +2678,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }}); - // Set up drop zones - const activeContainer = document.getElementById('active-container'); - const storageContainer = document.getElementById('storage-container'); - const activeDrop = document.getElementById('active-drop'); - const storageDrop = document.getElementById('storage-drop'); + // Set up drop zones (using previously declared variables) [activeContainer, activeDrop].forEach(zone => {{ if (zone) {{ @@ -2714,7 +2761,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return; }} - const activeContainer = document.getElementById('active-container'); const currentIsActive = currentTeam[petId]; console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); @@ -2750,7 +2796,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return; }} - const storageContainer = document.getElementById('storage-container'); const currentIsActive = currentTeam[petId]; console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); @@ -2780,27 +2825,25 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): function updateDropZoneVisibility() {{ - const activeContainer = document.getElementById('active-container'); - const storageContainer = document.getElementById('storage-container'); - const activeDrop = document.getElementById('active-drop'); - const storageDrop = document.getElementById('storage-drop'); + // Using previously declared container variables + // CRITICAL: Only update visual indicators, never move pets // Use CSS classes instead of direct style manipulation - if (activeContainer.children.length > 0) {{ - activeDrop.classList.add('has-pets'); + if (activeContainer && activeContainer.children.length > 0) {{ + if (activeDrop) activeDrop.classList.add('has-pets'); }} else {{ - activeDrop.classList.remove('has-pets'); + if (activeDrop) activeDrop.classList.remove('has-pets'); }} - if (storageContainer.children.length > 0) {{ - storageDrop.classList.add('has-pets'); + if (storageContainer && storageContainer.children.length > 0) {{ + if (storageDrop) storageDrop.classList.add('has-pets'); }} else {{ - storageDrop.classList.remove('has-pets'); + if (storageDrop) storageDrop.classList.remove('has-pets'); }} console.log('Drop zone visibility updated:', {{ - activeContainerPets: activeContainer.children.length, - storageContainerPets: storageContainer.children.length + activeContainerPets: activeContainer ? activeContainer.children.length : 0, + storageContainerPets: storageContainer ? storageContainer.children.length : 0 }}); }} @@ -2913,9 +2956,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Initialize interface with debugging console.log('Starting initialization...'); - // Debug initial state - const activeContainer = document.getElementById('active-container'); - const storageContainer = document.getElementById('storage-container'); + // Debug initial state (using previously declared variables) console.log('Initial state:', {{ activePets: activeContainer.children.length, storagePets: storageContainer.children.length @@ -2925,16 +2966,16 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): addClickToMoveBackup(); // Add double-click as backup updateSaveButton(); + // Delay updateDropZoneVisibility to ensure DOM is fully settled console.log('Before updateDropZoneVisibility...'); - updateDropZoneVisibility(); + setTimeout(() => {{ + console.log('Running delayed updateDropZoneVisibility...'); + updateDropZoneVisibility(); + }}, 100); console.log('Initialization complete.'); - // Run test to verify everything is working - setTimeout(() => {{ - console.log('Running delayed test...'); - runDragDropTest(); - }}, 1000); + // Test available via manual button only - no automatic execution // Add test button for manual debugging const testButton = document.createElement('button'); @@ -3109,13 +3150,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): import sys if hasattr(sys.modules.get('__main__'), 'bot_instance'): bot = sys.modules['__main__'].bot_instance - if hasattr(bot, 'send_team_builder_pin'): - # Use asyncio to run the async method - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(bot.send_team_builder_pin(nickname, pin_code)) - loop.close() + if hasattr(bot, 'send_message'): + # Send directly via bot's send_message method (non-async) + message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)" + bot.send_message(nickname, message) + print(f"✅ PIN sent to {nickname} via IRC") return except Exception as e: print(f"Could not send PIN via IRC bot: {e}")