From 08f7aa8ea8132f9b3191a55eab72a0ab7ceb5fe5 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Mon, 14 Jul 2025 21:10:28 +0100 Subject: [PATCH] Fix database constraints and team builder save functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- PROJECT_STATUS.md | 27 ++++-- src/database.py | 159 +++++++++++++++++++++++++++++++++- webserver.py | 216 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 382 insertions(+), 20 deletions(-) diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 82f18e7..ced467c 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -70,17 +70,30 @@ Web: Enter PIN → Validate PIN → Apply Database Changes → Clear Request ## 🐛 Known Issues & Bugs -### None Currently Known ✅ -All major bugs have been resolved: -- ✅ Fixed gym battle syntax errors -- ✅ Fixed player profile crashes with encounter data -- ✅ Fixed database column mapping issues -- ✅ Fixed indentation and import errors +### Current Issues - CRITICAL +- **Team Builder Auto-Move** - Active pets get moved to storage on page load automatically +- **Save Button Not Working** - Team builder save returns 501 "not implemented" error +- **Experience Not Saving** - Pet EXP doesn't persist or isn't visible in profiles +- **Achievement Travel Bug** - Players can't travel despite having achievements until catching a pet +- **Team Builder Save Auth** - PIN verification system not implemented in handlers + +### Current Issues - HIGH PRIORITY +- **Item Spawn Rate** - Items too common, need lower spawn rates +- **Location Wild Pets** - Same pets listed multiple times, broken "+(number) more" button +- **Petdex Accuracy** - Missing types in summary, showing pet instances instead of locations +- **Pet Spawns Logic** - Common pets should spawn in most compatible locations +- **Missing Database Tables** - No species_moves table for move learning system + +### Current Issues - MEDIUM PRIORITY +- **Team Command Navigation** - !team should link to web team page instead of IRC output +- **Pet Rename UX** - Should be done via team builder with edit buttons + PIN auth +- **Max Team Size** - Need to implement and enforce team size limits ### Recently Fixed - **Player Profile Crash** - Fixed 'int' object has no attribute 'split' error -- **Battle System Syntax** - Resolved indentation issues in gym battle completion +- **Battle System Syntax** - Resolved indentation issues in gym battle completion - **Encounter Data Mapping** - Corrected SQL column indices +- **Team Builder Drag-Drop** - Fixed drag and drop functionality with backup double-click --- diff --git a/src/database.py b/src/database.py index b0b1382..6dea100 100644 --- a/src/database.py +++ b/src/database.py @@ -315,6 +315,58 @@ class Database: ) """) + # Create species_moves table for move learning system + await db.execute(""" + CREATE TABLE IF NOT EXISTS species_moves ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + species_id INTEGER NOT NULL, + move_id INTEGER NOT NULL, + learn_level INTEGER NOT NULL, + learn_method TEXT DEFAULT 'level', + FOREIGN KEY (species_id) REFERENCES pet_species (id), + FOREIGN KEY (move_id) REFERENCES moves (id), + UNIQUE(species_id, move_id, learn_level) + ) + """) + + # Create location type compatibility table + await db.execute(""" + CREATE TABLE IF NOT EXISTS location_type_compatibility ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location_id INTEGER NOT NULL, + pet_type TEXT NOT NULL, + compatibility_modifier REAL DEFAULT 1.0, + FOREIGN KEY (location_id) REFERENCES locations (id), + UNIQUE(location_id, pet_type) + ) + """) + + # Create indexes for performance optimization + await db.execute("CREATE INDEX IF NOT EXISTS idx_pets_player_active ON pets (player_id, is_active)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_pets_species ON pets (species_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_player_encounters_player ON player_encounters (player_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_player_encounters_species ON player_encounters (species_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_location_spawns_location ON location_spawns (location_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_species_moves_species ON species_moves (species_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_species_moves_level ON species_moves (learn_level)") + + # Add team size validation trigger + await db.execute(""" + CREATE TRIGGER IF NOT EXISTS validate_team_size_on_update + AFTER UPDATE OF is_active ON pets + WHEN NEW.is_active != OLD.is_active + BEGIN + UPDATE pets SET is_active = OLD.is_active + WHERE id = NEW.id AND ( + SELECT COUNT(*) FROM pets + WHERE player_id = NEW.player_id AND is_active = TRUE + ) < 1; + END + """) + + # Enable foreign key constraints + await db.execute("PRAGMA foreign_keys = ON") + await db.commit() async def get_player(self, nickname: str) -> Optional[Dict]: @@ -863,10 +915,10 @@ class Database: "levels_gained": new_level - old_level } - # Update experience + # Update experience and level await db.execute(""" - UPDATE pets SET experience = ? WHERE id = ? - """, (new_exp, pet_id)) + UPDATE pets SET experience = ?, level = ? WHERE id = ? + """, (new_exp, new_level, pet_id)) # Handle level up if it occurred if new_level > old_level: @@ -1585,4 +1637,105 @@ class Database: "success": True, "pins_cleaned": pins_cleaned, "changes_cleaned": changes_cleaned + } + + # Species Moves System Methods + async def get_species_moves(self, species_id: int, max_level: int = None) -> List[Dict]: + """Get moves that a species can learn up to a certain level""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + query = """ + SELECT sm.*, m.name, m.type, m.category, m.power, m.accuracy, m.pp, m.description + FROM species_moves sm + JOIN moves m ON sm.move_id = m.id + WHERE sm.species_id = ? + """ + params = [species_id] + + if max_level: + query += " AND sm.learn_level <= ?" + params.append(max_level) + + query += " ORDER BY sm.learn_level ASC" + + cursor = await db.execute(query, params) + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def get_learnable_moves_for_pet(self, pet_id: int) -> List[Dict]: + """Get moves a specific pet can learn at their current level""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Get pet info + cursor = await db.execute(""" + SELECT p.species_id, p.level, p.id + FROM pets p WHERE p.id = ? + """, (pet_id,)) + pet = await cursor.fetchone() + + if not pet: + return [] + + # Get moves the pet can learn but doesn't know yet + cursor = await db.execute(""" + SELECT sm.*, m.name, m.type, m.category, m.power, m.accuracy, m.pp, m.description + FROM species_moves sm + JOIN moves m ON sm.move_id = m.id + WHERE sm.species_id = ? AND sm.learn_level <= ? + AND sm.move_id NOT IN ( + SELECT pm.move_id FROM pet_moves pm WHERE pm.pet_id = ? + ) + ORDER BY sm.learn_level DESC + """, (pet["species_id"], pet["level"], pet_id)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def validate_team_composition(self, player_id: int, proposed_changes: Dict) -> Dict: + """Validate team changes before applying them""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Get current pet states + cursor = await db.execute(""" + SELECT id, is_active FROM pets WHERE player_id = ? + """, (player_id,)) + current_pets = {str(row["id"]): bool(row["is_active"]) for row in await cursor.fetchall()} + + # Apply proposed changes to current state + new_state = current_pets.copy() + for pet_id, new_active_state in proposed_changes.items(): + if pet_id in new_state: + new_state[pet_id] = new_active_state + + # Count active pets in new state + active_count = sum(1 for is_active in new_state.values() if is_active) + + # Validate constraints + if active_count < 1: + return {"valid": False, "error": "Must have at least 1 active pet"} + + if active_count > 6: # Pokemon-style 6 pet limit + return {"valid": False, "error": "Cannot have more than 6 active pets"} + + return {"valid": True, "active_count": active_count} + + async def get_team_composition(self, player_id: int) -> Dict: + """Get current team composition stats""" + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + SELECT + COUNT(*) as total_pets, + SUM(CASE WHEN is_active THEN 1 ELSE 0 END) as active_pets, + SUM(CASE WHEN NOT is_active THEN 1 ELSE 0 END) as storage_pets + FROM pets WHERE player_id = ? + """, (player_id,)) + + result = await cursor.fetchone() + return { + "total_pets": result[0], + "active_pets": result[1], + "storage_pets": result[2] } \ No newline at end of file diff --git a/webserver.py b/webserver.py index e779a54..3644896 100644 --- a/webserver.py +++ b/webserver.py @@ -20,6 +20,11 @@ from src.database import Database class PetBotRequestHandler(BaseHTTPRequestHandler): """HTTP request handler for PetBot web server""" + @property + def database(self): + """Get database instance from server""" + return self.server.database + def do_GET(self): """Handle GET requests""" parsed_path = urlparse(self.path) @@ -2442,9 +2447,23 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): color: var(--text-secondary); font-style: italic; }} + + .back-link {{ + color: var(--text-accent); + text-decoration: none; + margin-bottom: 20px; + display: inline-block; + font-weight: 500; + }} + + .back-link:hover {{ + text-decoration: underline; + }} + ← Back to {nickname}'s Profile +

