Petbot/webserver.py
megaproxy fca0423c84 Add comprehensive startup script validation and enhanced pet system
- Enhanced start_petbot.sh with extensive validation and error checking
- Added emoji support to pet species system with database migration
- Expanded pet species from 9 to 33 unique pets with balanced spawn rates
- Improved database integrity validation and orphaned pet detection
- Added comprehensive pre-startup testing and configuration validation
- Enhanced locations with diverse species spawning across all areas
- Added dual-type pets and rarity-based spawn distribution
- Improved startup information display with feature overview
- Added background monitoring and validation systems

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 00:17:54 +00:00

6353 lines
238 KiB
Python

#!/usr/bin/env python3
"""
PetBot Web Server
Provides web interface for bot data including help, player stats, and pet collections
"""
import os
import sys
import asyncio
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from threading import Thread
import time
# Add the project directory to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.rate_limiter import RateLimiter, CommandCategory
class PetBotRequestHandler(BaseHTTPRequestHandler):
"""HTTP request handler for PetBot web server"""
@property
def database(self):
"""Get database instance from server"""
return self.server.database
@property
def bot(self):
"""Get bot instance from server"""
return getattr(self.server, 'bot', None)
@property
def rate_limiter(self):
"""Get rate limiter from bot instance"""
bot = self.bot
return getattr(bot, 'rate_limiter', None) if bot else None
def get_client_ip(self):
"""Get client IP address for rate limiting"""
# Check for X-Forwarded-For header (in case of proxy)
forwarded_for = self.headers.get('X-Forwarded-For')
if forwarded_for:
return forwarded_for.split(',')[0].strip()
# Check for X-Real-IP header
real_ip = self.headers.get('X-Real-IP')
if real_ip:
return real_ip.strip()
# Fallback to client address
return self.client_address[0]
def check_rate_limit(self):
"""Check rate limit for web requests"""
if not self.rate_limiter:
return True, None
client_ip = self.get_client_ip()
# Use IP address as user identifier for web requests
user_identifier = f"web:{client_ip}"
# Run async rate limit check
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
allowed, message = loop.run_until_complete(
self.rate_limiter.check_rate_limit(user_identifier, CommandCategory.WEB)
)
return allowed, message
finally:
loop.close()
def send_rate_limit_error(self, message):
"""Send rate limit error response"""
self.send_response(429)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.send_header('Retry-After', '60')
self.end_headers()
content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Rate Limit Exceeded - PetBot</title>
<style>
body {{
font-family: Arial, sans-serif;
max-width: 600px;
margin: 100px auto;
text-align: center;
background: #0f0f23;
color: #cccccc;
}}
.error-container {{
background: #2a2a4a;
padding: 30px;
border-radius: 10px;
border: 1px solid #444466;
}}
h1 {{ color: #ff6b6b; }}
.message {{
margin: 20px 0;
font-size: 1.1em;
}}
.retry {{
color: #66ff66;
margin-top: 20px;
}}
</style>
</head>
<body>
<div class="error-container">
<h1>⛔ Rate Limit Exceeded</h1>
<div class="message">{message}</div>
<div class="retry">Please wait before making more requests.</div>
</div>
</body>
</html>
"""
self.wfile.write(content.encode())
def send_json_response(self, data, status_code=200):
"""Send a JSON response"""
import json
self.send_response(status_code)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def get_unified_css(self):
"""Return unified CSS theme for all pages"""
return """
:root {
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #aaaaaa;
--text-accent: #66ff66;
--accent-blue: #4dabf7;
--accent-purple: #845ec2;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-secondary: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
--border-color: #444466;
--hover-color: #3a3a5a;
--shadow-color: rgba(0, 0, 0, 0.3);
--success-color: #51cf66;
--warning-color: #ffd43b;
--error-color: #ff6b6b;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 0;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
.main-container {
padding: 20px;
min-height: calc(100vh - 80px);
}
/* Navigation Bar */
.navbar {
background: var(--gradient-primary);
padding: 15px 20px;
box-shadow: 0 2px 10px var(--shadow-color);
margin-bottom: 0;
}
.nav-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.nav-brand {
font-size: 1.5em;
font-weight: bold;
color: white;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
}
.nav-links {
display: flex;
gap: 20px;
align-items: center;
}
.nav-link {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 20px;
transition: all 0.3s ease;
font-weight: 500;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.nav-link.active {
background: rgba(255, 255, 255, 0.3);
}
/* Dropdown Navigation */
.nav-dropdown {
position: relative;
display: inline-block;
}
.dropdown-arrow {
font-size: 0.8em;
margin-left: 5px;
transition: transform 0.3s ease;
}
.nav-dropdown:hover .dropdown-arrow {
transform: rotate(180deg);
}
.dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: var(--bg-secondary);
min-width: 180px;
box-shadow: 0 8px 16px var(--shadow-color);
border-radius: 8px;
z-index: 1000;
border: 1px solid var(--border-color);
overflow: hidden;
margin-top: 5px;
}
.nav-dropdown:hover .dropdown-content {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dropdown-item {
color: var(--text-primary);
padding: 12px 16px;
text-decoration: none;
display: block;
transition: background-color 0.3s ease;
border-bottom: 1px solid var(--border-color);
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:hover {
background: var(--bg-tertiary);
color: var(--text-accent);
}
@media (max-width: 768px) {
.nav-content {
flex-direction: column;
gap: 15px;
}
.nav-links {
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.dropdown-content {
position: static;
display: none;
width: 100%;
box-shadow: none;
border: none;
border-radius: 0;
background: var(--bg-tertiary);
margin-top: 0;
}
.nav-dropdown:hover .dropdown-content {
display: block;
}
}
/* Header styling */
.header {
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 40px 20px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: 0 5px 20px var(--shadow-color);
}
.header h1 {
margin: 0 0 10px 0;
font-size: 2.5em;
font-weight: bold;
}
.header p {
margin: 0;
font-size: 1.1em;
opacity: 0.9;
}
/* Card styling */
.card {
background: var(--bg-secondary);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 15px var(--shadow-color);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px var(--shadow-color);
}
.card h2 {
margin-top: 0;
color: var(--text-accent);
border-bottom: 2px solid var(--text-accent);
padding-bottom: 10px;
}
.card h3 {
color: var(--accent-blue);
margin-top: 25px;
}
/* Grid layouts */
.grid {
display: grid;
gap: 20px;
margin-bottom: 30px;
}
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
/* Buttons */
.btn {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 25px;
text-decoration: none;
font-weight: 500;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
margin: 5px;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px var(--shadow-color);
}
/* Badges and tags */
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: 500;
margin: 2px;
}
.badge-primary { background: var(--accent-blue); color: white; }
.badge-secondary { background: var(--bg-tertiary); color: var(--text-primary); }
.badge-success { background: var(--success-color); color: white; }
.badge-warning { background: var(--warning-color); color: #333; }
.badge-error { background: var(--error-color); color: white; }
/* Tables */
.table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--bg-secondary);
border-radius: 10px;
overflow: hidden;
}
.table th, .table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.table th {
background: var(--bg-tertiary);
font-weight: bold;
color: var(--text-accent);
}
.table tr:hover {
background: var(--bg-tertiary);
}
/* Loading and status messages */
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.error-message {
background: var(--error-color);
color: white;
padding: 15px;
border-radius: 10px;
margin: 20px 0;
}
.success-message {
background: var(--success-color);
color: white;
padding: 15px;
border-radius: 10px;
margin: 20px 0;
}
/* Pet-specific styles for petdex */
.pets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
margin-top: 20px;
}
.pet-card {
background: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px var(--shadow-color);
border: 1px solid var(--border-color);
transition: transform 0.3s ease;
}
.pet-card:hover {
transform: translateY(-3px);
}
.pet-header {
background: var(--bg-tertiary);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.pet-header h3 {
margin: 0;
font-size: 1.3em;
}
.type-badge {
background: var(--gradient-primary);
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: 500;
}
.pet-stats {
padding: 15px 20px;
background: var(--bg-secondary);
}
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9em;
}
.total-stats {
text-align: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
font-weight: 600;
color: var(--text-accent);
}
.pet-info {
padding: 15px 20px;
background: var(--bg-tertiary);
font-size: 0.9em;
line-height: 1.5;
}
.rarity-section {
margin-bottom: 40px;
}
/* Responsive design */
@media (max-width: 768px) {
.main-container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.card {
padding: 15px;
}
.grid-2, .grid-3, .grid-4 {
grid-template-columns: 1fr;
}
}
"""
def get_navigation_bar(self, current_page=""):
"""Return unified navigation bar HTML with dropdown menus"""
# Define navigation structure with dropdowns
nav_structure = [
("", "🏠 Home", []),
("players", "👥 Players", [
("leaderboard", "🏆 Leaderboard"),
("players", "📊 Statistics")
]),
("locations", "🗺️ Locations", [
("locations", "🌤️ Weather"),
("locations", "🎯 Spawns"),
("locations", "🏛️ Gyms")
]),
("petdex", "📚 Petdex", [
("petdex", "🔷 by Type"),
("petdex", "⭐ by Rarity"),
("petdex", "🔍 Search")
]),
("help", "📖 Help", [
("help", "⚡ Commands"),
("help", "📖 Web Guide"),
("help", "❓ FAQ")
])
]
nav_links = ""
for page_path, page_name, subpages in nav_structure:
active_class = " active" if current_page == page_path else ""
href = f"/{page_path}" if page_path else "/"
if subpages:
# Create dropdown menu
dropdown_items = ""
for sub_path, sub_name in subpages:
sub_href = f"/{sub_path}" if sub_path else "/"
dropdown_items += f'<a href="{sub_href}" class="dropdown-item">{sub_name}</a>'
nav_links += f'''
<div class="nav-dropdown">
<a href="{href}" class="nav-link{active_class}">{page_name} <span class="dropdown-arrow">▼</span></a>
<div class="dropdown-content">
{dropdown_items}
</div>
</div>'''
else:
# Regular nav link
nav_links += f'<a href="{href}" class="nav-link{active_class}">{page_name}</a>'
return f"""
<nav class="navbar">
<div class="nav-content">
<a href="/" class="nav-brand">🐾 PetBot</a>
<div class="nav-links">
{nav_links}
</div>
</div>
</nav>
"""
def get_page_template(self, title, content, current_page=""):
"""Return complete page HTML with unified theme"""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} - PetBot</title>
<style>{self.get_unified_css()}</style>
</head>
<body>
{self.get_navigation_bar(current_page)}
<div class="main-container">
{content}
</div>
</body>
</html>"""
def do_GET(self):
"""Handle GET requests with rate limiting"""
# Check rate limit first
allowed, rate_limit_message = self.check_rate_limit()
if not allowed:
self.send_rate_limit_error(rate_limit_message)
return
parsed_path = urlparse(self.path)
path = parsed_path.path
# Route handling
if path == '/':
self.serve_index()
elif path == '/help':
self.serve_help()
elif path == '/players':
self.serve_players()
elif path.startswith('/player/'):
nickname = path[8:] # Remove '/player/' prefix
self.serve_player_profile(nickname)
elif path == '/leaderboard':
self.serve_leaderboard()
elif path == '/locations':
self.serve_locations()
elif path == '/petdex':
self.serve_petdex()
elif path.startswith('/teambuilder/'):
nickname = path[13:] # Remove '/teambuilder/' prefix
self.serve_teambuilder(nickname)
else:
self.send_error(404, "Page not found")
def do_POST(self):
"""Handle POST requests with rate limiting"""
# Check rate limit first (POST requests have stricter limits)
allowed, rate_limit_message = self.check_rate_limit()
if not allowed:
self.send_json_response({"success": False, "error": rate_limit_message}, 429)
return
parsed_path = urlparse(self.path)
path = parsed_path.path
if path.startswith('/teambuilder/') and path.endswith('/save'):
nickname = path[13:-5] # Remove '/teambuilder/' prefix and '/save' suffix
self.handle_team_save(nickname)
elif path.startswith('/teambuilder/') and path.endswith('/verify'):
nickname = path[13:-7] # Remove '/teambuilder/' prefix and '/verify' suffix
self.handle_team_verify(nickname)
elif path.startswith('/teambuilder/') and '/config/save/' in path:
# Handle team configuration save: /teambuilder/{nickname}/config/save/{slot}
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
slot = parts[5]
self.handle_team_config_save(nickname, slot)
else:
self.send_error(400, "Invalid configuration save path")
elif path.startswith('/teambuilder/') and '/config/load/' in path:
# Handle team configuration load: /teambuilder/{nickname}/config/load/{slot}
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
slot = parts[5]
self.handle_team_config_load(nickname, slot)
else:
self.send_error(400, "Invalid configuration load path")
elif path.startswith('/teambuilder/') and '/config/rename/' in path:
# Handle team configuration rename: /teambuilder/{nickname}/config/rename/{slot}
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
slot = parts[5]
self.handle_team_config_rename(nickname, slot)
else:
self.send_error(400, "Invalid configuration rename path")
else:
self.send_error(404, "Page not found")
def serve_index(self):
"""Serve the main index page"""
content = """
<div class="header">
<h1>🐾 PetBot Game Hub</h1>
<p>Welcome to the PetBot web interface!</p>
<p><em>Connect to irc.libera.chat #petz to play</em></p>
</div>
<div class="grid grid-3">
<div class="card">
<h3>📚 Command Help</h3>
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
<a href="/help" class="btn btn-primary">View Help</a>
</div>
<div class="card">
<h3>👥 Player List</h3>
<p>View all registered players and their basic stats</p>
<a href="/players" class="btn btn-primary">Browse Players</a>
</div>
<div class="card">
<h3>🏆 Leaderboard</h3>
<p>Top players by level, pets caught, and achievements earned</p>
<a href="/leaderboard" class="btn btn-primary">View Rankings</a>
</div>
<div class="card">
<h3>🗺️ Locations</h3>
<p>Explore all game locations and see what pets can be found where</p>
<a href="/locations" class="btn btn-primary">Explore World</a>
</div>
<div class="card">
<h3>📖 Petdex</h3>
<p>Complete encyclopedia of all available pets with stats, types, and evolution info</p>
<a href="/petdex" class="btn btn-primary">Browse Petdex</a>
</div>
<div class="card">
<h3>🤖 Bot Status</h3>
<p><span class="badge badge-success">Online</span> and ready for commands!</p>
<p>Use <code>!help</code> in #petz for quick command reference</p>
</div>
</div>
"""
html = self.get_page_template("PetBot Game Hub", content, "")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_help(self):
"""Serve the help page using unified template"""
content = """
<div class="header">
<h1>📚 PetBot Commands</h1>
<p>Complete guide to Pokemon-style pet collecting in IRC</p>
</div>
<div class="section">
<div class="section-header">🚀 Getting Started</div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!start</div>
<div class="command-desc">Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.</div>
<div class="command-example">Example: !start</div>
</div>
<div class="command">
<div class="command-name">!help</div>
<div class="command-desc">Get a link to this comprehensive command reference page.</div>
<div class="command-example">Example: !help</div>
</div>
<div class="command">
<div class="command-name">!stats</div>
<div class="command-desc">View your basic trainer information including level, experience, and money.</div>
<div class="command-example">Example: !stats</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🌍 Exploration & Travel</div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!explore</div>
<div class="command-desc">Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!</div>
<div class="command-example">Example: !explore</div>
</div>
<div class="command">
<div class="command-name">!travel &lt;location&gt;</div>
<div class="command-desc">Move to a different location. Each area has unique pets and gyms. Some locations require achievements to unlock.</div>
<div class="command-example">Example: !travel whispering woods</div>
</div>
<div class="command">
<div class="command-name">!where / !location</div>
<div class="command-desc">See which location you're currently in and get information about the area.</div>
<div class="command-example">Example: !where</div>
</div>
</div>
<div class="info-box">
<h4>🗺️ Available Locations</h4>
<ul>
<li><strong>Starter Town</strong> - Peaceful starting area (Fire/Water/Grass pets)</li>
<li><strong>Whispering Woods</strong> - Ancient forest (Grass pets + new species: Vinewrap, Bloomtail)</li>
<li><strong>Electric Canyon</strong> - Charged valley (Electric/Rock pets)</li>
<li><strong>Crystal Caves</strong> - Underground caverns (Rock/Crystal pets)</li>
<li><strong>Frozen Tundra</strong> - Icy wasteland (Ice/Water pets)</li>
<li><strong>Dragon's Peak</strong> - Ultimate challenge (Fire/Rock/Ice pets)</li>
</ul>
</div>
<div class="info-box">
<h4>🌤️ Weather Effects</h4>
<ul>
<li><strong>Sunny</strong> - 1.5x Fire/Grass spawns (1-2 hours)</li>
<li><strong>Rainy</strong> - 2.0x Water spawns (45-90 minutes)</li>
<li><strong>Thunderstorm</strong> - 2.0x Electric spawns (30-60 minutes)</li>
<li><strong>Blizzard</strong> - 1.7x Ice/Water spawns (1-2 hours)</li>
<li><strong>Earthquake</strong> - 1.8x Rock spawns (30-90 minutes)</li>
<li><strong>Calm</strong> - Normal spawns (1.5-3 hours)</li>
</ul>
</div>
</div>
</div>
<div class="section">
<div class="section-header">⚔️ Battle System</div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!catch / !capture</div>
<div class="command-desc">Attempt to catch a wild pet that appeared during exploration. Success depends on the pet's level and rarity.</div>
<div class="command-example">Example: !catch</div>
</div>
<div class="command">
<div class="command-name">!battle</div>
<div class="command-desc">Start a turn-based battle with a wild pet. Defeat it to gain experience and money for your active pet.</div>
<div class="command-example">Example: !battle</div>
</div>
<div class="command">
<div class="command-name">!attack &lt;move&gt;</div>
<div class="command-desc">Use a specific move during battle. Each move has different power, type, and effects.</div>
<div class="command-example">Example: !attack flamethrower</div>
</div>
<div class="command">
<div class="command-name">!moves</div>
<div class="command-desc">View all available moves for your active pet, including their types and power levels.</div>
<div class="command-example">Example: !moves</div>
</div>
<div class="command">
<div class="command-name">!flee</div>
<div class="command-desc">Attempt to escape from the current battle. Not always successful!</div>
<div class="command-example">Example: !flee</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🏛️ Gym Battles <span class="badge">NEW!</span></div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!gym</div>
<div class="command-desc">List all gyms in your current location with your progress. Shows victories and next difficulty level.</div>
<div class="command-example">Example: !gym</div>
</div>
<div class="command">
<div class="command-name">!gym list</div>
<div class="command-desc">Show all gyms across all locations with your badge collection progress.</div>
<div class="command-example">Example: !gym list</div>
</div>
<div class="command">
<div class="command-name">!gym challenge "&lt;name&gt;"</div>
<div class="command-desc">Challenge a gym leader! You must be in the same location as the gym. Difficulty increases with each victory.</div>
<div class="command-example">Example: !gym challenge "Forest Guardian"</div>
</div>
<div class="command">
<div class="command-name">!gym info "&lt;name&gt;"</div>
<div class="command-desc">Get detailed information about a gym including leader, theme, team, and badge details.</div>
<div class="command-example">Example: !gym info "Storm Master"</div>
</div>
</div>
<div class="tip">
💡 <strong>Gym Strategy:</strong> Each gym specializes in a specific type. Bring pets with type advantages! The more you beat a gym, the harder it gets, but the better the rewards!
</div>
<div class="info-box">
<h4>🏆 Gym Leaders & Badges</h4>
<div class="gym-list">
<div class="gym-card">
<strong>🍃 Forest Guardian</strong><br>
Location: Starter Town<br>
Leader: Trainer Verde<br>
Theme: Grass-type
</div>
<div class="gym-card">
<strong>🌳 Nature's Haven</strong><br>
Location: Whispering Woods<br>
Leader: Elder Sage<br>
Theme: Grass-type
</div>
<div class="gym-card">
<strong>⚡ Storm Master</strong><br>
Location: Electric Canyon<br>
Leader: Captain Volt<br>
Theme: Electric-type
</div>
<div class="gym-card">
<strong>💎 Stone Crusher</strong><br>
Location: Crystal Caves<br>
Leader: Miner Magnus<br>
Theme: Rock-type
</div>
<div class="gym-card">
<strong>❄️ Ice Breaker</strong><br>
Location: Frozen Tundra<br>
Leader: Arctic Queen<br>
Theme: Ice/Water-type
</div>
<div class="gym-card">
<strong>🐉 Dragon Slayer</strong><br>
Location: Dragon's Peak<br>
Leader: Champion Drake<br>
Theme: Fire-type
</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🐾 Pet Management</div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!team</div>
<div class="command-desc">Access your team builder web interface for drag-and-drop team management with PIN verification.</div>
<div class="command-example">Example: !team</div>
</div>
<div class="command">
<div class="command-name">!pets</div>
<div class="command-desc">View your complete pet collection with detailed stats and information via web interface.</div>
<div class="command-example">Example: !pets</div>
</div>
<div class="command">
<div class="command-name">!activate &lt;pet&gt;</div>
<div class="command-desc">Add a pet to your active battle team. You can have multiple active pets for different situations.</div>
<div class="command-example">Example: !activate flamey</div>
</div>
<div class="command">
<div class="command-name">!deactivate &lt;pet&gt;</div>
<div class="command-desc">Remove a pet from your active team and put it in storage.</div>
<div class="command-example">Example: !deactivate aqua</div>
</div>
<div class="command">
<div class="command-name">!nickname &lt;pet&gt; &lt;new_name&gt;</div>
<div class="command-desc">Give a custom nickname to one of your pets. Nicknames must be 20 characters or less.</div>
<div class="command-example">Example: !nickname flamey FireStorm</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🎒 Inventory System <span class="badge">NEW!</span></div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!inventory / !inv / !items</div>
<div class="command-desc">View all items in your inventory organized by category. Shows quantities and item descriptions.</div>
<div class="command-example">Example: !inventory</div>
</div>
<div class="command">
<div class="command-name">!use &lt;item name&gt;</div>
<div class="command-desc">Use a consumable item from your inventory. Items can heal pets, boost stats, or provide other benefits.</div>
<div class="command-example">Example: !use Small Potion</div>
</div>
</div>
<div class="info-box">
<h4>🎯 Item Categories & Rarities</h4>
<ul>
<li><strong>○ Common (15%)</strong> - Small Potions, basic healing items</li>
<li><strong>◇ Uncommon (8-12%)</strong> - Large Potions, battle boosters, special berries</li>
<li><strong>◆ Rare (3-6%)</strong> - Super Potions, speed elixirs, location treasures</li>
<li><strong>★ Epic (2-3%)</strong> - Evolution stones, rare crystals, ancient artifacts</li>
<li><strong>✦ Legendary (1%)</strong> - Lucky charms, ancient fossils, ultimate items</li>
</ul>
</div>
<div class="tip">
💡 <strong>Item Discovery:</strong> Find items while exploring! Each location has unique treasures including rare Coin Pouches with 1-3 coins. Items stack in your inventory and can be used anytime.
</div>
</div>
<div class="section">
<div class="section-header">🏆 Achievements & Progress</div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!achievements</div>
<div class="command-desc">View your achievement progress and see which new locations you've unlocked.</div>
<div class="command-example">Example: !achievements</div>
</div>
</div>
<div class="info-box">
<h4>🎯 Location Unlock Requirements</h4>
<ul>
<li><strong>Pet Collector</strong> (5 pets) → Unlocks Whispering Woods</li>
<li><strong>Spark Collector</strong> (2 Electric species) → Unlocks Electric Canyon</li>
<li><strong>Rock Hound</strong> (3 Rock species) → Unlocks Crystal Caves</li>
<li><strong>Ice Breaker</strong> (5 Water/Ice species) → Unlocks Frozen Tundra</li>
<li><strong>Dragon Tamer</strong> (15 pets + 3 Fire species) → Unlocks Dragon's Peak</li>
</ul>
</div>
</div>
<div class="section">
<div class="section-header">⚡ Admin Commands <span class="badge">ADMIN ONLY</span></div>
<div class="section-content">
<div class="command-grid">
<div class="command">
<div class="command-name">!reload</div>
<div class="command-desc">Reload all bot modules without restarting. Useful for applying code changes.</div>
<div class="command-example">Example: !reload</div>
</div>
<div class="command">
<div class="command-name">!weather [location|all]</div>
<div class="command-desc">Check current weather conditions in specific location or all locations.</div>
<div class="command-example">Example: !weather Electric Canyon</div>
</div>
<div class="command">
<div class="command-name">!setweather &lt;weather&gt; [location] [duration]</div>
<div class="command-desc">Force change weather. Types: sunny, rainy, storm, blizzard, earthquake, calm</div>
<div class="command-example">Example: !setweather storm all 60</div>
</div>
<div class="command">
<div class="command-name">!backup create [description]</div>
<div class="command-desc">Create manual database backup with optional description.</div>
<div class="command-example">Example: !backup create "before update"</div>
</div>
<div class="command">
<div class="command-name">!rate_stats [user]</div>
<div class="command-desc">View rate limiting statistics for all users or specific user.</div>
<div class="command-example">Example: !rate_stats username</div>
</div>
<div class="command">
<div class="command-name">!status / !uptime</div>
<div class="command-desc">Check bot connection status, uptime, and system health information.</div>
<div class="command-example">Example: !status</div>
</div>
<div class="command">
<div class="command-name">!backups / !restore</div>
<div class="command-desc">List available backups or restore from backup. Use with caution!</div>
<div class="command-example">Example: !backups</div>
</div>
</div>
<div class="tip">
🔒 <strong>Admin Access:</strong> These commands require administrator privileges and are restricted to authorized users only.
</div>
</div>
</div>
<div class="section">
<div class="section-header">🌐 Web Interface</div>
<div class="section-content">
<div class="tip">
Access detailed information through the web dashboard at <strong>http://petz.rdx4.com/</strong>
<ul style="margin-top: 10px;">
<li><strong>Player Profiles</strong> - Complete stats, pet collections, and inventories with usage commands</li>
<li><strong>Team Builder</strong> - Drag-and-drop team management with PIN verification</li>
<li><strong>Enhanced Leaderboards</strong> - 8 categories: levels, experience, wealth, achievements, gym badges, rare pets</li>
<li><strong>Locations Guide</strong> - All areas with spawn information and current weather</li>
<li><strong>Gym Badges</strong> - Display your earned badges and battle progress</li>
<li><strong>Inventory Management</strong> - Visual item display with command instructions</li>
</ul>
</div>
</div>
</div>
<div class="footer">
<p><strong>🎮 PetBot v0.2.0</strong> - Pokemon-style pet collecting for IRC</p>
<p>Catch pets • Battle gyms • Collect items • Earn badges • Explore locations</p>
<p style="margin-top: 15px; opacity: 0.7;">
Need help? Ask in the channel or visit the web dashboard for detailed information!
</p>
</div>
"""
# Add command-specific CSS to the unified styles
additional_css = """
.section {
background: var(--bg-secondary);
border-radius: 15px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
overflow: hidden;
}
.section-header {
background: var(--gradient-primary);
color: white;
padding: 20px 25px;
font-size: 1.3em;
font-weight: 700;
}
.section-content {
padding: 25px;
}
.command-grid {
display: grid;
gap: 20px;
}
.command {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
background: var(--bg-tertiary);
}
.command:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15);
border-color: var(--text-accent);
}
.command-name {
background: var(--bg-primary);
padding: 15px 20px;
font-family: 'Fira Code', 'Courier New', monospace;
font-weight: bold;
color: var(--text-accent);
border-bottom: 1px solid var(--border-color);
font-size: 1.2em;
text-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
}
.command-desc {
padding: 20px;
line-height: 1.7;
color: var(--text-primary);
}
.command-example {
background: var(--bg-primary);
padding: 12px 20px;
font-family: 'Fira Code', 'Courier New', monospace;
color: var(--text-secondary);
border-top: 1px solid var(--border-color);
font-size: 0.95em;
}
.info-box {
background: var(--bg-tertiary);
padding: 20px;
border-radius: 12px;
margin: 20px 0;
border: 1px solid var(--border-color);
}
.info-box h4 {
margin: 0 0 15px 0;
color: var(--text-accent);
font-size: 1.1em;
font-weight: 600;
}
.info-box ul {
margin: 0;
padding-left: 25px;
}
.info-box li {
margin: 8px 0;
color: var(--text-primary);
}
.info-box strong {
color: var(--text-accent);
}
.footer {
text-align: center;
margin-top: 50px;
padding: 30px;
background: var(--bg-secondary);
border-radius: 15px;
color: var(--text-secondary);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.tip {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px;
margin: 20px 0;
font-weight: 500;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.gym-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
margin: 15px 0;
}
.gym-card {
background: var(--bg-primary);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.gym-card strong {
color: var(--text-accent);
}
"""
# Get the unified template with additional CSS
html_content = self.get_page_template("Command Help", content, "help")
# Insert additional CSS before closing </style> tag
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_players(self):
"""Serve the players page with real data"""
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Players", "Database not available")
return
# Fetch players data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
players_data = loop.run_until_complete(self.fetch_players_data(database))
loop.close()
self.serve_players_data(players_data)
except Exception as e:
print(f"Error fetching players data: {e}")
self.serve_error_page("Players", f"Error loading players: {str(e)}")
async def fetch_players_data(self, database):
"""Fetch all players data from database"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get all players with basic stats
cursor = await db.execute("""
SELECT p.nickname, p.level, p.experience, p.money, p.created_at,
l.name as location_name,
(SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count,
(SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets,
(SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count
FROM players p
LEFT JOIN locations l ON p.current_location_id = l.id
ORDER BY p.level DESC, p.experience DESC
""")
rows = await cursor.fetchall()
# Convert SQLite rows to dictionaries properly
players = []
for row in rows:
player_dict = {
'nickname': row[0],
'level': row[1],
'experience': row[2],
'money': row[3],
'created_at': row[4],
'location_name': row[5],
'pet_count': row[6],
'active_pets': row[7],
'achievement_count': row[8]
}
players.append(player_dict)
return players
except Exception as e:
print(f"Database error fetching players: {e}")
return []
def serve_players_data(self, players_data):
"""Serve players page with real data"""
# Calculate statistics
total_players = len(players_data)
total_pets = sum(p['pet_count'] for p in players_data) if players_data else 0
total_achievements = sum(p['achievement_count'] for p in players_data) if players_data else 0
highest_level = max((p['level'] for p in players_data), default=0) if players_data else 0
# Build statistics cards
stats_content = f"""
<div class="grid grid-4">
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">📊 Total Players</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_players}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">🐾 Total Pets</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_pets}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">🏆 Achievements</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_achievements}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">⭐ Highest Level</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{highest_level}</div>
</div>
</div>
"""
# Build players table HTML
if players_data:
players_html = ""
for i, player in enumerate(players_data, 1):
rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.")
players_html += f"""
<tr onclick="window.location.href='/player/{player['nickname']}'" style="cursor: pointer;">
<td><strong>{rank_emoji}</strong></td>
<td><a href="/player/{player['nickname']}" style="color: var(--text-accent); text-decoration: none;"><strong>{player['nickname']}</strong></a></td>
<td>{player['level']}</td>
<td>{player['experience']}</td>
<td>${player['money']}</td>
<td>{player['pet_count']}</td>
<td>{player['active_pets']}</td>
<td>{player['achievement_count']}</td>
<td>{player.get('location_name', 'Unknown')}</td>
</tr>"""
else:
players_html = """
<tr>
<td colspan="9" style="text-align: center; padding: 40px;">
No players found. Be the first to use !start in #petz!
</td>
</tr>"""
# Build table content
table_content = f"""
<div class="card">
<h2>🏆 Player Rankings</h2>
<table class="table">
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Level</th>
<th>Experience</th>
<th>Money</th>
<th>Pets</th>
<th>Active</th>
<th>Achievements</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{players_html}
</tbody>
</table>
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary);">
<p>💡 Click on any player name to view their detailed profile</p>
</div>
</div>
"""
# Combine all content
content = f"""
<div class="header">
<h1>👥 Registered Players</h1>
<p>All trainers on their pet collection journey</p>
</div>
{stats_content}
{table_content}
"""
html = self.get_page_template("Players", content, "players")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_error_page(self, page_name, error_msg):
"""Serve a generic error page using unified template"""
content = f"""
<div class="header">
<h1>⚠️ Error Loading {page_name}</h1>
</div>
<div class="error-message">
<h2>Unable to load page</h2>
<p>{error_msg}</p>
<p>Please try again later or contact an administrator.</p>
</div>
"""
# Add error-specific CSS
additional_css = """
.main-container {
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.error-message {
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid var(--error-color);
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.error-message h2 {
color: var(--error-color);
margin-top: 0;
}
"""
html_content = self.get_page_template("Error", content, "")
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_leaderboard(self):
"""Serve the enhanced leaderboard page with multiple categories"""
import asyncio
# Check rate limit first
allowed, rate_limit_message = self.check_rate_limit()
if not allowed:
self.send_rate_limit_error(rate_limit_message)
return
# Get database instance
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Leaderboard", "Database not available")
return
try:
# Run async database operations in event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Get all leaderboard data
leaderboard_data = loop.run_until_complete(self.get_leaderboard_data(database))
# Generate HTML content
content = self.generate_leaderboard_content(leaderboard_data)
html_content = self.get_page_template("Leaderboard - PetBot", content, "leaderboard")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
except Exception as e:
print(f"Error generating leaderboard: {e}")
self.serve_error_page("Leaderboard", f"Error loading leaderboard data: {str(e)}")
finally:
loop.close()
async def get_leaderboard_data(self, database):
"""Get all leaderboard data for different categories"""
leaderboard_data = {}
# 1. Top Players by Level
leaderboard_data['levels'] = await self.get_level_leaderboard(database)
# 2. Top Players by Experience
leaderboard_data['experience'] = await self.get_experience_leaderboard(database)
# 3. Richest Players
leaderboard_data['money'] = await self.get_money_leaderboard(database)
# 4. Most Pets Collected
leaderboard_data['pet_count'] = await self.get_pet_count_leaderboard(database)
# 5. Most Achievements
leaderboard_data['achievements'] = await self.get_achievement_leaderboard(database)
# 6. Gym Champions (most gym badges)
leaderboard_data['gym_badges'] = await self.get_gym_badge_leaderboard(database)
# 7. Highest Level Pet
leaderboard_data['highest_pet'] = await self.get_highest_pet_leaderboard(database)
# 8. Most Rare Pets
leaderboard_data['rare_pets'] = await self.get_rare_pet_leaderboard(database)
return leaderboard_data
async def get_level_leaderboard(self, database):
"""Get top players by level"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT nickname, level, experience
FROM players
ORDER BY level DESC, experience DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "level": p[1], "experience": p[2]} for p in players]
async def get_experience_leaderboard(self, database):
"""Get top players by total experience"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT nickname, level, experience
FROM players
ORDER BY experience DESC, level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "level": p[1], "experience": p[2]} for p in players]
async def get_money_leaderboard(self, database):
"""Get richest players"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT nickname, money, level
FROM players
ORDER BY money DESC, level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "money": p[1], "level": p[2]} for p in players]
async def get_pet_count_leaderboard(self, database):
"""Get players with most pets"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT p.nickname, COUNT(pets.id) as pet_count, p.level
FROM players p
LEFT JOIN pets ON p.id = pets.player_id
GROUP BY p.id, p.nickname, p.level
ORDER BY pet_count DESC, p.level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "pet_count": p[1], "level": p[2]} for p in players]
async def get_achievement_leaderboard(self, database):
"""Get players with most achievements"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT p.nickname, COUNT(pa.achievement_id) as achievement_count, p.level
FROM players p
LEFT JOIN player_achievements pa ON p.id = pa.player_id
GROUP BY p.id, p.nickname, p.level
ORDER BY achievement_count DESC, p.level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "achievement_count": p[1], "level": p[2]} for p in players]
async def get_gym_badge_leaderboard(self, database):
"""Get players with most gym victories (substitute for badges)"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Check if player_gym_battles table exists and has data
cursor = await db.execute("""
SELECT p.nickname,
COALESCE(COUNT(DISTINCT CASE WHEN pgb.victories > 0 THEN pgb.gym_id END), 0) as gym_victories,
p.level
FROM players p
LEFT JOIN player_gym_battles pgb ON p.id = pgb.player_id
GROUP BY p.id, p.nickname, p.level
ORDER BY gym_victories DESC, p.level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "badge_count": p[1], "level": p[2]} for p in players]
async def get_highest_pet_leaderboard(self, database):
"""Get players with highest level pets"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT p.nickname, MAX(pets.level) as highest_pet_level,
ps.name as pet_species, p.level as player_level
FROM players p
JOIN pets ON p.id = pets.player_id
JOIN pet_species ps ON pets.species_id = ps.id
GROUP BY p.id, p.nickname, p.level
ORDER BY highest_pet_level DESC, p.level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "highest_pet_level": p[1], "pet_species": p[2], "player_level": p[3]} for p in players]
async def get_rare_pet_leaderboard(self, database):
"""Get players with most rare pets (epic/legendary)"""
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
cursor = await db.execute("""
SELECT p.nickname, COUNT(pets.id) as rare_pet_count, p.level
FROM players p
JOIN pets ON p.id = pets.player_id
JOIN pet_species ps ON pets.species_id = ps.id
WHERE ps.rarity >= 4
GROUP BY p.id, p.nickname, p.level
ORDER BY rare_pet_count DESC, p.level DESC
LIMIT 10
""")
players = await cursor.fetchall()
return [{"nickname": p[0], "rare_pet_count": p[1], "level": p[2]} for p in players]
def generate_leaderboard_content(self, leaderboard_data):
"""Generate HTML content for the enhanced leaderboard"""
content = """
<div class="header">
<h1>🏆 PetBot Leaderboards</h1>
<p>Compete with trainers across all categories!</p>
</div>
<div class="leaderboard-nav">
<button class="category-btn active" onclick="showCategory('levels')">🎯 Levels</button>
<button class="category-btn" onclick="showCategory('experience')">⭐ Experience</button>
<button class="category-btn" onclick="showCategory('money')">💰 Wealth</button>
<button class="category-btn" onclick="showCategory('pet_count')">🐾 Pet Count</button>
<button class="category-btn" onclick="showCategory('achievements')">🏅 Achievements</button>
<button class="category-btn" onclick="showCategory('gym_badges')">🏛️ Gym Badges</button>
<button class="category-btn" onclick="showCategory('highest_pet')">🌟 Highest Pet</button>
<button class="category-btn" onclick="showCategory('rare_pets')">💎 Rare Pets</button>
</div>
"""
# Generate each leaderboard category
content += self.generate_leaderboard_category("levels", "🎯 Level Leaders", leaderboard_data['levels'],
["Rank", "Player", "Level", "Experience"],
lambda p, i: [i+1, p['nickname'], p['level'], f"{p['experience']:,}"], True)
content += self.generate_leaderboard_category("experience", "⭐ Experience Champions", leaderboard_data['experience'],
["Rank", "Player", "Experience", "Level"],
lambda p, i: [i+1, p['nickname'], f"{p['experience']:,}", p['level']])
content += self.generate_leaderboard_category("money", "💰 Wealthiest Trainers", leaderboard_data['money'],
["Rank", "Player", "Money", "Level"],
lambda p, i: [i+1, p['nickname'], f"${p['money']:,}", p['level']])
content += self.generate_leaderboard_category("pet_count", "🐾 Pet Collectors", leaderboard_data['pet_count'],
["Rank", "Player", "Pet Count", "Level"],
lambda p, i: [i+1, p['nickname'], p['pet_count'], p['level']])
content += self.generate_leaderboard_category("achievements", "🏅 Achievement Hunters", leaderboard_data['achievements'],
["Rank", "Player", "Achievements", "Level"],
lambda p, i: [i+1, p['nickname'], p['achievement_count'], p['level']])
content += self.generate_leaderboard_category("gym_badges", "🏛️ Gym Champions", leaderboard_data['gym_badges'],
["Rank", "Player", "Gym Badges", "Level"],
lambda p, i: [i+1, p['nickname'], p['badge_count'], p['level']])
content += self.generate_leaderboard_category("highest_pet", "🌟 Elite Pet Trainers", leaderboard_data['highest_pet'],
["Rank", "Player", "Highest Pet", "Species", "Player Level"],
lambda p, i: [i+1, p['nickname'], f"Lvl {p['highest_pet_level']}", p['pet_species'], p['player_level']])
content += self.generate_leaderboard_category("rare_pets", "💎 Rare Pet Masters", leaderboard_data['rare_pets'],
["Rank", "Player", "Rare Pets", "Level"],
lambda p, i: [i+1, p['nickname'], p['rare_pet_count'], p['level']])
# Add JavaScript for category switching
content += """
<script>
function showCategory(category) {
// Hide all categories
const categories = document.querySelectorAll('.leaderboard-category');
categories.forEach(cat => cat.style.display = 'none');
// Show selected category
document.getElementById(category).style.display = 'block';
// Update button states
const buttons = document.querySelectorAll('.category-btn');
buttons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
// Show first category by default
document.addEventListener('DOMContentLoaded', function() {
showCategory('levels');
});
</script>
<style>
.leaderboard-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0;
justify-content: center;
}
.category-btn {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
}
.category-btn:hover {
background: var(--hover-color);
border-color: var(--text-accent);
}
.category-btn.active {
background: var(--text-accent);
color: var(--bg-primary);
border-color: var(--text-accent);
}
.leaderboard-category {
margin: 20px 0;
display: none;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-secondary);
border-radius: 10px;
overflow: hidden;
box-shadow: var(--shadow-dark);
}
.leaderboard-table th {
background: var(--gradient-primary);
color: white;
padding: 15px;
text-align: left;
font-weight: bold;
}
.leaderboard-table td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.leaderboard-table tr:nth-child(even) {
background: var(--bg-tertiary);
}
.leaderboard-table tr:hover {
background: var(--hover-color);
}
.rank-1 { color: #FFD700; font-weight: bold; }
.rank-2 { color: #C0C0C0; font-weight: bold; }
.rank-3 { color: #CD7F32; font-weight: bold; }
.category-title {
color: var(--text-accent);
margin: 30px 0 15px 0;
font-size: 1.5em;
text-align: center;
}
.no-data {
text-align: center;
color: var(--text-secondary);
font-style: italic;
padding: 20px;
}
</style>
"""
return content
def generate_leaderboard_category(self, category_id, title, data, headers, row_formatter, is_default=False):
"""Generate HTML for a single leaderboard category"""
display_style = "block" if is_default else "none"
content = f"""
<div id="{category_id}" class="leaderboard-category" style="display: {display_style};">
<h2 class="category-title">{title}</h2>
"""
if not data or len(data) == 0:
content += '<div class="no-data">No data available for this category yet.</div>'
else:
content += '<table class="leaderboard-table">'
# Headers
content += '<thead><tr>'
for header in headers:
content += f'<th>{header}</th>'
content += '</tr></thead>'
# Data rows
content += '<tbody>'
for i, player in enumerate(data):
row_data = row_formatter(player, i)
rank_class = f"rank-{i+1}" if i < 3 else ""
content += f'<tr class="{rank_class}">'
for cell in row_data:
content += f'<td>{cell}</td>'
content += '</tr>'
content += '</tbody>'
content += '</table>'
content += '</div>'
return content
def serve_locations(self):
"""Serve the locations page with real data"""
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Locations", "Database not available")
return
# Fetch locations data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
loop.close()
self.serve_locations_data(locations_data)
except Exception as e:
print(f"Error fetching locations data: {e}")
self.serve_error_page("Locations", f"Error loading locations: {str(e)}")
async def fetch_locations_data(self, database):
"""Fetch all locations and their spawn data from database"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get all locations
cursor = await db.execute("""
SELECT l.*,
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
LEFT JOIN pet_species ps ON ls.species_id = ps.id
GROUP BY l.id
ORDER BY l.id
""")
rows = await cursor.fetchall()
# Convert SQLite rows to dictionaries properly
locations = []
for row in rows:
location_dict = {
'id': row[0],
'name': row[1],
'description': row[2],
'level_min': row[3],
'level_max': row[4],
'spawns': row[5] if len(row) > 5 else None
}
locations.append(location_dict)
return locations
except Exception as e:
print(f"Database error fetching locations: {e}")
return []
def serve_locations_data(self, locations_data):
"""Serve locations page with real data using unified template"""
# Build locations HTML
locations_html = ""
if locations_data:
for location in locations_data:
spawns = location.get('spawns', 'No pets found')
if not spawns or spawns == 'None':
spawns = "No pets spawn here yet"
# 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 = ""
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'<span class="spawn-badge">{spawn}</span>'
# Add hidden spawn badges (initially hidden)
if hidden_spawns:
location_id = location['id']
for spawn in hidden_spawns:
spawn_badges += f'<span class="spawn-badge hidden-spawn" id="hidden-{location_id}">{spawn}</span>'
# Add functional "show more" button
spawn_badges += f'<span class="spawn-badge more-button" onclick="toggleSpawns({location_id})">+{len(hidden_spawns)} more</span>'
if not spawn_badges:
spawn_badges = '<em style="color: var(--text-secondary);">No pets spawn here yet</em>'
locations_html += f"""
<div class="location-card">
<div class="location-header">
<h3>🗺️ {location['name']}</h3>
<div class="location-id">ID: {location['id']}</div>
</div>
<div class="location-description">
{location['description']}
</div>
<div class="location-levels">
<strong>Level Range:</strong> {location['level_min']}-{location['level_max']}
</div>
<div class="location-spawns">
<strong>Wild Pets:</strong><br>
{spawn_badges}
</div>
</div>"""
else:
locations_html = """
<div class="location-card">
<div class="location-header">
<h3>No Locations Found</h3>
</div>
<div class="location-description">
No game locations are configured yet.
</div>
</div>"""
content = f"""
<div class="header">
<h1>🗺️ Game Locations</h1>
<p>Explore all areas and discover what pets await you!</p>
</div>
<div class="info-section">
<h2>🎯 How Locations Work</h2>
<p><strong>Travel:</strong> Use <code>!travel &lt;location&gt;</code> to move between areas</p>
<p><strong>Explore:</strong> Use <code>!explore</code> to find wild pets in your current location</p>
<p><strong>Unlock:</strong> Some locations require achievements - catch specific pet types to unlock new areas!</p>
<p><strong>Weather:</strong> Check <code>!weather</code> for conditions that boost certain pet spawn rates</p>
</div>
<div class="locations-grid">
{locations_html}
</div>
<div class="info-section">
<p style="text-align: center; color: var(--text-secondary); margin: 0;">
💡 Use <code>!wild &lt;location&gt;</code> in #petz to see what pets spawn in a specific area
</p>
</div>
<script>
function toggleSpawns(locationId) {{
const hiddenSpawns = document.querySelectorAll(`#hidden-${{locationId}}`);
const moreButton = document.querySelector(`[onclick="toggleSpawns(${{locationId}})"]`);
if (!moreButton) return;
const isHidden = hiddenSpawns[0] && hiddenSpawns[0].style.display === 'none' || hiddenSpawns[0] && hiddenSpawns[0].classList.contains('hidden-spawn');
if (isHidden) {{
// Show hidden spawns
hiddenSpawns.forEach(spawn => {{
spawn.classList.remove('hidden-spawn');
spawn.style.display = 'inline-block';
}});
moreButton.textContent = 'Show less';
moreButton.className = 'spawn-badge less-button';
moreButton.setAttribute('onclick', `hideSpawns(${{locationId}})`);
}}
}}
function hideSpawns(locationId) {{
const hiddenSpawns = document.querySelectorAll(`#hidden-${{locationId}}`);
const lessButton = document.querySelector(`[onclick="hideSpawns(${{locationId}})"]`);
if (!lessButton) return;
// Hide spawns again
hiddenSpawns.forEach(spawn => {{
spawn.classList.add('hidden-spawn');
spawn.style.display = 'none';
}});
const hiddenCount = hiddenSpawns.length;
lessButton.textContent = `+${{hiddenCount}} more`;
lessButton.className = 'spawn-badge more-button';
lessButton.setAttribute('onclick', `toggleSpawns(${{locationId}})`);
}}
</script>
"""
# Add locations-specific CSS
additional_css = """
.locations-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 25px;
margin-top: 30px;
}
.location-card {
background: var(--bg-secondary);
border-radius: 15px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
transition: transform 0.3s ease;
}
.location-card:hover {
transform: translateY(-5px);
}
.location-header {
background: var(--gradient-primary);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.location-header h3 {
margin: 0;
font-size: 1.3em;
font-weight: 700;
}
.location-id {
background: rgba(255,255,255,0.2);
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8em;
}
.location-description {
padding: 20px;
color: var(--text-primary);
font-style: italic;
border-bottom: 1px solid var(--border-color);
}
.location-levels {
padding: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.location-spawns {
padding: 20px;
}
.spawn-badge {
background: var(--bg-tertiary);
color: var(--text-accent);
padding: 4px 10px;
border-radius: 12px;
font-size: 0.85em;
margin: 3px;
display: inline-block;
border: 1px solid var(--border-color);
}
.hidden-spawn {
display: none;
}
.more-button {
background: var(--gradient-primary) !important;
color: white !important;
cursor: pointer;
transition: transform 0.2s ease;
}
.more-button:hover {
transform: scale(1.05);
}
.less-button {
background: #ff6b6b !important;
color: white !important;
cursor: pointer;
transition: transform 0.2s ease;
}
.less-button:hover {
transform: scale(1.05);
}
.info-section {
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.info-section h2 {
color: var(--text-accent);
margin-top: 0;
}
"""
# Get the unified template with additional CSS
html_content = self.get_page_template("Game Locations", content, "locations")
# Insert additional CSS before closing </style> tag
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_petdex(self):
"""Serve the petdex page with all pet species data"""
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_error_page("Petdex", "Database not available")
return
# Fetch petdex data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
petdex_data = loop.run_until_complete(self.fetch_petdex_data(database))
loop.close()
self.serve_petdex_data(petdex_data)
except Exception as e:
print(f"Error fetching petdex data: {e}")
self.serve_error_page("Petdex", f"Error loading petdex: {str(e)}")
async def fetch_petdex_data(self, database):
"""Fetch all pet species data from database"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get all pet species with evolution information (no duplicates)
cursor = await db.execute("""
SELECT DISTINCT ps.id, ps.name, ps.type1, ps.type2, ps.base_hp, ps.base_attack,
ps.base_defense, ps.base_speed, ps.evolution_level, ps.evolution_species_id,
ps.rarity, ps.emoji,
evolve_to.name as evolves_to_name,
(SELECT COUNT(*) FROM location_spawns ls WHERE ls.species_id = ps.id) as location_count
FROM pet_species ps
LEFT JOIN pet_species evolve_to ON ps.evolution_species_id = evolve_to.id
ORDER BY ps.rarity ASC, ps.name ASC
""")
rows = await cursor.fetchall()
pets = []
for row in rows:
pet_dict = {
'id': row[0], 'name': row[1], 'type1': row[2], 'type2': row[3],
'base_hp': row[4], 'base_attack': row[5], 'base_defense': row[6],
'base_speed': row[7], 'evolution_level': row[8],
'evolution_species_id': row[9], 'rarity': row[10], 'emoji': row[11],
'evolves_to_name': row[12], 'location_count': row[13]
}
pets.append(pet_dict)
# Get spawn locations for each pet
for pet in pets:
cursor = await db.execute("""
SELECT l.name, ls.min_level, ls.max_level, ls.spawn_rate
FROM location_spawns ls
JOIN locations l ON ls.location_id = l.id
WHERE ls.species_id = ?
ORDER BY l.name ASC
""", (pet['id'],))
spawn_rows = await cursor.fetchall()
pet['spawn_locations'] = []
for spawn_row in spawn_rows:
spawn_dict = {
'location_name': spawn_row[0], 'min_level': spawn_row[1],
'max_level': spawn_row[2], 'spawn_rate': spawn_row[3]
}
pet['spawn_locations'].append(spawn_dict)
return pets
except Exception as e:
print(f"Database error fetching petdex: {e}")
return []
def serve_petdex_data(self, petdex_data):
"""Serve petdex page with all pet species data"""
# Build pet cards HTML grouped by rarity
rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"}
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
# Calculate statistics
total_species = len(petdex_data)
type_counts = {}
for pet in petdex_data:
if pet['type1'] not in type_counts:
type_counts[pet['type1']] = 0
type_counts[pet['type1']] += 1
if pet['type2'] and pet['type2'] not in type_counts:
type_counts[pet['type2']] = 0
if pet['type2']:
type_counts[pet['type2']] += 1
# Build statistics section
stats_content = f"""
<div class="grid grid-4">
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">📊 Total Species</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_species}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">🎨 Types</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len(type_counts)}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">⭐ Rarities</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len(set(pet['rarity'] for pet in petdex_data))}</div>
</div>
<div class="card">
<h3 style="color: var(--accent-blue); margin-top: 0;">🧬 Evolutions</h3>
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len([p for p in petdex_data if p['evolution_level']])}</div>
</div>
</div>
"""
pets_by_rarity = {}
for pet in petdex_data:
rarity = pet['rarity']
if rarity not in pets_by_rarity:
pets_by_rarity[rarity] = []
pets_by_rarity[rarity].append(pet)
petdex_html = ""
total_species = len(petdex_data)
for rarity in sorted(pets_by_rarity.keys()):
pets_in_rarity = pets_by_rarity[rarity]
rarity_name = rarity_names.get(rarity, f"Rarity {rarity}")
rarity_color = rarity_colors.get(rarity, "#ffffff")
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: {rarity_color}; border-bottom: 2px solid {rarity_color}; padding-bottom: 10px;">
{rarity_name} ({len(pets_in_rarity)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_in_rarity:
# Build type display
type_str = pet['type1']
if pet['type2']:
type_str += f"/{pet['type2']}"
# Build evolution info
evolution_info = ""
if pet['evolution_level'] and pet['evolves_to_name']:
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']}{pet['evolves_to_name']}"
elif pet['evolution_level']:
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']}"
# Build spawn locations
spawn_info = ""
if pet['spawn_locations']:
locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})"
for loc in pet['spawn_locations'][:3]]
if len(pet['spawn_locations']) > 3:
locations.append(f"+{len(pet['spawn_locations']) - 3} more")
spawn_info = f"<br><strong>Found in:</strong> {', '.join(locations)}"
else:
spawn_info = "<br><strong>Found in:</strong> Not yet available"
# Calculate total base stats
total_stats = pet['base_hp'] + pet['base_attack'] + pet['base_defense'] + pet['base_speed']
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {rarity_color};">
<div class="pet-header">
<h3 style="color: {rarity_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-row">
<span>HP: {pet['base_hp']}</span>
<span>ATK: {pet['base_attack']}</span>
</div>
<div class="stat-row">
<span>DEF: {pet['base_defense']}</span>
<span>SPD: {pet['base_speed']}</span>
</div>
<div class="total-stats">Total: {total_stats}</div>
</div>
<div class="pet-info">
<strong>Rarity:</strong> {rarity_name}{evolution_info}{spawn_info}
</div>
</div>"""
petdex_html += """
</div>
</div>"""
if not petdex_data:
petdex_html = """
<div style="text-align: center; padding: 60px; color: var(--text-secondary);">
<h2>No pet species found!</h2>
<p>The petdex appears to be empty. Contact an administrator.</p>
</div>"""
# Combine all content
content = f"""
<div class="header">
<h1>📖 Petdex</h1>
<p>Complete encyclopedia of all available pets</p>
</div>
{stats_content}
<div class="card">
<h2>📊 Pet Collection by Rarity</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">🎯 Pets are organized by rarity. Use <code>!wild &lt;location&gt;</code> in #petz to see what spawns where!</p>
{petdex_html}
</div>
"""
html = self.get_page_template("Petdex", content, "petdex")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_player_profile(self, nickname):
"""Serve individual player profile page"""
# URL decode the nickname in case it has special characters
from urllib.parse import unquote
nickname = unquote(nickname)
# Get database instance from the server class
database = self.server.database if hasattr(self.server, 'database') else None
if not database:
self.serve_player_error(nickname, "Database not available")
return
# Fetch player data
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
loop.close()
if player_data is None:
self.serve_player_not_found(nickname)
return
self.serve_player_data(nickname, player_data)
except Exception as e:
print(f"Error fetching player data for {nickname}: {e}")
self.serve_player_error(nickname, f"Error loading player data: {str(e)}")
return
async def fetch_player_data(self, database, nickname):
"""Fetch all player data from database"""
try:
# Get player info
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Get player basic info
cursor = await db.execute("""
SELECT p.*, l.name as location_name, l.description as location_desc
FROM players p
LEFT JOIN locations l ON p.current_location_id = l.id
WHERE p.nickname = ?
""", (nickname,))
player = await cursor.fetchone()
if not player:
return None
# Convert to dict manually
player_dict = {
'id': player[0],
'nickname': player[1],
'created_at': player[2],
'last_active': player[3],
'level': player[4],
'experience': player[5],
'money': player[6],
'current_location_id': player[7],
'location_name': player[8],
'location_desc': player[9]
}
# Get player pets
cursor = await db.execute("""
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.is_active DESC, p.level DESC, p.id ASC
""", (player_dict['id'],))
pets_rows = await cursor.fetchall()
pets = []
for row in pets_rows:
pet_dict = {
'id': row[0], 'player_id': row[1], 'species_id': row[2],
'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': bool(row[13]), # Convert to proper boolean
'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17],
'emoji': row[18] if row[18] else '🐾' # Add emoji support
}
pets.append(pet_dict)
# Get player achievements
cursor = await db.execute("""
SELECT pa.*, a.name as achievement_name, a.description as achievement_desc
FROM player_achievements pa
JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = ?
ORDER BY pa.completed_at DESC
""", (player_dict['id'],))
achievements_rows = await cursor.fetchall()
achievements = []
for row in achievements_rows:
achievement_dict = {
'id': row[0], 'player_id': row[1], 'achievement_id': row[2],
'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5]
}
achievements.append(achievement_dict)
# Get player inventory
cursor = await db.execute("""
SELECT i.name, i.description, i.category, i.rarity, pi.quantity
FROM player_inventory pi
JOIN items i ON pi.item_id = i.id
WHERE pi.player_id = ?
ORDER BY i.rarity DESC, i.name ASC
""", (player_dict['id'],))
inventory_rows = await cursor.fetchall()
inventory = []
for row in inventory_rows:
item_dict = {
'name': row[0], 'description': row[1], 'category': row[2],
'rarity': row[3], 'quantity': row[4]
}
inventory.append(item_dict)
# Get player gym badges
cursor = await db.execute("""
SELECT g.name, g.badge_name, g.badge_icon, l.name as location_name,
pgb.victories, pgb.first_victory_date, pgb.highest_difficulty
FROM player_gym_battles pgb
JOIN gyms g ON pgb.gym_id = g.id
JOIN locations l ON g.location_id = l.id
WHERE pgb.player_id = ? AND pgb.victories > 0
ORDER BY pgb.first_victory_date ASC
""", (player_dict['id'],))
gym_badges_rows = await cursor.fetchall()
gym_badges = []
for row in gym_badges_rows:
badge_dict = {
'gym_name': row[0], 'badge_name': row[1], 'badge_icon': row[2],
'location_name': row[3], 'victories': row[4],
'first_victory_date': row[5], 'highest_difficulty': row[6]
}
gym_badges.append(badge_dict)
# Get player encounters using database method
encounters = []
try:
# Use the existing database method which handles row factory properly
temp_encounters = await database.get_player_encounters(player_dict['id'])
for enc in temp_encounters:
encounter_dict = {
'species_name': enc['species_name'],
'type1': enc['type1'],
'type2': enc['type2'],
'rarity': enc['rarity'],
'total_encounters': enc['total_encounters'],
'caught_count': enc['caught_count'],
'first_encounter_date': enc['first_encounter_date']
}
encounters.append(encounter_dict)
except Exception as e:
print(f"Error fetching encounters: {e}")
encounters = []
# Get encounter stats
try:
encounter_stats = await database.get_encounter_stats(player_dict['id'])
except Exception as e:
print(f"Error fetching encounter stats: {e}")
encounter_stats = {
'species_encountered': 0,
'total_encounters': 0,
'total_species': 0,
'completion_percentage': 0.0
}
return {
'player': player_dict,
'pets': pets,
'achievements': achievements,
'inventory': inventory,
'gym_badges': gym_badges,
'encounters': encounters,
'encounter_stats': encounter_stats
}
except Exception as e:
print(f"Database error fetching player {nickname}: {e}")
return None
def serve_player_not_found(self, nickname):
"""Serve player not found page using unified template"""
content = f"""
<div class="header">
<h1>🚫 Player Not Found</h1>
</div>
<div class="error-message">
<h2>Player "{nickname}" not found</h2>
<p>This player hasn't started their journey yet or doesn't exist.</p>
<p>Players can use <code>!start</code> in #petz to begin their adventure!</p>
</div>
"""
# Add error-specific CSS
additional_css = """
.main-container {
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.error-message {
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid var(--error-color);
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.error-message h2 {
color: var(--error-color);
margin-top: 0;
}
"""
html_content = self.get_page_template("Player Not Found", content, "players")
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(404)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_player_error(self, nickname, error_msg):
"""Serve player error page using unified template"""
content = f"""
<div class="header">
<h1>⚠️ Error</h1>
</div>
<div class="error-message">
<h2>Unable to load player data</h2>
<p>{error_msg}</p>
<p>Please try again later or contact an administrator.</p>
</div>
"""
# Add error-specific CSS
additional_css = """
.main-container {
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.error-message {
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid var(--error-color);
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.error-message h2 {
color: var(--error-color);
margin-top: 0;
}
"""
html_content = self.get_page_template("Player Error", content, "players")
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_player_data(self, nickname, player_data):
"""Serve player profile page with real data"""
player = player_data['player']
pets = player_data['pets']
achievements = player_data['achievements']
inventory = player_data.get('inventory', [])
gym_badges = player_data.get('gym_badges', [])
encounters = player_data.get('encounters', [])
encounter_stats = player_data.get('encounter_stats', {})
# Calculate stats
active_pets = [pet for pet in pets if pet['is_active']]
total_pets = len(pets)
active_count = len(active_pets)
# Build pets table HTML
pets_html = ""
if pets:
for pet in pets:
status = "⭐ Active" if pet['is_active'] else "📦 Storage"
status_class = "pet-active" if pet['is_active'] else "pet-stored"
name = pet['nickname'] or pet['species_name']
type_str = pet['type1']
if pet['type2']:
type_str += f"/{pet['type2']}"
pets_html += f"""
<tr>
<td class="{status_class}">{status}</td>
<td><strong>{pet.get('emoji', '🐾')} {name}</strong></td>
<td>{pet['species_name']}</td>
<td><span class="type-badge">{type_str}</span></td>
<td>{pet['level']}</td>
<td>{pet['hp']}/{pet['max_hp']}</td>
<td>ATK: {pet['attack']} | DEF: {pet['defense']} | SPD: {pet['speed']}</td>
</tr>"""
else:
pets_html = """
<tr>
<td colspan="7" style="text-align: center; padding: 40px;">
No pets found. Use !explore and !catch to start your collection!
</td>
</tr>"""
# Build achievements HTML
achievements_html = ""
if achievements:
for achievement in achievements:
achievements_html += f"""
<div class="achievement-card">
<div class="achievement-icon">🏆</div>
<div class="achievement-content">
<h4>{achievement['achievement_name']}</h4>
<p>{achievement['achievement_desc']}</p>
<span class="achievement-date">Earned: {achievement['completed_at']}</span>
</div>
</div>"""
else:
achievements_html = """
<div class="empty-state">
<div class="empty-icon">🏆</div>
<h3>No achievements yet</h3>
<p>Keep exploring and catching pets to earn achievements!</p>
</div>"""
# Build inventory HTML
inventory_html = ""
if inventory:
rarity_symbols = {
"common": "",
"uncommon": "",
"rare": "",
"epic": "",
"legendary": ""
}
rarity_colors = {
"common": "#ffffff",
"uncommon": "#1eff00",
"rare": "#0070dd",
"epic": "#a335ee",
"legendary": "#ff8000"
}
for item in inventory:
symbol = rarity_symbols.get(item['rarity'], "")
color = rarity_colors.get(item['rarity'], "#ffffff")
quantity_str = f" x{item['quantity']}" if item['quantity'] > 1 else ""
inventory_html += f"""
<div class="inventory-item" style="border-left-color: {color};">
<div class="item-header">
<strong style="color: {color};">{symbol} {item['name']}{quantity_str}</strong>
</div>
<div class="item-description">{item['description']}</div>
<div class="item-meta">Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}</div>
<div class="item-command">💬 Use with: <code>!use {item['name']}</code></div>
</div>"""
else:
inventory_html = """
<div class="empty-state">
<div class="empty-icon">🎒</div>
<h3>No items yet</h3>
<p>Try exploring to find useful items!</p>
</div>"""
# Build gym badges HTML
badges_html = ""
if gym_badges:
for badge in gym_badges:
# Safely handle date formatting
try:
if badge['first_victory_date'] and isinstance(badge['first_victory_date'], str):
badge_date = badge['first_victory_date'].split()[0]
else:
badge_date = 'Unknown'
except (AttributeError, IndexError):
badge_date = 'Unknown'
badges_html += f"""
<div class="badge-card">
<div class="badge-icon">{badge['badge_icon']}</div>
<div class="badge-content">
<h4>{badge['badge_name']}</h4>
<p>Earned from {badge['gym_name']} ({badge['location_name']})</p>
<div class="badge-stats">
<span>First victory: {badge_date}</span>
<span>Total victories: {badge['victories']}</span>
<span>Highest difficulty: Level {badge['highest_difficulty']}</span>
</div>
</div>
</div>"""
else:
badges_html = """
<div class="empty-state">
<div class="empty-icon">🏆</div>
<h3>No gym badges yet</h3>
<p>Challenge gyms to earn badges and prove your training skills!</p>
</div>"""
# Build encounters HTML
encounters_html = ""
if encounters:
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
for encounter in encounters:
rarity_color = rarity_colors.get(encounter['rarity'], "#ffffff")
type_str = encounter['type1']
if encounter['type2']:
type_str += f"/{encounter['type2']}"
# Safely handle date formatting
try:
if encounter['first_encounter_date'] and isinstance(encounter['first_encounter_date'], str):
encounter_date = encounter['first_encounter_date'].split()[0]
else:
encounter_date = 'Unknown'
except (AttributeError, IndexError):
encounter_date = 'Unknown'
encounters_html += f"""
<div class="encounter-card" style="border-left-color: {rarity_color};">
<div class="encounter-header">
<strong style="color: {rarity_color};">{encounter['species_name']}</strong>
<span class="type-badge">{type_str}</span>
</div>
<div class="encounter-stats">
<span>Encountered {encounter['total_encounters']} times</span>
<span>Caught {encounter['caught_count']} times</span>
</div>
<div class="encounter-date">First seen: {encounter_date}</div>
</div>"""
else:
encounters_html = """
<div class="empty-state">
<div class="empty-icon">👁️</div>
<h3>No pets encountered yet</h3>
<p>Use !explore to discover wild pets!</p>
</div>"""
# Build content for the unified template
content = f"""
<div class="header">
<h1>🐾 {nickname}'s Profile</h1>
<p>Level {player['level']} Trainer</p>
<p><em>Currently in {player.get('location_name', 'Unknown Location')}</em></p>
<div style="margin-top: 20px;">
<a href="/teambuilder/{nickname}" class="btn btn-primary">
🔧 Team Builder
</a>
</div>
</div>
<!-- Quick Navigation -->
<div class="quick-nav">
<a href="#stats" class="nav-pill">📊 Stats</a>
<a href="#pets" class="nav-pill">🐾 Pets</a>
<a href="#achievements" class="nav-pill">🏆 Achievements</a>
<a href="#inventory" class="nav-pill">🎒 Inventory</a>
<a href="#gym-badges" class="nav-pill">🏆 Badges</a>
<a href="#encounters" class="nav-pill">👁️ Encounters</a>
</div>
<div class="section" id="stats">
<div class="section-header">📊 Player Statistics</div>
<div class="section-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{player['level']}</div>
<div class="stat-label">Level</div>
</div>
<div class="stat-card">
<div class="stat-value">{player['experience']}</div>
<div class="stat-label">Experience</div>
</div>
<div class="stat-card">
<div class="stat-value">${player['money']}</div>
<div class="stat-label">Money</div>
</div>
<div class="stat-card">
<div class="stat-value">{total_pets}</div>
<div class="stat-label">Pets Caught</div>
</div>
<div class="stat-card">
<div class="stat-value">{active_count}</div>
<div class="stat-label">Active Pets</div>
</div>
<div class="stat-card">
<div class="stat-value">{len(achievements)}</div>
<div class="stat-label">Achievements</div>
</div>
<div class="stat-card">
<div class="stat-value">{encounter_stats.get('species_encountered', 0)}</div>
<div class="stat-label">Species Seen</div>
</div>
<div class="stat-card">
<div class="stat-value">{encounter_stats.get('completion_percentage', 0)}%</div>
<div class="stat-label">Petdex Complete</div>
</div>
</div>
</div>
</div>
<div class="section" id="pets">
<div class="section-header">🐾 Pet Collection</div>
<div class="section-content">
<div class="pets-container">
<div class="pets-table-wrapper">
<table class="pets-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>Species</th>
<th>Type</th>
<th>Level</th>
<th>HP</th>
<th>Stats</th>
</tr>
</thead>
<tbody>
{pets_html}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="section" id="achievements">
<div class="section-header">🏆 Achievements</div>
<div class="section-content">
<div class="achievements-grid">
{achievements_html}
</div>
</div>
</div>
<div class="section" id="inventory">
<div class="section-header">🎒 Inventory</div>
<div class="section-content">
<div class="inventory-grid">
{inventory_html}
</div>
</div>
</div>
<div class="section" id="gym-badges">
<div class="section-header">🏆 Gym Badges</div>
<div class="section-content">
<div class="badges-grid">
{badges_html}
</div>
</div>
</div>
<div class="section" id="encounters">
<div class="section-header">👁️ Pet Encounters</div>
<div class="section-content">
<div class="encounters-summary">
<p>Species discovered: {encounter_stats.get('species_encountered', 0)}/{encounter_stats.get('total_species', 0)}
({encounter_stats.get('completion_percentage', 0)}% complete)</p>
<p>Total encounters: {encounter_stats.get('total_encounters', 0)}</p>
</div>
<div class="encounters-grid">
{encounters_html}
</div>
</div>
</div>
"""
# Add custom CSS for the profile page
additional_css = """
/* Profile Page Styles */
html {
scroll-behavior: smooth;
}
.quick-nav {
background: var(--bg-secondary);
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 2px 10px var(--shadow-color);
}
.nav-pill {
display: inline-block;
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
margin: 5px;
font-size: 0.9em;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
}
.nav-pill:hover {
background: var(--gradient-primary);
color: white;
transform: translateY(-2px);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: var(--bg-tertiary);
padding: 20px;
border-radius: 10px;
text-align: center;
border: 1px solid var(--border-color);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: var(--text-accent);
margin-bottom: 5px;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9em;
}
.pets-table-wrapper {
overflow-x: auto;
}
.pets-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
min-width: 600px;
}
.pets-table th, .pets-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.pets-table th {
background: var(--bg-primary);
color: var(--text-accent);
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.pets-table tr:hover {
background: var(--hover-color);
}
.pet-active {
color: var(--text-accent);
font-weight: bold;
}
.pet-stored {
color: var(--text-secondary);
}
.type-badge {
background: var(--bg-primary);
color: var(--text-accent);
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
margin-right: 5px;
}
.achievements-grid, .badges-grid, .encounters-grid, .inventory-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.achievement-card, .badge-card, .encounter-card, .inventory-item {
background: var(--bg-tertiary);
padding: 15px;
border-radius: 8px;
border-left: 4px solid var(--text-accent);
transition: transform 0.3s ease;
}
.achievement-card:hover, .badge-card:hover, .encounter-card:hover, .inventory-item:hover {
transform: translateY(-3px);
}
.achievement-card {
display: flex;
align-items: flex-start;
gap: 15px;
}
.achievement-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.achievement-content h4 {
margin: 0 0 8px 0;
color: var(--text-accent);
}
.achievement-content p {
margin: 0 0 8px 0;
color: var(--text-primary);
}
.achievement-date {
color: var(--text-secondary);
font-size: 0.9em;
}
.badge-card {
display: flex;
align-items: flex-start;
gap: 15px;
border-left-color: gold;
}
.badge-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.badge-content h4 {
margin: 0 0 8px 0;
color: gold;
}
.badge-content p {
margin: 0 0 10px 0;
color: var(--text-primary);
}
.badge-stats {
display: flex;
flex-direction: column;
gap: 4px;
}
.badge-stats span {
color: var(--text-secondary);
font-size: 0.9em;
}
.encounter-card {
border-left: 4px solid var(--text-accent);
}
.encounter-header {
margin-bottom: 10px;
}
.encounter-stats {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.encounter-stats span {
color: var(--text-primary);
font-size: 0.9em;
}
.encounter-date {
color: var(--text-secondary);
font-size: 0.9em;
}
.inventory-item {
border-left: 4px solid var(--text-accent);
}
.item-header {
margin-bottom: 8px;
}
.item-description {
color: var(--text-primary);
margin-bottom: 8px;
}
.item-meta {
color: var(--text-secondary);
font-size: 0.9em;
}
.item-command {
margin-top: 8px;
color: var(--text-accent);
font-size: 0.85em;
}
.item-command code {
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
color: var(--text-primary);
}
.empty-state {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.empty-icon {
font-size: 3em;
margin-bottom: 15px;
}
.empty-state h3 {
margin: 0 0 10px 0;
color: var(--text-primary);
}
.empty-state p {
margin: 0;
font-size: 1.1em;
}
.encounters-summary {
text-align: center;
margin-bottom: 20px;
padding: 15px;
background: var(--bg-tertiary);
border-radius: 8px;
}
.encounters-summary p {
margin: 5px 0;
color: var(--text-secondary);
}
.btn {
display: inline-block;
padding: 12px 24px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
text-align: center;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
/* Mobile Responsive */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-card {
padding: 15px;
}
.stat-value {
font-size: 1.5em;
}
.achievements-grid, .badges-grid, .encounters-grid, .inventory-grid {
grid-template-columns: 1fr;
}
.nav-pill {
padding: 6px 12px;
font-size: 0.8em;
margin: 3px;
}
.pets-table {
min-width: 500px;
}
.pets-table th, .pets-table td {
padding: 8px 10px;
font-size: 0.9em;
}
}
"""
# Get the unified template with the additional CSS
html_content = self.get_page_template(f"{nickname}'s Profile", content, "players")
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def log_message(self, format, *args):
"""Override to reduce logging noise"""
pass
def serve_teambuilder(self, nickname):
"""Serve the team builder interface"""
from urllib.parse import unquote
nickname = unquote(nickname)
try:
from src.database import Database
database = Database()
# Get event loop
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
if player_data is None:
self.serve_player_not_found(nickname)
return
pets = player_data['pets']
if not pets:
self.serve_teambuilder_no_pets(nickname)
return
self.serve_teambuilder_interface(nickname, pets)
except Exception as e:
print(f"Error loading team builder for {nickname}: {e}")
self.serve_player_error(nickname, f"Error loading team builder: {str(e)}")
def serve_teambuilder_no_pets(self, nickname):
"""Show message when player has no pets using unified template"""
content = f"""
<div class="header">
<h1>🐾 Team Builder</h1>
<p>Build your perfect team for battles and adventures</p>
</div>
<div class="no-pets-message">
<h2>🐾 No Pets Found</h2>
<p>{nickname}, you need to catch some pets before using the team builder!</p>
<p>Head to the IRC channel and use <code>!explore</code> to find wild pets!</p>
<a href="/player/{nickname}" class="btn btn-primary">← Back to Profile</a>
</div>
"""
# Add no-pets-specific CSS
additional_css = """
.main-container {
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.no-pets-message {
background: var(--bg-secondary);
padding: 40px;
border-radius: 15px;
border: 2px solid var(--warning-color);
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.no-pets-message h2 {
color: var(--warning-color);
margin-top: 0;
}
"""
html_content = self.get_page_template(f"Team Builder - {nickname}", content, "players")
html_content = html_content.replace("</style>", additional_css + "</style>")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_teambuilder_interface(self, nickname, pets):
"""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']]
# 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"
status_class = "active" if is_active else "storage"
type_str = pet['type1']
if pet['type2']:
type_str += f"/{pet['type2']}"
# Get emoji for the pet species
emoji = pet.get('emoji', '🐾') # Default to paw emoji if none specified
# 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}, team_order={pet.get('team_order', 'None')}")
# 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()}" data-team-order="{pet.get('team_order', '')}">
<div class="pet-header">
<h4 class="pet-name">{emoji} {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>"""
# Create 6 numbered slots and place pets in their positions
team_slots = [''] * 6 # Initialize 6 empty slots
# Place active pets in their team_order positions
for pet in active_pets:
team_order = pet.get('team_order')
if team_order and 1 <= team_order <= 6:
team_slots[team_order - 1] = make_pet_card(pet, True)
storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets)
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Builder - {nickname}</title>
<style>
: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;
}}
.team-slots-container {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
min-height: 400px;
}}
.team-slot {{
background: var(--bg-tertiary);
border: 2px dashed #666;
border-radius: 12px;
padding: 10px;
position: relative;
min-height: 120px;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}}
.team-slot:hover {{
border-color: var(--text-accent);
background: var(--drag-hover);
}}
.team-slot.drag-over {{
border-color: var(--text-accent);
background: var(--drag-hover);
border-style: solid;
transform: scale(1.02);
}}
.slot-number {{
position: absolute;
top: 5px;
left: 5px;
background: var(--active-color);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9em;
font-weight: bold;
z-index: 10;
}}
.slot-content {{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}}
.slot-content:empty::before {{
content: "Empty Slot";
color: var(--text-secondary);
font-style: italic;
opacity: 0.7;
}}
.slot-content .pet-card {{
margin: 0;
width: 100%;
max-width: none;
}}
.pet-card {{
background: var(--bg-tertiary);
border-radius: 12px;
padding: 15px;
cursor: grab;
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
user-select: none;
}}
.pet-card:active {{
cursor: grabbing;
}}
.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;
}}
.back-link {{
color: var(--text-accent);
text-decoration: none;
margin-bottom: 20px;
display: inline-block;
font-weight: 500;
}}
.back-link:hover {{
text-decoration: underline;
}}
.config-section {{
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin: 30px 0;
border: 1px solid var(--bg-tertiary);
}}
.config-slots {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}}
.config-slot {{
background: var(--bg-tertiary);
border-radius: 10px;
padding: 20px;
border: 1px solid var(--drag-hover);
}}
.config-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}}
.slot-label {{
font-weight: bold;
color: var(--text-accent);
}}
.config-name {{
color: var(--text-secondary);
font-style: italic;
}}
.config-actions {{
display: flex;
gap: 10px;
}}
.config-btn {{
flex: 1;
padding: 10px 15px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
transition: all 0.3s ease;
}}
.save-config {{
background: #4CAF50;
color: white;
}}
.save-config:hover {{
background: #45a049;
transform: translateY(-2px);
}}
.load-config {{
background: #2196F3;
color: white;
}}
.load-config:hover:not(:disabled) {{
background: #1976D2;
transform: translateY(-2px);
}}
.load-config:disabled {{
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}}
.working-section {{
background: var(--bg-secondary);
border-radius: 15px;
padding: 20px;
margin: 20px 0;
border: 2px solid var(--text-accent);
}}
.config-selector {{
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}}
.config-selector label {{
color: var(--text-primary);
font-weight: 500;
}}
.config-selector select {{
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--drag-hover);
border-radius: 8px;
padding: 10px 15px;
font-size: 1em;
min-width: 250px;
}}
.config-selector select:focus {{
outline: none;
border-color: var(--text-accent);
box-shadow: 0 0 0 2px rgba(102, 255, 102, 0.2);
}}
.quick-save-btn {{
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s ease;
}}
.quick-save-btn:hover:not(:disabled) {{
background: #45a049;
transform: translateY(-2px);
}}
.quick-save-btn:disabled {{
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}}
.config-status {{
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px 15px;
border-left: 4px solid var(--text-accent);
}}
.status-text {{
color: var(--text-secondary);
font-style: italic;
}}
.config-quick-actions {{
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}}
.config-action-btn {{
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
min-width: 120px;
}}
.config-action-btn:hover:not(:disabled) {{
background: var(--secondary-color);
transform: translateY(-1px);
}}
.config-action-btn:disabled {{
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}}
.rename-btn {{
background: #FF9800;
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
margin-left: 10px;
transition: all 0.3s ease;
}}
.rename-btn:hover:not(:disabled) {{
background: #F57C00;
transform: translateY(-1px);
}}
.rename-btn:disabled {{
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}}
</style>
</head>
<body>
<a href="/player/{nickname}" class="back-link">← Back to {nickname}'s Profile</a>
<div class="header">
<h1>🐾 Team Builder</h1>
<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="working-section">
<div class="section-header">💾 Team Configurations</div>
<p style="color: var(--text-secondary); margin-bottom: 20px;">
Save up to 3 different team setups for quick switching between strategies
</p>
<div class="config-selector">
<label for="active-config">Choose configuration to edit:</label>
<select id="active-config" onchange="switchActiveConfig()">
<option value="current">Current Team (unsaved)</option>
<option value="1">Config 1: <span id="selector-name-1">Empty Slot</span></option>
<option value="2">Config 2: <span id="selector-name-2">Empty Slot</span></option>
<option value="3">Config 3: <span id="selector-name-3">Empty Slot</span></option>
</select>
<button class="quick-save-btn" id="quick-save-btn" onclick="quickSaveConfig()" disabled>💾 Quick Save</button>
<button class="rename-btn" id="rename-btn" onclick="renameConfig()" disabled>✏️ Rename</button>
</div>
<div class="config-status" id="config-status">
<span class="status-text">Editing current team (not saved to any configuration)</span>
</div>
<div class="config-quick-actions">
<button class="config-action-btn" onclick="loadConfigToEdit(1)" id="load-edit-1" disabled>
📂 Load Config 1
</button>
<button class="config-action-btn" onclick="loadConfigToEdit(2)" id="load-edit-2" disabled>
📂 Load Config 2
</button>
<button class="config-action-btn" onclick="loadConfigToEdit(3)" id="load-edit-3" disabled>
📂 Load Config 3
</button>
</div>
</div>
<div class="team-sections">
<div class="team-section">
<div class="section-header active-header">⭐ Active Team</div>
<div class="team-slots-container" id="team-slots-container">
<div class="team-slot" id="slot-1" data-position="1">
<div class="slot-number">1</div>
<div class="slot-content">{team_slots[0]}</div>
</div>
<div class="team-slot" id="slot-2" data-position="2">
<div class="slot-number">2</div>
<div class="slot-content">{team_slots[1]}</div>
</div>
<div class="team-slot" id="slot-3" data-position="3">
<div class="slot-number">3</div>
<div class="slot-content">{team_slots[2]}</div>
</div>
<div class="team-slot" id="slot-4" data-position="4">
<div class="slot-number">4</div>
<div class="slot-content">{team_slots[3]}</div>
</div>
<div class="team-slot" id="slot-5" data-position="5">
<div class="slot-number">5</div>
<div class="slot-content">{team_slots[4]}</div>
</div>
<div class="team-slot" id="slot-6" data-position="6">
<div class="slot-number">6</div>
<div class="slot-content">{team_slots[5]}</div>
</div>
</div>
</div>
<div class="team-section">
<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">
<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;
// Test function to verify basic functionality
function runDragDropTest() {{
console.log('=== DRAG DROP TEST ===');
const petCards = document.querySelectorAll('.pet-card');
console.log('Pet cards found:', petCards.length);
petCards.forEach((card, index) => {{
console.log(`Card ${{index}}: ID=${{card.dataset.petId}}, draggable=${{card.draggable}}, active=${{card.dataset.active}}`);
}});
const containers = ['active-container', 'storage-container', 'active-drop', 'storage-drop'];
containers.forEach(id => {{
const element = document.getElementById(id);
console.log(`Container ${{id}}: exists=${{!!element}}`);
}});
// Test function availability (non-destructive)
if (petCards.length > 0) {{
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'}}`);
console.log('Move functions available:', {{
movePetToStorage: typeof movePetToStorage === 'function',
movePetToActive: typeof movePetToActive === 'function'
}});
console.log('✅ Test complete - no pets were moved');
}}
}}
// Add click-to-move as backup for drag issues
function addClickToMoveBackup() {{
document.querySelectorAll('.pet-card').forEach(card => {{
// Add double-click handler
card.addEventListener('dblclick', function() {{
const petId = this.dataset.petId;
const currentPosition = currentTeam[petId];
console.log(`Double-click: Moving pet ${{petId}} from ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`);
if (currentPosition) {{
movePetToStorage(petId);
}} else {{
// Find first empty slot
for (let i = 1; i <= 6; i++) {{
const slot = document.getElementById(`slot-${{i}}`);
const slotContent = slot.querySelector('.slot-content');
if (slotContent.children.length === 0) {{
movePetToTeamSlot(petId, i);
return;
}}
}}
console.log('No empty slots available');
}}
}});
// Add visual hint
const hint = document.createElement('div');
hint.textContent = '💡 Double-click to move';
hint.style.cssText = 'position: absolute; top: 5px; left: 5px; background: rgba(0,0,0,0.7); color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; pointer-events: none; opacity: 0; transition: opacity 0.3s;';
card.style.position = 'relative';
card.appendChild(hint);
card.addEventListener('mouseenter', () => hint.style.opacity = '1');
card.addEventListener('mouseleave', () => hint.style.opacity = '0');
}});
}}
// Declare container variables once at the top level
const teamSlotsContainer = document.getElementById('team-slots-container');
const storageContainer = document.getElementById('storage-container');
const storageDrop = document.getElementById('storage-drop');
const teamSlots = Array.from({{length: 6}}, (_, i) => document.getElementById(`slot-${{i + 1}}`));
// 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(`Team slots container:`, teamSlotsContainer);
console.log(`Storage container has ${{storageContainer.children.length}} pets initially`);
let teamPositions = {{}}; // Track pet positions in team slots
allCards.forEach((card, index) => {{
const petId = card.dataset.petId;
const isActive = card.dataset.active === 'true';
const teamOrder = card.dataset.teamOrder;
console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, team_order=${{teamOrder}}`);
if (isActive && teamOrder) {{
teamPositions[petId] = parseInt(teamOrder);
originalTeam[petId] = parseInt(teamOrder);
currentTeam[petId] = parseInt(teamOrder);
}} else {{
originalTeam[petId] = false;
currentTeam[petId] = false;
}}
}});
console.log('Team positions:', teamPositions);
console.log('Original team state:', originalTeam);
console.log('Current team state:', currentTeam);
// Completely rewritten drag and drop for slot system
function initializeDragAndDrop() {{
console.log('Initializing drag and drop...');
// Make all pet cards draggable
document.querySelectorAll('.pet-card').forEach(card => {{
card.draggable = true;
card.style.cursor = 'grab';
card.addEventListener('dragstart', function(e) {{
console.log('DRAGSTART: Pet ID', this.dataset.petId);
draggedElement = this;
this.style.opacity = '0.5';
this.style.cursor = 'grabbing';
// Check if dataTransfer exists (it won't in synthetic events)
if (e.dataTransfer) {{
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', this.dataset.petId);
}} else {{
console.log('Warning: dataTransfer is null (synthetic event)');
}}
}});
card.addEventListener('dragend', function(e) {{
console.log('DRAGEND');
this.style.opacity = '';
this.style.cursor = 'grab';
draggedElement = null;
// Clear all highlights
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
}});
}});
// Set up team slot drop zones
teamSlots.forEach((slot, index) => {{
const position = index + 1;
slot.addEventListener('dragover', function(e) {{
e.preventDefault();
if (e.dataTransfer) {{
e.dataTransfer.dropEffect = 'move';
}}
}});
slot.addEventListener('dragenter', function(e) {{
e.preventDefault();
this.classList.add('drag-over');
console.log(`DRAGENTER: Team slot ${{position}}`);
}});
slot.addEventListener('dragleave', function(e) {{
if (!this.contains(e.relatedTarget)) {{
this.classList.remove('drag-over');
}}
}});
slot.addEventListener('drop', function(e) {{
e.preventDefault();
console.log(`DROP: Team slot ${{position}}`);
this.classList.remove('drag-over');
if (draggedElement) {{
const petId = draggedElement.dataset.petId;
console.log(`Moving pet ${{petId}} to team slot ${{position}}`);
movePetToTeamSlot(petId, position);
}}
}});
}});
// Set up storage drop zones
[storageContainer, storageDrop].forEach(zone => {{
if (zone) {{
zone.addEventListener('dragover', function(e) {{
e.preventDefault();
if (e.dataTransfer) {{
e.dataTransfer.dropEffect = 'move';
}}
}});
zone.addEventListener('dragenter', function(e) {{
e.preventDefault();
this.classList.add('drag-over');
console.log('DRAGENTER: Storage zone');
}});
zone.addEventListener('dragleave', function(e) {{
if (!this.contains(e.relatedTarget)) {{
this.classList.remove('drag-over');
}}
}});
zone.addEventListener('drop', function(e) {{
e.preventDefault();
console.log('DROP: Storage zone');
this.classList.remove('drag-over');
if (draggedElement) {{
const petId = draggedElement.dataset.petId;
console.log('Moving pet', petId, 'to storage');
movePetToStorage(petId);
}}
}});
}}
}});
console.log('Drag and drop initialization complete');
}}
function movePetToTeamSlot(petId, position) {{
console.log(`movePetToTeamSlot called for pet ${{petId}}, position ${{position}}`);
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
if (!card) {{
console.log(`No card found for pet ${{petId}}`);
return;
}}
const slot = document.getElementById(`slot-${{position}}`);
if (!slot) {{
console.log(`No slot found for position ${{position}}`);
return;
}}
const slotContent = slot.querySelector('.slot-content');
// Check if slot is already occupied
if (slotContent.children.length > 0) {{
console.log(`Slot ${{position}} is already occupied, swapping pets`);
const existingCard = slotContent.querySelector('.pet-card');
if (existingCard) {{
const existingPetId = existingCard.dataset.petId;
// Move existing pet to storage
movePetToStorage(existingPetId);
}}
}}
// Update state
currentTeam[petId] = position;
// Move DOM element
card.classList.remove('storage');
card.classList.add('active');
card.dataset.active = 'true';
card.dataset.teamOrder = position;
card.querySelector('.status-badge').textContent = 'Active';
slotContent.appendChild(card);
// Update interface
updateSaveButton();
console.log(`Pet moved to team slot ${{position}} successfully`);
}}
function movePetToStorage(petId) {{
console.log(`movePetToStorage called for pet ${{petId}}`);
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
if (!card) {{
console.log(`No card found for pet ${{petId}}`);
return;
}}
const currentPosition = currentTeam[petId];
console.log(`Pet ${{petId}} current state: ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`);
if (currentPosition) {{
console.log(`Moving pet ${{petId}} to storage...`);
// Update state
currentTeam[petId] = false;
// Move DOM element
card.classList.remove('active');
card.classList.add('storage');
card.dataset.active = 'false';
card.dataset.teamOrder = '';
card.querySelector('.status-badge').textContent = 'Storage';
storageContainer.appendChild(card);
// Update interface
updateSaveButton();
updateDropZoneVisibility();
console.log('Pet moved to storage successfully');
}} else {{
console.log(`Pet ${{petId}} is already in storage, no move needed`);
}}
}}
function updateDropZoneVisibility() {{
// Update storage drop zone visibility
if (storageContainer && storageContainer.children.length > 0) {{
if (storageDrop) storageDrop.classList.add('has-pets');
}} else {{
if (storageDrop) storageDrop.classList.remove('has-pets');
}}
console.log('Drop zone visibility updated:', {{
storageContainerPets: storageContainer ? storageContainer.children.length : 0
}});
}}
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 with debugging
console.log('Starting initialization...');
// Debug initial state (using previously declared variables)
console.log('Initial state:', {{
activePets: activeContainer.children.length,
storagePets: storageContainer.children.length
}});
initializeDragAndDrop();
addClickToMoveBackup(); // Add double-click as backup
updateSaveButton();
// Delay updateDropZoneVisibility to ensure DOM is fully settled
console.log('Before updateDropZoneVisibility...');
setTimeout(() => {{
console.log('Running delayed updateDropZoneVisibility...');
updateDropZoneVisibility();
}}, 100);
console.log('Initialization complete.');
// Test available via manual button only - no automatic execution
// Add test button for manual debugging
const testButton = document.createElement('button');
testButton.textContent = '🧪 Test Functions';
testButton.style.cssText = 'position: fixed; top: 10px; right: 10px; z-index: 9999; padding: 10px; background: #ff6b6b; color: white; border: none; border-radius: 5px; cursor: pointer;';
testButton.onclick = runDragDropTest;
document.body.appendChild(testButton);
// Add instruction for backup method
const instruction = document.createElement('div');
instruction.innerHTML = '💡 <strong>Backup:</strong> Double-click any pet to move it between Active/Storage';
instruction.style.cssText = 'position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.8); color: white; padding: 10px; border-radius: 5px; font-size: 12px; z-index: 9999; max-width: 250px;';
document.body.appendChild(instruction);
// 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!');
// Load existing configurations on page load
loadConfigurationList();
// Track which configuration is currently being edited
let activeConfigSlot = 'current';
let hasUnsavedChanges = false;
// Team Configuration Functions
async function saveConfig(slot) {{
const configName = prompt(`Enter a name for configuration slot ${{slot}}:`, `Team Config ${{slot}}`);
if (!configName) return;
try {{
const teamData = getCurrentTeamData();
const response = await fetch(`/teambuilder/{nickname}/config/save/${{slot}}`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
name: configName,
team: teamData
}})
}});
const result = await response.json();
if (result.success) {{
showMessage(`Configuration '${{configName}}' saved to slot ${{slot}}!`, 'success');
updateConfigSlot(slot, configName);
}} else {{
showMessage('Failed to save configuration: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
async function loadConfig(slot) {{
if (!confirm(`Load team configuration from slot ${{slot}}? This will replace your current team setup.`)) {{
return;
}}
try {{
const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{
method: 'POST'
}});
const result = await response.json();
if (result.success) {{
applyTeamConfiguration(result.team_data);
showMessage(`Team configuration '${{result.config_name}}' loaded!`, 'success');
}} else {{
showMessage('Failed to load configuration: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
function getCurrentTeamData() {{
const teamData = [];
// Get pets from team slots
for (let i = 1; i <= 6; i++) {{
const slot = document.getElementById(`slot-${{i}}`);
const petCard = slot.querySelector('.pet-card');
if (petCard) {{
teamData.push({{
pet_id: parseInt(petCard.dataset.petId),
position: i
}});
}}
}}
return teamData;
}}
function applyTeamConfiguration(teamData) {{
// Clear all current team positions
for (let i = 1; i <= 6; i++) {{
const slot = document.getElementById(`slot-${{i}}`);
const petCard = slot.querySelector('.pet-card');
if (petCard) {{
// Move pet back to storage
const storageContainer = document.getElementById('storage-container');
storageContainer.appendChild(petCard);
petCard.dataset.active = 'false';
petCard.classList.remove('active');
petCard.classList.add('storage');
}}
slot.querySelector('.slot-content').innerHTML = '';
}}
// Apply new team configuration
teamData.forEach(config => {{
const petCard = document.querySelector(`[data-pet-id="${{config.pet_id}}"]`);
if (petCard) {{
const targetSlot = document.getElementById(`slot-${{config.position}}`);
const slotContent = targetSlot.querySelector('.slot-content');
slotContent.appendChild(petCard);
petCard.dataset.active = 'true';
petCard.classList.remove('storage');
petCard.classList.add('active');
}}
}});
updateSaveButton();
updateDropZoneVisibility();
}}
async function loadConfigurationList() {{
// This would typically load from an API endpoint that lists all configs
// For now, we'll check each slot individually
for (let slot = 1; slot <= 3; slot++) {{
try {{
const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{
method: 'POST'
}});
if (response.ok) {{
const result = await response.json();
if (result.success) {{
updateConfigSlot(slot, result.config_name);
}}
}}
}} catch (error) {{
// Slot is empty, which is fine
}}
}}
}}
function updateConfigSlot(slot, configName) {{
const nameElement = document.getElementById(`config-name-${{slot}}`);
const loadButton = document.getElementById(`load-btn-${{slot}}`);
nameElement.textContent = configName;
nameElement.style.color = 'var(--text-primary)';
nameElement.style.fontStyle = 'normal';
loadButton.disabled = false;
// Update selector dropdown
const selectorElement = document.getElementById(`selector-name-${{slot}}`);
if (selectorElement) {{
selectorElement.textContent = configName;
}}
// Update option text in dropdown
const option = document.querySelector(`#active-config option[value="${{slot}}"]`);
if (option) {{
option.textContent = `Config ${{slot}}: ${{configName}}`;
}}
}}
async function switchActiveConfig() {{
const selector = document.getElementById('active-config');
const newSlot = selector.value;
// Check for unsaved changes
if (hasUnsavedChanges && activeConfigSlot !== 'current') {{
if (!confirm('You have unsaved changes. Do you want to switch configurations anyway?')) {{
selector.value = activeConfigSlot; // Revert selection
return;
}}
}}
activeConfigSlot = newSlot;
if (newSlot === 'current') {{
// Switch to current team (no loading)
updateConfigStatus('Editing current team (not saved to any configuration)');
document.getElementById('quick-save-btn').disabled = true;
}} else {{
// Load the selected configuration
try {{
const response = await fetch(`/teambuilder/{nickname}/config/load/${{newSlot}}`, {{
method: 'POST'
}});
const result = await response.json();
if (result.success) {{
applyTeamConfiguration(result.team_data);
updateConfigStatus(`Editing: ${{result.config_name}}`);
document.getElementById('quick-save-btn').disabled = false;
hasUnsavedChanges = false;
}} else {{
// Configuration doesn't exist, start editing a new one
updateConfigStatus(`Editing: Config ${{newSlot}} (new configuration)`);
document.getElementById('quick-save-btn').disabled = false;
hasUnsavedChanges = true;
}}
}} catch (error) {{
showMessage('Error loading configuration: ' + error.message, 'error');
selector.value = 'current';
activeConfigSlot = 'current';
}}
}}
}}
async function quickSaveConfig() {{
if (activeConfigSlot === 'current') return;
try {{
const teamData = getCurrentTeamData();
// Get existing config name or use default
let configName = `Team Config ${{activeConfigSlot}}`;
const existingConfig = await fetch(`/teambuilder/{nickname}/config/load/${{activeConfigSlot}}`, {{
method: 'POST'
}});
if (existingConfig.ok) {{
const result = await existingConfig.json();
if (result.success) {{
configName = result.config_name;
}}
}}
const response = await fetch(`/teambuilder/{nickname}/config/save/${{activeConfigSlot}}`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
name: configName,
team: teamData
}})
}});
const result = await response.json();
if (result.success) {{
showMessage(`Configuration '${{configName}}' saved!`, 'success');
updateConfigSlot(activeConfigSlot, configName);
hasUnsavedChanges = false;
updateConfigStatus(`Editing: ${{configName}} (saved)`);
}} else {{
showMessage('Failed to save configuration: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
function updateConfigStatus(statusText) {{
const statusElement = document.querySelector('#config-status .status-text');
statusElement.textContent = statusText;
}}
// Override existing functions to track changes
const originalUpdateSaveButton = updateSaveButton;
updateSaveButton = function() {{
originalUpdateSaveButton();
// Track changes for configurations
if (activeConfigSlot !== 'current') {{
hasUnsavedChanges = true;
updateConfigStatus(`Editing: Config ${{activeConfigSlot}} (unsaved changes)`);
}}
}}
// Load configuration to edit - loads config into current team without switching selector
async function loadConfigToEdit(slot) {{
if (!confirm(`Load Config ${{slot}} for editing? This will replace your current team setup.`)) {{
return;
}}
try {{
const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{
method: 'POST'
}});
const result = await response.json();
if (result.success) {{
applyTeamConfiguration(result.team_data);
// Keep the selector on "current" but show that we loaded a config
document.getElementById('active-config').value = 'current';
updateConfigStatus(`Loaded '${{result.config_name}}' for editing (unsaved changes)`);
hasUnsavedChanges = true;
updateSaveButton();
showMessage(`Config '${{result.config_name}}' loaded for editing!`, 'success');
}} else {{
showMessage('Failed to load configuration: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
// Rename current configuration
async function renameConfig() {{
const activeSlot = document.getElementById('active-config').value;
if (activeSlot === 'current') {{
showMessage('Please select a saved configuration to rename', 'error');
return;
}}
const currentName = document.getElementById(`selector-name-${{activeSlot}}`).textContent;
const newName = prompt('Enter new name for this configuration:', currentName === 'Empty Slot' ? '' : currentName);
if (newName === null || newName.trim() === '') {{
return; // User cancelled or entered empty name
}}
try {{
const response = await fetch(`/teambuilder/{nickname}/config/rename/${{activeSlot}}`, {{
method: 'POST',
headers: {{
'Content-Type': 'application/json'
}},
body: JSON.stringify({{
new_name: newName.trim()
}})
}});
const result = await response.json();
if (result.success) {{
// Update the display
document.getElementById(`selector-name-${{activeSlot}}`).textContent = newName.trim();
updateConfigStatus(`Editing: ${{newName.trim()}} (Config ${{activeSlot}})`);
showMessage(`Configuration renamed to '${{newName.trim()}}'!`, 'success');
}} else {{
showMessage('Failed to rename configuration: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
console.log('🐾 Team Builder initialized successfully!');
</script>
</body>
</html>"""
# Generate storage pets HTML first
storage_pets_html = ""
for pet in inactive_pets:
storage_pets_html += make_pet_card(pet, False)
# Generate active pets HTML for team slots
active_pets_html = ""
for pet in active_pets:
if pet.get('team_order'):
active_pets_html += make_pet_card(pet, True)
# Create content using string concatenation instead of f-strings to avoid CSS brace issues
team_builder_content = """
<style>
/* Team Builder Specific CSS */
.team-builder-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: var(--text-accent);
font-size: 2.5em;
margin: 0;
}
.team-sections {
margin-top: 30px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: var(--text-accent);
border-bottom: 2px solid var(--accent-blue);
padding-bottom: 10px;
margin-bottom: 20px;
}
.team-slots-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 15px;
margin-bottom: 30px;
justify-content: center;
}
.team-slot {
background: var(--bg-secondary);
border: 2px dashed var(--border-color);
border-radius: 12px;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
position: relative;
transition: all 0.3s ease;
}
.team-slot:hover {
border-color: var(--accent-blue);
background: var(--hover-color);
}
.team-slot.drag-over {
border-color: var(--text-accent);
background: var(--hover-color);
transform: scale(1.02);
}
.slot-header {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-weight: bold;
color: var(--text-accent);
font-size: 1.1em;
}
.slot-content {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.empty-slot {
color: var(--text-secondary);
font-style: italic;
text-align: center;
}
.storage-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.pet-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 15px;
cursor: move;
user-select: none;
transition: all 0.3s ease;
position: relative;
}
.pet-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
border-color: var(--accent-blue);
}
.pet-card.active {
border-color: var(--text-accent);
background: var(--bg-tertiary);
}
.pet-card.storage {
border-color: var(--border-color);
}
.pet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.pet-name {
color: var(--text-primary);
margin: 0;
font-size: 1.2em;
}
.status-badge {
background: var(--accent-blue);
color: white;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8em;
font-weight: bold;
}
.pet-card.active .status-badge {
background: var(--text-accent);
}
.pet-species, .pet-type {
color: var(--text-secondary);
margin: 5px 0;
}
.hp-section {
margin: 10px 0;
}
.hp-label {
font-size: 0.9em;
color: var(--text-secondary);
}
.hp-bar {
background: var(--bg-primary);
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 4px;
}
.hp-fill {
height: 100%;
transition: width 0.3s ease;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 10px;
}
.stat {
text-align: center;
}
.stat-label {
font-size: 0.7em;
color: var(--text-secondary);
display: block;
}
.stat-value {
font-weight: bold;
color: var(--text-primary);
}
.controls {
text-align: center;
margin: 30px 0;
}
.save-btn {
background: var(--text-accent);
color: var(--bg-primary);
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 15px;
}
.save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 255, 102, 0.3);
}
.save-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
}
.back-btn {
background: var(--accent-blue);
color: white;
padding: 12px 20px;
border-radius: 8px;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.back-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(74, 171, 247, 0.3);
}
.pin-section {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
margin: 20px 0;
text-align: center;
display: none;
}
.pin-input {
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 15px;
font-size: 1.5em;
color: var(--text-primary);
text-align: center;
width: 200px;
margin: 15px;
}
.verify-btn {
background: var(--success-color);
color: white;
border: none;
padding: 12px 25px;
border-radius: 8px;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s ease;
}
.verify-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(81, 207, 102, 0.3);
}
.message {
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
.message.success {
background: var(--success-color);
color: white;
}
.message.error {
background: var(--error-color);
color: white;
}
.drag-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.8);
color: white;
padding: 15px;
border-radius: 8px;
font-size: 12px;
z-index: 9999;
max-width: 250px;
}
@keyframes bounce {
0%, 20%, 60%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
80% { transform: translateY(-5px); }
}
/* Team Configuration CSS */
.config-selector {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.config-selector label {
color: var(--text-primary);
font-weight: 500;
}
.config-selector select {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--drag-hover);
border-radius: 8px;
padding: 8px 12px;
font-size: 1em;
min-width: 200px;
}
.quick-save-btn {
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s ease;
}
.quick-save-btn:hover:not(:disabled) {
background: #45a049;
transform: translateY(-2px);
}
.quick-save-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}
.rename-btn {
background: #FF9800;
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
margin-left: 10px;
transition: all 0.3s ease;
}
.rename-btn:hover:not(:disabled) {
background: #F57C00;
transform: translateY(-1px);
}
.rename-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}
.config-status {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px 15px;
border-left: 4px solid var(--text-accent);
margin-bottom: 15px;
}
.status-text {
color: var(--text-secondary);
font-style: italic;
}
.config-quick-actions {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
.config-action-btn {
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
min-width: 120px;
}
.config-action-btn:hover:not(:disabled) {
background: var(--secondary-color);
transform: translateY(-1px);
}
.config-action-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}
</style>
<div class="team-builder-container">
<div class="header">
<h1>🐾 Team Builder</h1>
<p>Drag pets between Active Team and Storage. Double-click as backup.</p>
</div>
<div class="team-sections">
<div class="section">
<h2>⚔️ Active Team (1-6 pets)</h2>
<div class="team-slots-container" id="team-slots-container">
<div class="team-slot" id="slot-1">
<div class="slot-header">Slot 1 (Leader)</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
<div class="team-slot" id="slot-2">
<div class="slot-header">Slot 2</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
<div class="team-slot" id="slot-3">
<div class="slot-header">Slot 3</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
<div class="team-slot" id="slot-4">
<div class="slot-header">Slot 4</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
<div class="team-slot" id="slot-5">
<div class="slot-header">Slot 5</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
<div class="team-slot" id="slot-6">
<div class="slot-header">Slot 6</div>
<div class="slot-content">
<div class="empty-slot">Drop pet here</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>📦 Storage</h2>
<div class="storage-container" id="storage-container">
""" + storage_pets_html + active_pets_html + """
</div>
</div>
</div>
<div class="section">
<h2>💾 Team Configurations</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">
Save up to 3 different team setups for quick switching between strategies
</p>
<div class="config-selector">
<label for="active-config">Choose configuration to edit:</label>
<select id="active-config" onchange="switchActiveConfig()">
<option value="current">Current Team (unsaved)</option>
<option value="1">Config 1: <span id="selector-name-1">Empty Slot</span></option>
<option value="2">Config 2: <span id="selector-name-2">Empty Slot</span></option>
<option value="3">Config 3: <span id="selector-name-3">Empty Slot</span></option>
</select>
<button class="quick-save-btn" id="quick-save-btn" onclick="quickSaveConfig()" disabled>💾 Quick Save</button>
<button class="rename-btn" id="rename-btn" onclick="renameConfig()" disabled>✏️ Rename</button>
</div>
<div class="config-status" id="config-status">
<span class="status-text">Editing current team (not saved to any configuration)</span>
</div>
<div class="config-quick-actions">
<button class="config-action-btn" onclick="loadConfigToEdit(1)" id="load-edit-1" disabled>
📂 Load Config 1
</button>
<button class="config-action-btn" onclick="loadConfigToEdit(2)" id="load-edit-2" disabled>
📂 Load Config 2
</button>
<button class="config-action-btn" onclick="loadConfigToEdit(3)" id="load-edit-3" disabled>
📂 Load Config 3
</button>
</div>
</div>
<div class="controls">
<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>
</div>
<div class="drag-hint">
💡 <strong>How to use:</strong><br>
• Drag pets to team slots<br>
• Double-click to move pets<br>
• Empty slots show placeholders
</div>
<script>
let originalTeam = {};
let currentTeam = {};
let draggedElement = null;
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('Team Builder: DOM loaded, initializing...');
initializeTeamBuilder();
});
function initializeTeamBuilder() {
console.log('Team Builder: Starting initialization...');
// Initialize team state
const allCards = document.querySelectorAll('.pet-card');
console.log(`Found ${allCards.length} pet cards`);
allCards.forEach(card => {
const petId = card.dataset.petId;
const isActive = card.dataset.active === 'true';
const teamOrder = card.dataset.teamOrder;
if (isActive && teamOrder) {
originalTeam[petId] = parseInt(teamOrder);
currentTeam[petId] = parseInt(teamOrder);
// Move to correct slot
const slot = document.getElementById(`slot-${teamOrder}`);
if (slot) {
const slotContent = slot.querySelector('.slot-content');
slotContent.innerHTML = '';
slotContent.appendChild(card);
}
} else {
originalTeam[petId] = false;
currentTeam[petId] = false;
}
});
console.log('Team state initialized:', originalTeam);
// Initialize drag and drop
initializeDragAndDrop();
// Initialize double-click backup
initializeDoubleClick();
// Update save button
updateSaveButton();
console.log('Team Builder: Initialization complete');
}
function initializeDragAndDrop() {
console.log('Initializing drag and drop...');
// Make pet cards draggable
document.querySelectorAll('.pet-card').forEach(card => {
card.draggable = true;
card.addEventListener('dragstart', function(e) {
draggedElement = this;
this.style.opacity = '0.5';
console.log('Drag started:', this.dataset.petId);
});
card.addEventListener('dragend', function(e) {
this.style.opacity = '';
draggedElement = null;
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
});
});
// Set up team slot drop zones
for (let i = 1; i <= 6; i++) {
const slot = document.getElementById(`slot-${i}`);
if (slot) {
slot.addEventListener('dragover', e => e.preventDefault());
slot.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
slot.addEventListener('dragleave', function(e) {
if (!this.contains(e.relatedTarget)) {
this.classList.remove('drag-over');
}
});
slot.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedElement) {
movePetToTeamSlot(draggedElement.dataset.petId, i);
}
});
}
}
// Set up storage drop zone
const storageContainer = document.getElementById('storage-container');
if (storageContainer) {
storageContainer.addEventListener('dragover', e => e.preventDefault());
storageContainer.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
storageContainer.addEventListener('dragleave', function(e) {
if (!this.contains(e.relatedTarget)) {
this.classList.remove('drag-over');
}
});
storageContainer.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedElement) {
movePetToStorage(draggedElement.dataset.petId);
}
});
}
}
function initializeDoubleClick() {
document.querySelectorAll('.pet-card').forEach(card => {
card.addEventListener('dblclick', function() {
const petId = this.dataset.petId;
const currentPosition = currentTeam[petId];
if (currentPosition) {
// Move to storage
movePetToStorage(petId);
} else {
// Find first empty slot
for (let i = 1; i <= 6; i++) {
const slot = document.getElementById(`slot-${i}`);
const slotContent = slot.querySelector('.slot-content');
if (slotContent.children.length === 0 || slotContent.querySelector('.empty-slot')) {
movePetToTeamSlot(petId, i);
return;
}
}
console.log('No empty slots available');
}
});
});
}
function movePetToTeamSlot(petId, position) {
console.log(`Moving pet ${petId} to slot ${position}`);
const card = document.querySelector(`[data-pet-id="${petId}"]`);
const slot = document.getElementById(`slot-${position}`);
if (!card || !slot) return;
const slotContent = slot.querySelector('.slot-content');
// If slot is occupied, move existing pet to storage
const existingCard = slotContent.querySelector('.pet-card');
if (existingCard) {
movePetToStorage(existingCard.dataset.petId);
}
// Update state
currentTeam[petId] = position;
// Update card
card.classList.remove('storage');
card.classList.add('active');
card.dataset.active = 'true';
card.dataset.teamOrder = position;
card.querySelector('.status-badge').textContent = 'Active';
// Move to slot
slotContent.innerHTML = '';
slotContent.appendChild(card);
updateSaveButton();
}
function movePetToStorage(petId) {
console.log(`Moving pet ${petId} to storage`);
const card = document.querySelector(`[data-pet-id="${petId}"]`);
const storageContainer = document.getElementById('storage-container');
if (!card || !storageContainer) return;
// Update state
currentTeam[petId] = false;
// Update card
card.classList.remove('active');
card.classList.add('storage');
card.dataset.active = 'false';
card.dataset.teamOrder = '';
card.querySelector('.status-badge').textContent = 'Storage';
// Move to storage
storageContainer.appendChild(card);
updateSaveButton();
}
function updateSaveButton() {
const saveBtn = document.getElementById('save-btn');
const hasChanges = JSON.stringify(originalTeam) !== JSON.stringify(currentTeam);
saveBtn.disabled = !hasChanges;
saveBtn.textContent = hasChanges ? '🔒 Save Team Changes' : '✅ No Changes';
}
async function saveTeam() {
const teamData = {};
Object.entries(currentTeam).forEach(([petId, position]) => {
teamData[petId] = position;
});
try {
const response = await fetch('/teambuilder/""" + nickname + """/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(teamData)
});
const result = await response.json();
if (result.success) {
document.getElementById('pin-section').style.display = 'block';
showMessage('PIN sent to IRC! Check your private messages.', 'success');
} else {
showMessage('Error: ' + result.error, 'error');
}
} catch (error) {
showMessage('Network error: ' + error.message, 'error');
}
}
async function verifyPin() {
const pin = document.getElementById('pin-input').value;
if (!pin || pin.length !== 6) {
showMessage('Please enter a 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 saved successfully!', 'success');
originalTeam = { ...currentTeam };
updateSaveButton();
document.getElementById('pin-section').style.display = 'none';
document.getElementById('pin-input').value = '';
// Celebration animation
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>`;
if (type === 'success') {
setTimeout(() => {
messageArea.innerHTML = '';
}, 5000);
}
}
// PIN input keyboard support
document.getElementById('pin-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
verifyPin();
}
});
</script>
"""
# Get the unified template
html_content = self.get_page_template(f"Team Builder - {nickname}", team_builder_content, "")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def handle_team_save(self, nickname):
"""Handle team save request and generate PIN"""
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"""
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"""
print(f"🔐 PIN for {nickname}: {pin_code}")
# Try to send via IRC bot if available
if self.bot and hasattr(self.bot, 'send_message_sync'):
try:
# Send PIN via private message using sync wrapper
self.bot.send_message_sync(nickname, f"🔐 Team Builder PIN: {pin_code}")
self.bot.send_message_sync(nickname, f"💡 Enter this PIN on the web page to confirm your team changes. PIN expires in 10 minutes.")
print(f"✅ PIN sent to {nickname} via IRC")
except Exception as e:
print(f"❌ Failed to send PIN via IRC: {e}")
else:
print(f"❌ No IRC bot available to send PIN to {nickname}")
print(f"💡 Manual PIN for {nickname}: {pin_code}")
def handle_team_config_save(self, nickname, slot):
"""Handle saving team configuration to a slot"""
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:
data = json.loads(post_data)
config_name = data.get("name", f"Team Config {slot}")
team_data = data.get("team", [])
except json.JSONDecodeError:
self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400)
return
# 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_config_save_async(nickname, slot_num, config_name, 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_config_save: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_team_config_save_async(self, nickname, slot_num, config_name, team_data):
"""Async handler for team configuration save"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Save configuration
import json
success = await self.database.save_team_configuration(
player["id"], slot_num, config_name, json.dumps(team_data)
)
if success:
return {"success": True, "message": f"Team configuration '{config_name}' saved to slot {slot_num}"}
else:
return {"success": False, "error": "Failed to save team configuration"}
except Exception as e:
print(f"Error in _handle_team_config_save_async: {e}")
return {"success": False, "error": str(e)}
def handle_team_config_load(self, nickname, slot):
"""Handle loading team configuration from a slot"""
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_config_load_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_config_load: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_team_config_load_async(self, nickname, slot_num):
"""Async handler for team configuration load"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Load configuration
config = await self.database.load_team_configuration(player["id"], slot_num)
if config:
import json
team_data = json.loads(config["team_data"])
return {
"success": True,
"config_name": config["config_name"],
"team_data": team_data,
"updated_at": config["updated_at"]
}
else:
return {"success": False, "error": f"No team configuration found in slot {slot_num}"}
except Exception as e:
print(f"Error in _handle_team_config_load_async: {e}")
return {"success": False, "error": str(e)}
def handle_team_config_rename(self, nickname, slot):
"""Handle renaming team configuration in a slot"""
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
# Get the new name from request body
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
import json
try:
data = json.loads(post_data.decode('utf-8'))
new_name = data.get('new_name', '').strip()
if not new_name:
self.send_json_response({"success": False, "error": "Configuration name cannot be empty"}, 400)
return
if len(new_name) > 50:
self.send_json_response({"success": False, "error": "Configuration name too long (max 50 characters)"}, 400)
return
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_config_rename_async(nickname, slot_num, new_name))
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_config_rename: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_team_config_rename_async(self, nickname, slot_num, new_name):
"""Async handler for team configuration rename"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Check if configuration exists in the slot
existing_config = await self.database.load_team_configuration(player["id"], slot_num)
if not existing_config:
return {"success": False, "error": f"No team configuration found in slot {slot_num}"}
# Rename the configuration
success = await self.database.rename_team_configuration(player["id"], slot_num, new_name)
if success:
return {
"success": True,
"message": f"Configuration renamed to '{new_name}'",
"new_name": new_name
}
else:
return {"success": False, "error": "Failed to rename configuration"}
except Exception as e:
print(f"Error in _handle_team_config_rename_async: {e}")
return {"success": False, "error": str(e)}
class PetBotWebServer:
"""Standalone web server for PetBot"""
def __init__(self, database=None, port=8080, bot=None):
self.database = database or Database()
self.port = port
self.bot = bot
self.server = None
def run(self):
"""Start the web server"""
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
self.server.database = self.database
self.server.bot = self.bot
print(f'🌐 Starting PetBot web server on http://0.0.0.0:{self.port}')
print(f'📡 Accessible from WSL at: http://172.27.217.61:{self.port}')
print(f'📡 Accessible from Windows at: http://localhost:{self.port}')
print('')
print('🌐 Public access at: http://petz.rdx4.com/')
print('')
self.server.serve_forever()
def start_in_thread(self):
"""Start the web server in a background thread"""
import threading
self.thread = threading.Thread(target=self.run, daemon=True)
self.thread.start()
def stop(self):
"""Stop the web server"""
if self.server:
self.server.shutdown()
self.server.server_close()
def run_standalone():
"""Run the web server in standalone mode"""
import sys
port = 8080
if len(sys.argv) > 1:
try:
port = int(sys.argv[1])
except ValueError:
print('Usage: python webserver.py [port]')
sys.exit(1)
server = PetBotWebServer(port)
print('🌐 PetBot Web Server')
print('=' * 50)
print(f'Port: {port}')
print('')
print('🔗 Local URLs:')
print(f' http://localhost:{port}/ - Game Hub (local)')
print(f' http://localhost:{port}/help - Command Help (local)')
print(f' http://localhost:{port}/players - Player List (local)')
print(f' http://localhost:{port}/leaderboard - Leaderboard (local)')
print(f' http://localhost:{port}/locations - Locations (local)')
print('')
print('🌐 Public URLs:')
print(' http://petz.rdx4.com/ - Game Hub')
print(' http://petz.rdx4.com/help - Command Help')
print(' http://petz.rdx4.com/players - Player List')
print(' http://petz.rdx4.com/leaderboard - Leaderboard')
print(' http://petz.rdx4.com/locations - Locations')
print('')
print('Press Ctrl+C to stop')
try:
server.run()
except KeyboardInterrupt:
print('\n✅ Web server stopped')
if __name__ == '__main__':
run_standalone()