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}")