🐾 Team Builder

Drag pets between Active and Storage to build your perfect team

@@ -2560,14 +2579,20 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }} - // Initialize team state - document.querySelectorAll('.pet-card').forEach(card => {{ + // Initialize team state with debugging + console.log('Initializing team state...'); + document.querySelectorAll('.pet-card').forEach((card, index) => {{ const petId = card.dataset.petId; const isActive = card.dataset.active === 'true'; originalTeam[petId] = isActive; currentTeam[petId] = isActive; + + console.log(`Pet ${{index}}: ID=${{petId}}, isActive=${{isActive}}, parentContainer=${{card.parentElement.id}}`); }}); + console.log('Original team state:', originalTeam); + console.log('Current team state:', currentTeam); + // Completely rewritten drag and drop - simpler approach function initializeDragAndDrop() {{ console.log('Initializing drag and drop...'); @@ -2682,13 +2707,21 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} function movePetToActive(petId) {{ + console.log(`movePetToActive called for pet ${{petId}}`); const card = document.querySelector(`[data-pet-id="${{petId}}"]`); - if (!card) return; + if (!card) {{ + console.log(`No card found for pet ${{petId}}`); + return; + }} const activeContainer = document.getElementById('active-container'); const currentIsActive = currentTeam[petId]; + console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); + if (!currentIsActive) {{ + console.log(`Moving pet ${{petId}} to active...`); + // Update state currentTeam[petId] = true; @@ -2704,17 +2737,27 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): updateDropZoneVisibility(); console.log('Pet moved to active successfully'); + }} else {{ + console.log(`Pet ${{petId}} is already active, no move needed`); }} }} function movePetToStorage(petId) {{ + console.log(`movePetToStorage called for pet ${{petId}}`); const card = document.querySelector(`[data-pet-id="${{petId}}"]`); - if (!card) return; + if (!card) {{ + console.log(`No card found for pet ${{petId}}`); + return; + }} const storageContainer = document.getElementById('storage-container'); const currentIsActive = currentTeam[petId]; + console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); + if (currentIsActive) {{ + console.log(`Moving pet ${{petId}} to storage...`); + // Update state currentTeam[petId] = false; @@ -2730,6 +2773,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): updateDropZoneVisibility(); console.log('Pet moved to storage successfully'); + }} else {{ + console.log(`Pet ${{petId}} is already in storage, no move needed`); }} }} @@ -2740,8 +2785,23 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): const activeDrop = document.getElementById('active-drop'); const storageDrop = document.getElementById('storage-drop'); - activeDrop.style.display = activeContainer.children.length > 0 ? 'none' : 'flex'; - storageDrop.style.display = storageContainer.children.length > 0 ? 'none' : 'flex'; + // Use CSS classes instead of direct style manipulation + if (activeContainer.children.length > 0) {{ + activeDrop.classList.add('has-pets'); + }} else {{ + activeDrop.classList.remove('has-pets'); + }} + + if (storageContainer.children.length > 0) {{ + storageDrop.classList.add('has-pets'); + }} else {{ + storageDrop.classList.remove('has-pets'); + }} + + console.log('Drop zone visibility updated:', {{ + activeContainerPets: activeContainer.children.length, + storageContainerPets: storageContainer.children.length + }}); }} function updateSaveButton() {{ @@ -2850,16 +2910,31 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} }}); - // Initialize interface + // Initialize interface with debugging + console.log('Starting initialization...'); + + // Debug initial state + const activeContainer = document.getElementById('active-container'); + const storageContainer = document.getElementById('storage-container'); + console.log('Initial state:', {{ + activePets: activeContainer.children.length, + storagePets: storageContainer.children.length + }}); + initializeDragAndDrop(); addClickToMoveBackup(); // Add double-click as backup updateSaveButton(); + + console.log('Before updateDropZoneVisibility...'); updateDropZoneVisibility(); + console.log('Initialization complete.'); + // Run test to verify everything is working setTimeout(() => {{ + console.log('Running delayed test...'); runDragDropTest(); - }}, 500); + }}, 1000); // Add test button for manual debugging const testButton = document.createElement('button'); @@ -2897,11 +2972,132 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): def handle_team_save(self, nickname): """Handle team save request and generate PIN""" - self.send_json_response({"success": False, "error": "Team save not fully implemented yet"}, 501) + try: + # Get POST data + content_length = int(self.headers.get('Content-Length', 0)) + if content_length == 0: + self.send_json_response({"success": False, "error": "No data provided"}, 400) + return + + post_data = self.rfile.read(content_length).decode('utf-8') + + # Parse JSON data + import json + try: + team_data = json.loads(post_data) + except json.JSONDecodeError: + self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_save_async(nickname, team_data)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 400) + + except Exception as e: + print(f"Error in handle_team_save: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_save_async(self, nickname, team_data): + """Async handler for team save""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Validate team composition + validation = await self.database.validate_team_composition(player["id"], team_data) + if not validation["valid"]: + return {"success": False, "error": validation["error"]} + + # Create pending team change with PIN + import json + result = await self.database.create_pending_team_change( + player["id"], + json.dumps(team_data) + ) + + if result["success"]: + # Send PIN via IRC + self.send_pin_via_irc(nickname, result["pin_code"]) + + return { + "success": True, + "message": "PIN sent to your IRC private messages", + "expires_in_minutes": 10 + } + else: + return result + + except Exception as e: + print(f"Error in _handle_team_save_async: {e}") + return {"success": False, "error": str(e)} def handle_team_verify(self, nickname): """Handle PIN verification and apply team changes""" - self.send_json_response({"success": False, "error": "PIN verification not fully implemented yet"}, 501) + try: + # Get POST data + content_length = int(self.headers.get('Content-Length', 0)) + if content_length == 0: + self.send_json_response({"success": False, "error": "No PIN provided"}, 400) + return + + post_data = self.rfile.read(content_length).decode('utf-8') + + # Parse JSON data + import json + try: + data = json.loads(post_data) + pin_code = data.get("pin", "").strip() + except (json.JSONDecodeError, AttributeError): + self.send_json_response({"success": False, "error": "Invalid data format"}, 400) + return + + if not pin_code: + self.send_json_response({"success": False, "error": "PIN code is required"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_verify_async(nickname, pin_code)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 400) + + except Exception as e: + print(f"Error in handle_team_verify: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_verify_async(self, nickname, pin_code): + """Async handler for PIN verification""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Apply team changes with PIN verification + result = await self.database.apply_team_change(player["id"], pin_code) + + if result["success"]: + return { + "success": True, + "message": f"Team changes applied successfully! {result['changes_applied']} pets updated.", + "changes_applied": result["changes_applied"] + } + else: + return result + + except Exception as e: + print(f"Error in _handle_team_verify_async: {e}") + return {"success": False, "error": str(e)} def send_pin_via_irc(self, nickname, pin_code): """Send PIN to player via IRC private message"""