diff --git a/src/database.py b/src/database.py index f3ff3f7..df02e20 100644 --- a/src/database.py +++ b/src/database.py @@ -616,23 +616,22 @@ class Database: async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row - query = """ - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - """ - params = [player_id] if active_only: - query += " AND p.is_active = TRUE" - - # Order by team position for active pets, then by id for storage pets - query += " ORDER BY CASE WHEN p.is_active THEN COALESCE(p.team_order, 999) ELSE 999 END ASC, p.id ASC" - - cursor = await db.execute(query, params) - rows = await cursor.fetchall() - return [dict(row) for row in rows] + # Use the new active team system + return await self.get_active_pets(player_id) + else: + # Return all pets (existing behavior for storage pets) + query = """ + SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + ORDER BY p.id ASC + """ + cursor = await db.execute(query, (player_id,)) + rows = await cursor.fetchall() + return [dict(row) for row in rows] async def get_player_location(self, player_id: int) -> Optional[Dict]: async with aiosqlite.connect(self.db_path) as db: @@ -1165,19 +1164,59 @@ class Database: return True async def get_active_pets(self, player_id: int) -> List[Dict]: - """Get all active pets for a player""" + """Get all active pets for a player using new active_teams system""" async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row + + # Get the current active team slot cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, - ps.base_hp, ps.base_attack, ps.base_defense, ps.base_speed - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = 1 - ORDER BY p.team_order ASC, p.id ASC + SELECT active_slot FROM active_teams WHERE player_id = ? """, (player_id,)) - rows = await cursor.fetchall() - return [dict(row) for row in rows] + active_team_row = await cursor.fetchone() + + if not active_team_row: + return [] # No active team set + + active_slot = active_team_row[0] + + # Get the team configuration for the active slot + cursor = await db.execute(""" + SELECT team_data FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, active_slot)) + config_row = await cursor.fetchone() + + if not config_row or not config_row[0]: + return [] # No team configuration or empty team + + # Parse the team data + import json + try: + team_data = json.loads(config_row[0]) if isinstance(config_row[0], str) else config_row[0] + except (json.JSONDecodeError, TypeError): + return [] + + # Get full pet details for each pet in the team + active_pets = [] + for pet_data in team_data: + if pet_data and 'id' in pet_data: + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.type1, ps.type2, + ps.base_hp, ps.base_attack, ps.base_defense, ps.base_speed + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.id = ? AND p.player_id = ? + """, (pet_data['id'], player_id)) + pet_row = await cursor.fetchone() + if pet_row: + pet_dict = dict(pet_row) + # Add team_order from the saved configuration + pet_dict['team_order'] = pet_data.get('team_order', len(active_pets) + 1) + active_pets.append(pet_dict) + + # Sort by team_order + active_pets.sort(key=lambda x: x.get('team_order', 999)) + return active_pets def calculate_exp_for_level(self, level: int) -> int: """Calculate total experience needed to reach a level""" @@ -2112,61 +2151,45 @@ class Database: try: await db.execute("BEGIN TRANSACTION") - if team_slot == 1: - # Team 1: Update active team - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE player_id = ? - """, (player_id,)) - - # Activate selected pets - for pet_id_str, position in team_changes.items(): - if position and str(position).isdigit(): - await db.execute(""" - UPDATE pets SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (int(position), int(pet_id_str), player_id)) + # NEW DESIGN: All team slots (1, 2, 3) are saved as configurations + pets_list = [] + for pet_id_str, position in team_changes.items(): + if position: + # Get full pet information from database + cursor = await db.execute(""" + SELECT p.id, p.nickname, p.level, p.hp, p.max_hp, p.attack, p.defense, p.speed, p.happiness, + ps.name as species_name, ps.type1, ps.type2 + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.id = ? AND p.player_id = ? + """, (int(pet_id_str), player_id)) + pet_row = await cursor.fetchone() + + if pet_row: + # Convert Row object to dict properly + pet_dict = { + 'id': pet_row['id'], + 'nickname': pet_row['nickname'], + 'level': pet_row['level'], + 'hp': pet_row['hp'], + 'max_hp': pet_row['max_hp'], + 'attack': pet_row['attack'], + 'defense': pet_row['defense'], + 'speed': pet_row['speed'], + 'happiness': pet_row['happiness'], + 'species_name': pet_row['species_name'], + 'type1': pet_row['type1'], + 'type2': pet_row['type2'], + 'team_order': int(position) + } + pets_list.append(pet_dict) - else: - # Teams 2-3: Save as configuration with full pet details - pets_list = [] - for pet_id_str, position in team_changes.items(): - if position: - # Get full pet information from database - cursor = await db.execute(""" - SELECT p.id, p.nickname, p.level, p.hp, p.max_hp, p.attack, p.defense, p.speed, p.happiness, - ps.name as species_name, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? - """, (int(pet_id_str), player_id)) - pet_row = await cursor.fetchone() - - if pet_row: - # Convert Row object to dict properly - pet_dict = { - 'id': pet_row['id'], - 'nickname': pet_row['nickname'], - 'level': pet_row['level'], - 'hp': pet_row['hp'], - 'max_hp': pet_row['max_hp'], - 'attack': pet_row['attack'], - 'defense': pet_row['defense'], - 'speed': pet_row['speed'], - 'happiness': pet_row['happiness'], - 'species_name': pet_row['species_name'], - 'type1': pet_row['type1'], - 'type2': pet_row['type2'], - 'team_order': int(position) - } - pets_list.append(pet_dict) - - # Save configuration in format expected by web interface - await db.execute(""" - INSERT OR REPLACE INTO team_configurations - (player_id, slot_number, config_name, team_data, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - """, (player_id, team_slot, f"Team {team_slot}", json.dumps(pets_list))) + # Save configuration for any slot (1, 2, or 3) + await db.execute(""" + INSERT OR REPLACE INTO team_configurations + (player_id, slot_number, config_name, team_data, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + """, (player_id, team_slot, f"Team {team_slot}", json.dumps(pets_list))) await db.commit() return {"success": True, "message": f"Team {team_slot} saved successfully"} @@ -2301,37 +2324,118 @@ class Database: return {"valid": True, "active_count": active_count} async def get_active_team(self, player_id: int) -> Dict: - """Get active team pets with their positions""" + """Get active team pets with their positions using new active_teams table design""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Get the active slot for this player + cursor = await db.execute(""" + SELECT active_slot FROM active_teams WHERE player_id = ? + """, (player_id,)) + active_row = await cursor.fetchone() + + if not active_row: + # No active team set, return empty + return {} + + active_slot = active_row['active_slot'] + + # Get the team configuration for the active slot + cursor = await db.execute(""" + SELECT team_data FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, active_slot)) + config_row = await cursor.fetchone() + + if not config_row: + # No configuration for active slot, return empty + return {} + + # Parse the team data and convert to the expected format + import json + try: + team_pets = json.loads(config_row['team_data']) + team_dict = {} + + for pet in team_pets: + team_order = pet.get('team_order') + if team_order: + team_dict[str(team_order)] = { + 'id': pet['id'], + 'name': pet['nickname'] or pet['species_name'], + 'species_name': pet['species_name'], + 'level': pet['level'], + 'hp': pet['hp'], + 'max_hp': pet['max_hp'], + 'type_primary': pet['type1'], + 'type_secondary': pet['type2'], + 'attack': pet['attack'], + 'defense': pet['defense'], + 'speed': pet['speed'] + } + + return team_dict + + except (json.JSONDecodeError, KeyError) as e: + print(f"Error parsing active team data: {e}") + return {} + + async def set_active_team_slot(self, player_id: int, slot_number: int) -> Dict: + """Set which team slot is currently active""" + try: + if slot_number not in [1, 2, 3]: + return {"success": False, "error": "Invalid slot number. Must be 1, 2, or 3"} + + async with aiosqlite.connect(self.db_path) as db: + # Check if the slot has a team configuration + cursor = await db.execute(""" + SELECT team_data FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, slot_number)) + config = await cursor.fetchone() + + if not config: + return {"success": False, "error": f"No team configuration found in slot {slot_number}"} + + # Check if team has pets + import json + try: + team_data = json.loads(config[0]) + if not team_data: + return {"success": False, "error": f"Team {slot_number} is empty"} + except json.JSONDecodeError: + return {"success": False, "error": f"Invalid team data in slot {slot_number}"} + + # Update or insert active team slot + await db.execute(""" + INSERT OR REPLACE INTO active_teams (player_id, active_slot, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """, (player_id, slot_number)) + + await db.commit() + + return { + "success": True, + "message": f"Team {slot_number} is now active", + "active_slot": slot_number, + "pet_count": len(team_data) + } + + except Exception as e: + print(f"Error setting active team slot: {e}") + return {"success": False, "error": str(e)} + + async def get_active_team_slot(self, player_id: int) -> int: + """Get the current active team slot for a player""" async with aiosqlite.connect(self.db_path) as db: cursor = await db.execute(""" - SELECT - p.id, p.nickname, ps.name as species_name, p.level, p.hp, p.max_hp, p.team_order, - ps.type1, ps.type2, p.attack, p.defense, p.speed - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = TRUE - ORDER BY p.team_order ASC + SELECT active_slot FROM active_teams + WHERE player_id = ? """, (player_id,)) + result = await cursor.fetchone() - pets = await cursor.fetchall() - team_dict = {} - - for pet in pets: - team_dict[str(pet[6])] = { # team_order as key - 'id': pet[0], - 'name': pet[1] or pet[2], # nickname or species_name - 'species_name': pet[2], - 'level': pet[3], - 'hp': pet[4], - 'max_hp': pet[5], - 'type_primary': pet[7], - 'type_secondary': pet[8], - 'attack': pet[9], - 'defense': pet[10], - 'speed': pet[11] - } - - return team_dict + # Default to slot 1 if not set + return result[0] if result else 1 async def get_team_composition(self, player_id: int) -> Dict: """Get current team composition stats""" @@ -3025,44 +3129,78 @@ class Database: return False async def get_active_pets(self, player_id: int) -> List[Dict]: - """Get all active pets for a player (excluding fainted pets)""" + """Get all active pets for a player (excluding fainted pets) using new active_teams system""" async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL - ORDER BY p.team_order - """, (player_id,)) - rows = await cursor.fetchall() - return [dict(row) for row in rows] + # Get the current active team slot + cursor = await db.execute(""" + SELECT active_slot FROM active_teams WHERE player_id = ? + """, (player_id,)) + active_team_row = await cursor.fetchone() + + if not active_team_row: + return [] # No active team set + + active_slot = active_team_row[0] + + # Get the team configuration for the active slot + cursor = await db.execute(""" + SELECT team_data FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, active_slot)) + config_row = await cursor.fetchone() + + if not config_row or not config_row[0]: + return [] # No team configuration or empty team + + # Parse the team data + import json + try: + team_data = json.loads(config_row[0]) if isinstance(config_row[0], str) else config_row[0] + except (json.JSONDecodeError, TypeError): + return [] + + # Get full pet details for each pet in the team (excluding fainted) + active_pets = [] + for pet_data in team_data: + if pet_data and 'id' in pet_data: + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.id = ? AND p.player_id = ? AND p.fainted_at IS NULL + """, (pet_data['id'], player_id)) + pet_row = await cursor.fetchone() + if pet_row: + pet_dict = dict(pet_row) + # Add team_order from the saved configuration + pet_dict['team_order'] = pet_data.get('team_order', len(active_pets) + 1) + active_pets.append(pet_dict) + + # Sort by team_order + active_pets.sort(key=lambda x: x.get('team_order', 999)) + return active_pets async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: """Get all pets for a player, optionally filtering to active only""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - if active_only: - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL - ORDER BY p.team_order - """, (player_id,)) - else: + if active_only: + # Use the new active team system (this calls the updated get_active_pets method) + return await self.get_active_pets(player_id) + else: + # Return all pets for storage view + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row cursor = await db.execute(""" SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? - ORDER BY p.is_active DESC, p.team_order, p.level DESC + ORDER BY p.level DESC, p.id ASC """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] + + rows = await cursor.fetchall() + return [dict(row) for row in rows] # Pet Moves System Methods async def get_pet_moves(self, pet_id: int) -> List[Dict]: diff --git a/webserver.py b/webserver.py index a93d42f..1c6bc91 100644 --- a/webserver.py +++ b/webserver.py @@ -5082,8 +5082,10 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Get player and team configurations player = loop.run_until_complete(database.get_player(nickname)) team_configs = [] + current_active_slot = 1 # Default if player: team_configs = loop.run_until_complete(database.get_player_team_configurations(player['id'])) + current_active_slot = loop.run_until_complete(database.get_active_team_slot(player['id'])) # Debug logging print(f"Team Builder Debug for {nickname}:") @@ -5268,6 +5270,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): border-color: var(--text-accent); background: var(--bg-primary); } + + .team-card.active { + border-color: var(--accent-green); + box-shadow: 0 0 15px rgba(83, 255, 169, 0.3); + } .team-card-header { display: flex; @@ -5329,6 +5336,62 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): background: var(--text-accent); color: var(--bg-primary); } + + .team-actions { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 15px; + } + + .swap-team-btn { + background: var(--accent-blue); + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s ease; + } + + .swap-team-btn:hover { + background: #339af0; + transform: translateY(-1px); + } + + .active-badge { + background: var(--accent-green); + color: var(--bg-primary); + padding: 8px 16px; + border-radius: 8px; + font-weight: bold; + text-align: center; + display: block; + } + + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } + } .team-sections { margin-top: 30px; @@ -5944,6 +6007,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Initialize when DOM is ready // Global variables for team management let currentEditingTeam = 1; // Default to team 1 + const playerNickname = '""" + nickname + """'; // Player nickname for API calls document.addEventListener('DOMContentLoaded', function() { console.log('Team Builder: DOM loaded, initializing...'); @@ -5976,6 +6040,107 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Load team data for this slot (to be implemented) loadTeamConfiguration(teamSlot); } + + function showMessage(message, type = 'info') { + // Check if message area exists, if not create it + let messageArea = document.getElementById('message-area'); + if (!messageArea) { + messageArea = document.createElement('div'); + messageArea.id = 'message-area'; + messageArea.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + `; + document.body.appendChild(messageArea); + } + + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${type}`; + messageDiv.style.cssText = ` + background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'}; + color: white; + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 10px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + animation: slideIn 0.3s ease-out; + `; + messageDiv.textContent = message; + + messageArea.appendChild(messageDiv); + + // Remove message after 5 seconds + setTimeout(() => { + messageDiv.style.animation = 'slideOut 0.3s ease-out'; + setTimeout(() => messageDiv.remove(), 300); + }, 5000); + } + + async function swapToTeam(teamSlot) { + console.log('Swapping to team slot:', teamSlot); + + // Show loading state + const swapBtn = document.querySelector(`[data-slot="${teamSlot}"] .swap-team-btn`); + if (swapBtn) { + swapBtn.disabled = true; + swapBtn.textContent = '⏳ Switching...'; + } + + try { + const response = await fetch(`/teambuilder/${playerNickname}/swap/${teamSlot}`, { + method: 'POST' + }); + + const result = await response.json(); + + if (result.success) { + // Update UI to reflect new active team + document.querySelectorAll('.team-card').forEach(card => { + card.classList.remove('active'); + const actions = card.querySelector('.team-actions'); + const slot = card.dataset.slot; + + // Update button/badge + if (slot == teamSlot) { + actions.innerHTML = ` + + 🟢 Active Team + `; + card.classList.add('active'); + } else { + const teamName = card.querySelector('h3').textContent; + actions.innerHTML = ` + + + `; + } + }); + + showMessage(`${result.message}`, 'success'); + } else { + showMessage(`Failed to switch team: ${result.error}`, 'error'); + // Reset button + if (swapBtn) { + swapBtn.disabled = false; + swapBtn.textContent = '🔄 Make Active'; + } + } + } catch (error) { + console.error('Error swapping team:', error); + showMessage('Failed to switch team. Please try again.', 'error'); + // Reset button + if (swapBtn) { + swapBtn.disabled = false; + swapBtn.textContent = '🔄 Make Active'; + } + } + } function loadTeamConfiguration(teamSlot) { console.log('Loading team configuration for slot:', teamSlot); @@ -6608,8 +6773,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): pet_previews = '
Empty
' * 6 status_text = "Empty team" + active_class = "active" if slot == current_active_slot else "" + is_active = slot == current_active_slot + team_cards_html += f''' -
+

