Implement complete team swap functionality between web interface and IRC battles
🔄 **New Active Team System** - Replace Team 1 hardcoded active system with flexible active_teams table - Players can now edit ALL teams (1, 2, 3) equally via web interface - Support for swapping any saved team as the active battle team 🌐 **Web Interface Enhancements** - Add "Make Active" buttons to team management hub - Real-time team swapping with loading states and success notifications - Visual indicators for currently active team with green highlighting - Updated team builder to treat all team slots consistently 🎮 **IRC Battle Integration** - Update get_active_pets() and get_player_pets() methods to use new active_teams table - IRC battles (\!battle, \!attack, \!gym) now use web-selected active team - Real-time sync: team swaps via web immediately affect IRC battles - Maintain backward compatibility with existing IRC commands 🛠️ **Database Architecture** - Add active_teams table with player_id -> active_slot mapping - Migrate existing active teams to team_configurations format - Update team save logic to store all teams as configurations - Add set_active_team_slot() and get_active_team_slot() methods ✅ **Key Features** - Seamless web-to-IRC active team synchronization - All teams editable with proper PIN verification - Enhanced UX with animations and proper error handling - Maintains data 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
285a7c4a7e
commit
5293da2921
2 changed files with 578 additions and 140 deletions
318
webserver.py
318
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 = `
|
||||
<button class="edit-team-btn" onclick="selectTeam(${slot})">
|
||||
📝 Edit ${card.querySelector('h3').textContent}
|
||||
</button>
|
||||
<span class="active-badge">🟢 Active Team</span>
|
||||
`;
|
||||
card.classList.add('active');
|
||||
} else {
|
||||
const teamName = card.querySelector('h3').textContent;
|
||||
actions.innerHTML = `
|
||||
<button class="edit-team-btn" onclick="selectTeam(${slot})">
|
||||
📝 Edit ${teamName}
|
||||
</button>
|
||||
<button class="swap-team-btn" onclick="swapToTeam(${slot})">🔄 Make Active</button>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
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 = '<div class="mini-pet empty">Empty</div>' * 6
|
||||
status_text = "Empty team"
|
||||
|
||||
active_class = "active" if slot == current_active_slot else ""
|
||||
is_active = slot == current_active_slot
|
||||
|
||||
team_cards_html += f'''
|
||||
<div class="team-card {'active' if slot == 1 else ''}" data-slot="{slot}">
|
||||
<div class="team-card {active_class}" data-slot="{slot}">
|
||||
<div class="team-card-header">
|
||||
<h3>Team {slot}</h3>
|
||||
<span class="team-status">{status_text}</span>
|
||||
|
|
@ -6617,9 +6785,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|||
<div class="team-preview">
|
||||
{pet_previews}
|
||||
</div>
|
||||
<button class="edit-team-btn {'active' if slot == 1 else ''}" onclick="selectTeam({slot})">
|
||||
{'🟢 Currently Editing' if slot == 1 else f'📝 Edit Team {slot}'}
|
||||
</button>
|
||||
<div class="team-actions">
|
||||
<button class="edit-team-btn" onclick="selectTeam({slot})">
|
||||
📝 Edit Team {slot}
|
||||
</button>
|
||||
{'<span class="active-badge">🟢 Active Team</span>' if is_active else f'<button class="swap-team-btn" onclick="swapToTeam({slot})">🔄 Make Active</button>'}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
else:
|
||||
|
|
@ -6655,7 +6826,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|||
pet_previews = '<div class="mini-pet empty">Empty</div>' * 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'''
|
||||
<div class="team-card {active_class}" data-slot="{config['slot']}">
|
||||
|
|
@ -6666,9 +6838,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|||
<div class="team-preview">
|
||||
{pet_previews}
|
||||
</div>
|
||||
<button class="edit-team-btn {active_class}" onclick="selectTeam({config['slot']})">
|
||||
{'🟢 Currently Editing' if config['slot'] == 1 else f'📝 Edit {config["name"]}'}
|
||||
</button>
|
||||
<div class="team-actions">
|
||||
<button class="edit-team-btn" onclick="selectTeam({config['slot']})">
|
||||
📝 Edit {config["name"]}
|
||||
</button>
|
||||
{'<span class="active-badge">🟢 Active Team</span>' if is_active else f'<button class="swap-team-btn" onclick="swapToTeam({config["slot"]})">🔄 Make Active</button>'}
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
|
@ -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):
|
|||
<a href="/teambuilder/{self.path.split('/')[2]}/team/{team_identifier}" class="btn btn-primary">
|
||||
✏️ Edit Team {team_identifier}
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="alert('Team swap coming soon!')">
|
||||
<button class="btn btn-success" onclick="swapToTeam({team_identifier})">
|
||||
🔄 Make Active
|
||||
</button>
|
||||
'''
|
||||
|
|
@ -10660,6 +10877,89 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|||
margin-top: 8px;
|
||||
}}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const playerNickname = '{nickname}';
|
||||
|
||||
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(`button[onclick="swapToTeam(${{teamSlot}})"]`);
|
||||
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) {{
|
||||
showMessage(`${{result.message}}`, 'success');
|
||||
// Reload page to show updated active team
|
||||
setTimeout(() => {{
|
||||
window.location.reload();
|
||||
}}, 1500);
|
||||
}} 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';
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
'''
|
||||
|
||||
def generate_individual_team_editor_content(self, nickname, team_identifier, team_data, player_pets):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue