Enhance team builder with full drag-and-drop and detailed pet stats

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-14 17:11:54 +01:00
parent 9cf2231a03
commit 7d49730a5f

View file

@ -2032,10 +2032,67 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
self.wfile.write(html.encode())
def serve_teambuilder_interface(self, nickname, pets):
"""Serve the team builder interface - basic version for now"""
"""Serve the full interactive team builder interface"""
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
def make_pet_card(pet, is_active):
name = pet['nickname'] or pet['species_name']
status = "Active" if is_active else "Storage"
status_class = "active" if is_active else "storage"
type_str = pet['type1']
if pet['type2']:
type_str += f"/{pet['type2']}"
# 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"
return f"""
<div class="pet-card {status_class}" draggable="true" data-pet-id="{pet['id']}" data-active="{str(is_active).lower()}">
<div class="pet-header">
<h4 class="pet-name">{name}</h4>
<div class="status-badge">{status}</div>
</div>
<div class="pet-species">Level {pet['level']} {pet['species_name']}</div>
<div class="pet-type">{type_str}</div>
<div class="hp-section">
<div class="hp-label">HP: {pet['hp']}/{pet['max_hp']}</div>
<div class="hp-bar">
<div class="hp-fill" style="width: {hp_percent}%; background: {hp_color};"></div>
</div>
</div>
<div class="stats-grid">
<div class="stat">
<span class="stat-label">ATK</span>
<span class="stat-value">{pet['attack']}</span>
</div>
<div class="stat">
<span class="stat-label">DEF</span>
<span class="stat-value">{pet['defense']}</span>
</div>
<div class="stat">
<span class="stat-label">SPD</span>
<span class="stat-value">{pet['speed']}</span>
</div>
<div class="stat">
<span class="stat-label">EXP</span>
<span class="stat-value">{pet['experience']}</span>
</div>
</div>
<div class="pet-happiness">
<span class="happiness-emoji">{'😊' if pet['happiness'] > 70 else '😐' if pet['happiness'] > 40 else '😞'}</span>
<span>Happiness: {pet['happiness']}/100</span>
</div>
</div>"""
active_cards = ''.join(make_pet_card(pet, True) for pet in active_pets)
storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets)
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
@ -2043,44 +2100,648 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Builder - {nickname}</title>
<style>
body {{ font-family: Arial, sans-serif; background: #0f0f23; color: #cccccc; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 10px; text-align: center; margin-bottom: 20px; }}
.teams {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
.team-section {{ background: #1e1e3f; padding: 20px; border-radius: 10px; }}
.pet-list {{ margin-top: 15px; }}
.pet-item {{ background: #2a2a4a; padding: 10px; margin: 8px 0; border-radius: 5px; }}
.controls {{ text-align: center; margin-top: 20px; }}
.btn {{ padding: 12px 24px; margin: 0 10px; border: none; border-radius: 20px; cursor: pointer; text-decoration: none; display: inline-block; }}
.primary {{ background: #4CAF50; color: white; }}
.secondary {{ background: #666; color: white; }}
:root {{
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #999999;
--text-accent: #66ff66;
--active-color: #4CAF50;
--storage-color: #FF9800;
--drag-hover: #3a3a5a;
--shadow: 0 4px 15px rgba(0,0,0,0.3);
}}
body {{
font-family: 'Segoe UI', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
margin: 0;
padding: 20px;
min-height: 100vh;
}}
.header {{
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: var(--shadow);
}}
.header h1 {{ margin: 0; font-size: 2.5em; }}
.header p {{ margin: 10px 0 0 0; opacity: 0.9; }}
.team-sections {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}}
@media (max-width: 768px) {{
.team-sections {{ grid-template-columns: 1fr; }}
}}
.team-section {{
background: var(--bg-secondary);
border-radius: 15px;
padding: 20px;
min-height: 500px;
box-shadow: var(--shadow);
}}
.section-header {{
font-size: 1.3em;
font-weight: bold;
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
text-align: center;
}}
.active-header {{ background: var(--active-color); color: white; }}
.storage-header {{ background: var(--storage-color); color: white; }}
.pets-container {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
min-height: 200px;
}}
.pet-card {{
background: var(--bg-tertiary);
border-radius: 12px;
padding: 15px;
cursor: move;
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}}
.pet-card:hover {{
transform: translateY(-3px);
box-shadow: var(--shadow);
}}
.pet-card.active {{ border-color: var(--active-color); }}
.pet-card.storage {{ border-color: var(--storage-color); }}
.pet-card.dragging {{
opacity: 0.6;
transform: rotate(3deg) scale(0.95);
z-index: 1000;
}}
.pet-header {{
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}}
.pet-name {{
margin: 0;
font-size: 1.2em;
color: var(--text-accent);
font-weight: bold;
}}
.status-badge {{
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}}
.active .status-badge {{ background: var(--active-color); color: white; }}
.storage .status-badge {{ background: var(--storage-color); color: white; }}
.pet-species {{
color: var(--text-secondary);
margin-bottom: 5px;
font-weight: 500;
}}
.pet-type {{
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.85em;
display: inline-block;
margin-bottom: 10px;
}}
.hp-section {{
margin: 10px 0;
}}
.hp-label {{
font-size: 0.9em;
color: var(--text-secondary);
margin-bottom: 5px;
}}
.hp-bar {{
background: #333;
border-radius: 10px;
height: 8px;
overflow: hidden;
}}
.hp-fill {{
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
}}
.stats-grid {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 12px 0;
}}
.stat {{
display: flex;
justify-content: space-between;
background: rgba(255,255,255,0.05);
padding: 6px 10px;
border-radius: 6px;
}}
.stat-label {{
color: var(--text-secondary);
font-size: 0.85em;
}}
.stat-value {{
color: var(--text-accent);
font-weight: bold;
}}
.pet-happiness {{
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
font-size: 0.9em;
color: var(--text-secondary);
}}
.happiness-emoji {{
font-size: 1.2em;
}}
.drop-zone {{
min-height: 80px;
border: 2px dashed #666;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
margin-top: 15px;
transition: all 0.3s ease;
font-style: italic;
}}
.drop-zone.drag-over {{
border-color: var(--text-accent);
background: var(--drag-hover);
color: var(--text-accent);
border-style: solid;
}}
.drop-zone.has-pets {{
display: none;
}}
.controls {{
background: var(--bg-secondary);
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: var(--shadow);
}}
.save-btn {{
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
border: none;
padding: 15px 30px;
font-size: 1.1em;
border-radius: 25px;
cursor: pointer;
margin: 0 10px;
transition: all 0.3s ease;
font-weight: bold;
}}
.save-btn:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}}
.save-btn:disabled {{
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}}
.back-btn {{
background: linear-gradient(135deg, #666, #555);
color: white;
text-decoration: none;
padding: 15px 30px;
border-radius: 25px;
display: inline-block;
margin: 0 10px;
transition: all 0.3s ease;
font-weight: bold;
}}
.back-btn:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}}
.pin-section {{
background: var(--bg-secondary);
padding: 25px;
border-radius: 15px;
margin-top: 20px;
text-align: center;
display: none;
box-shadow: var(--shadow);
}}
.pin-input {{
background: var(--bg-tertiary);
border: 2px solid #666;
color: var(--text-primary);
padding: 15px;
font-size: 1.3em;
border-radius: 10px;
text-align: center;
width: 200px;
margin: 0 10px;
letter-spacing: 2px;
}}
.pin-input:focus {{
border-color: var(--text-accent);
outline: none;
box-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
}}
.verify-btn {{
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
border: none;
padding: 15px 30px;
font-size: 1.1em;
border-radius: 25px;
cursor: pointer;
margin: 0 10px;
transition: all 0.3s ease;
font-weight: bold;
}}
.verify-btn:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);
}}
.message {{
background: var(--bg-tertiary);
padding: 15px;
border-radius: 10px;
margin: 15px 0;
text-align: center;
}}
.success {{ border-left: 4px solid #4CAF50; }}
.error {{ border-left: 4px solid #f44336; }}
.info {{ border-left: 4px solid #2196F3; }}
.empty-state {{
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
font-style: italic;
}}
</style>
</head>
<body>
<div class="header">
<h1>🐾 Team Builder</h1>
<p><strong>{nickname}</strong> | Active: {len(active_pets)} | Storage: {len(inactive_pets)}</p>
<p>Drag pets between Active and Storage to build your perfect team</p>
<p><strong>{nickname}</strong> | Active: {len(active_pets)} pets | Storage: {len(inactive_pets)} pets</p>
</div>
<div class="teams">
<div class="team-sections">
<div class="team-section">
<h3> Active Team</h3>
<div class="pet-list">
{''.join(f'<div class="pet-item">{pet["nickname"] or pet["species_name"]} (Lv.{pet["level"]})</div>' for pet in active_pets) or '<div class="pet-item">No active pets</div>'}
<div class="section-header active-header"> Active Team</div>
<div class="pets-container" id="active-container">
{active_cards}
</div>
<div class="drop-zone {('has-pets' if active_pets else '')}" id="active-drop">
Drop pets here to add to your active team
</div>
</div>
<div class="team-section">
<h3>📦 Storage</h3>
<div class="pet-list">
{''.join(f'<div class="pet-item">{pet["nickname"] or pet["species_name"]} (Lv.{pet["level"]})</div>' for pet in inactive_pets) or '<div class="pet-item">No stored pets</div>'}
<div class="section-header storage-header">📦 Storage</div>
<div class="pets-container" id="storage-container">
{storage_cards}
</div>
<div class="drop-zone {('has-pets' if inactive_pets else '')}" id="storage-drop">
Drop pets here to store them
</div>
</div>
</div>
<div class="controls">
<p><em>Full drag-and-drop interface coming soon!</em></p>
<a href="/player/{nickname}" class="btn secondary"> Back to Profile</a>
<button class="save-btn" id="save-btn" onclick="saveTeam()">🔒 Save Team Changes</button>
<a href="/player/{nickname}" class="back-btn"> Back to Profile</a>
<div style="margin-top: 15px; color: var(--text-secondary); font-size: 0.9em;">
Changes are saved securely with PIN verification via IRC
</div>
</div>
<div class="pin-section" id="pin-section">
<h3>🔐 PIN Verification Required</h3>
<p>A 6-digit PIN has been sent to you via IRC private message.</p>
<p>Enter the PIN below to confirm your team changes:</p>
<input type="text" class="pin-input" id="pin-input" placeholder="000000" maxlength="6">
<button class="verify-btn" onclick="verifyPin()"> Verify & Apply Changes</button>
<div id="message-area"></div>
</div>
<script>
let originalTeam = {{}};
let currentTeam = {{}};
let draggedElement = null;
// Initialize team state
document.querySelectorAll('.pet-card').forEach(card => {{
const petId = card.dataset.petId;
const isActive = card.dataset.active === 'true';
originalTeam[petId] = isActive;
currentTeam[petId] = isActive;
}});
// Enhanced drag and drop functionality
document.querySelectorAll('.pet-card').forEach(card => {{
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
}});
document.querySelectorAll('.drop-zone, .pets-container').forEach(zone => {{
zone.addEventListener('dragover', handleDragOver);
zone.addEventListener('drop', handleDrop);
zone.addEventListener('dragenter', handleDragEnter);
zone.addEventListener('dragleave', handleDragLeave);
}});
function handleDragStart(e) {{
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.outerHTML);
// Add visual feedback
setTimeout(() => {{
this.style.opacity = '0.6';
}}, 0);
}}
function handleDragEnd(e) {{
this.classList.remove('dragging');
this.style.opacity = '';
// Clear all drag-over states
document.querySelectorAll('.drop-zone, .pets-container').forEach(zone => {{
zone.classList.remove('drag-over');
}});
}}
function handleDragOver(e) {{
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}}
function handleDragEnter(e) {{
e.preventDefault();
if (e.target.classList.contains('drop-zone') || e.target.classList.contains('pets-container')) {{
e.target.classList.add('drag-over');
}}
}}
function handleDragLeave(e) {{
if (e.target.classList.contains('drop-zone') || e.target.classList.contains('pets-container')) {{
// Only remove if we're actually leaving the element
if (!e.target.contains(e.relatedTarget)) {{
e.target.classList.remove('drag-over');
}}
}}
}}
function handleDrop(e) {{
e.preventDefault();
if (!draggedElement) return;
const petId = draggedElement.dataset.petId;
let newActiveStatus;
let targetContainer;
// Determine target based on drop zone or container
if (e.target.id === 'active-drop' || e.target.closest('#active-container')) {{
newActiveStatus = true;
targetContainer = document.getElementById('active-container');
moveToActive(draggedElement);
}} else if (e.target.id === 'storage-drop' || e.target.closest('#storage-container')) {{
newActiveStatus = false;
targetContainer = document.getElementById('storage-container');
moveToStorage(draggedElement);
}}
if (newActiveStatus !== undefined && newActiveStatus !== currentTeam[petId]) {{
currentTeam[petId] = newActiveStatus;
updateSaveButton();
updateDropZoneVisibility();
// Visual feedback
draggedElement.style.transform = 'scale(1.05)';
setTimeout(() => {{
draggedElement.style.transform = '';
}}, 200);
}}
// Clear all drag states
document.querySelectorAll('.drop-zone, .pets-container').forEach(zone => {{
zone.classList.remove('drag-over');
}});
}}
function moveToActive(card) {{
const container = document.getElementById('active-container');
card.classList.remove('storage');
card.classList.add('active');
card.dataset.active = 'true';
card.querySelector('.status-badge').textContent = 'Active';
container.appendChild(card);
}}
function moveToStorage(card) {{
const container = document.getElementById('storage-container');
card.classList.remove('active');
card.classList.add('storage');
card.dataset.active = 'false';
card.querySelector('.status-badge').textContent = 'Storage';
container.appendChild(card);
}}
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');
activeDrop.style.display = activeContainer.children.length > 0 ? 'none' : 'flex';
storageDrop.style.display = storageContainer.children.length > 0 ? 'none' : 'flex';
}}
function updateSaveButton() {{
const hasChanges = JSON.stringify(originalTeam) !== JSON.stringify(currentTeam);
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = !hasChanges;
if (hasChanges) {{
saveBtn.textContent = '🔒 Save Team Changes';
}} else {{
saveBtn.textContent = '✅ No Changes';
}}
}}
async function saveTeam() {{
const changes = {{}};
for (const petId in currentTeam) {{
if (currentTeam[petId] !== originalTeam[petId]) {{
changes[petId] = currentTeam[petId];
}}
}}
if (Object.keys(changes).length === 0) {{
showMessage('No changes to save!', 'info');
return;
}}
try {{
const response = await fetch('/teambuilder/{nickname}/save', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(changes)
}});
const result = await response.json();
if (result.success) {{
showMessage('PIN sent to IRC! Check your private messages.', 'success');
document.getElementById('pin-section').style.display = 'block';
document.getElementById('pin-section').scrollIntoView({{ behavior: 'smooth' }});
}} else {{
showMessage('Error: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
async function verifyPin() {{
const pin = document.getElementById('pin-input').value.trim();
if (pin.length !== 6 || !/^\\d{{6}}$/.test(pin)) {{
showMessage('Please enter a valid 6-digit PIN', 'error');
return;
}}
try {{
const response = await fetch('/teambuilder/{nickname}/verify', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ pin: pin }})
}});
const result = await response.json();
if (result.success) {{
showMessage('Team changes applied successfully! 🎉', 'success');
// Update original team state
originalTeam = {{...currentTeam}};
updateSaveButton();
document.getElementById('pin-section').style.display = 'none';
document.getElementById('pin-input').value = '';
// Visual celebration
document.querySelectorAll('.pet-card').forEach(card => {{
card.style.animation = 'bounce 0.6s ease-in-out';
}});
setTimeout(() => {{
document.querySelectorAll('.pet-card').forEach(card => {{
card.style.animation = '';
}});
}}, 600);
}} else {{
showMessage('Verification failed: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
function showMessage(text, type) {{
const messageArea = document.getElementById('message-area');
messageArea.innerHTML = `<div class="message ${{type}}">${{text}}</div>`;
// Auto-hide success messages
if (type === 'success') {{
setTimeout(() => {{
messageArea.innerHTML = '';
}}, 5000);
}}
}}
// Add keyboard support for PIN input
document.getElementById('pin-input').addEventListener('keypress', function(e) {{
if (e.key === 'Enter') {{
verifyPin();
}}
}});
// Initialize interface
updateSaveButton();
updateDropZoneVisibility();
// Add bounce animation
const style = document.createElement('style');
style.textContent = `
@keyframes bounce {{
0%, 20%, 60%, 100% {{ transform: translateY(0); }}
40% {{ transform: translateY(-10px); }}
80% {{ transform: translateY(-5px); }}
}}
`;
document.head.appendChild(style);
console.log('🐾 Team Builder initialized successfully!');
</script>
</body>
</html>"""