Team {slot}

{status_text} @@ -6617,9 +6785,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
{pet_previews}
- +
+ + {'🟢 Active Team' if is_active else f''} +
''' else: @@ -6655,7 +6826,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): pet_previews = '
Empty
' * 6 status_text = f"{config['pet_count']}/6 pets" if config['pet_count'] > 0 else "Empty team" - active_class = "active" if config['slot'] == 1 else "" # Default to team 1 as active + active_class = "active" if config['slot'] == current_active_slot else "" + is_active = config['slot'] == current_active_slot team_cards_html += f'''
@@ -6666,9 +6838,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
{pet_previews}
- +
+ + {'🟢 Active Team' if is_active else f''} +
''' @@ -7842,6 +8017,48 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Error in _handle_team_config_apply_async: {e}") return {"success": False, "error": str(e)} + def handle_team_swap_request(self, nickname, slot): + """Handle team swap request to change active team""" + try: + # Validate slot number + try: + slot_num = int(slot) + if slot_num < 1 or slot_num > 3: + self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) + return + except ValueError: + self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_swap_async(nickname, slot_num)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) + + except Exception as e: + print(f"Error in handle_team_swap_request: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_swap_async(self, nickname, slot_num): + """Async handler for team swapping""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Set the new active team slot + result = await self.database.set_active_team_slot(player["id"], slot_num) + return result + + except Exception as e: + print(f"Error in _handle_team_swap_async: {e}") + return {"success": False, "error": str(e)} + def handle_test_team_save(self, nickname): """Handle test team builder save request and generate PIN""" try: @@ -10372,7 +10589,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): ✏️ Edit Team {team_identifier} - ''' @@ -10660,6 +10877,89 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): margin-top: 8px; }} + + ''' def generate_individual_team_editor_content(self, nickname, team_identifier, team_data, player_pets):