Petbot/webserver.py
megaproxy 285a7c4a7e Complete team builder enhancement with active team display and pet counts
- Fix pet count display for all saved teams (handles both list and dict formats)
- Add comprehensive active team display with individual pet cards on hub
- Show detailed pet information: stats, HP bars, happiness, types, levels
- Implement responsive grid layout for active pet cards with hover effects
- Add proper data format handling between active and saved teams
- Create dedicated team hub with both overview and detailed sections
- Standardize team data pipeline for consistent display across all interfaces

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

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

11580 lines
444 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
import math
# 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"""
# Class-level admin sessions storage
admin_sessions = {}
@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;
border-radius: 0 0 15px 15px;
}
.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;
}
/* IV Display Styles */
.iv-section {
margin-top: 15px;
padding: 12px;
background: var(--bg-primary);
border-radius: 8px;
border: 1px solid var(--border-color);
}
.iv-title {
font-size: 0.9em;
font-weight: bold;
color: var(--text-accent);
margin-bottom: 8px;
text-align: center;
}
.iv-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-bottom: 8px;
}
.iv-stat {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8em;
padding: 2px 0;
}
.iv-value {
font-weight: bold;
padding: 2px 6px;
border-radius: 4px;
min-width: 28px;
text-align: center;
}
.iv-perfect { background: #4caf50; color: white; }
.iv-excellent { background: #2196f3; color: white; }
.iv-good { background: #ff9800; color: white; }
.iv-fair { background: #ff5722; color: white; }
.iv-poor { background: #607d8b; color: white; }
.iv-total {
text-align: center;
font-size: 0.85em;
padding-top: 8px;
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
}
.iv-total-value {
font-weight: bold;
color: var(--text-accent);
}
/* 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;
}
.iv-grid {
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?sort=type", "🔷 by Type"),
("petdex?sort=rarity", "⭐ by Rarity"),
("petdex?sort=name", "🔤 by Name"),
("petdex?sort=location", "🗺️ by Location"),
("petdex?sort=all", "📋 Show All"),
("petdex#search", "🔍 Search")
]),
("help", "📖 Help", [
("help", "📋 Commands"),
("faq", "❓ 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"""
print(f"GET request: {self.path}")
# 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
print(f"Parsed path: {path}")
# Route handling
if path == '/':
self.serve_index()
elif path == '/help':
self.serve_help()
elif path == '/faq':
self.serve_faq()
elif path == '/players':
self.serve_players()
elif path.startswith('/player/') and path.endswith('/pets'):
# Handle /player/{nickname}/pets - must come before general /player/ route
nickname = path[8:-5] # Remove '/player/' prefix and '/pets' suffix
print(f"DEBUG: Route matched! Parsed nickname from path '{path}' as '{nickname}'")
self.serve_player_pets(nickname)
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/') 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 '/team/' in path:
# Handle individual team editor: /teambuilder/{nickname}/team/{slot}
parts = path.split('/')
if len(parts) >= 5:
nickname = parts[2]
team_identifier = parts[4] # Could be 1, 2, 3, or 'active'
self.serve_individual_team_editor(nickname, team_identifier)
else:
self.send_error(400, "Invalid team editor path")
elif path.startswith('/teambuilder/'):
# Check if it's just the base teambuilder path (hub)
path_parts = path[13:].split('/') # Remove '/teambuilder/' prefix
if len(path_parts) == 1 and path_parts[0]: # Just nickname
nickname = path_parts[0]
self.serve_team_selection_hub(nickname)
else:
self.send_error(404, "Invalid teambuilder path")
elif path.startswith('/testteambuilder/'):
nickname = path[17:] # Remove '/testteambuilder/' prefix
self.serve_test_teambuilder(nickname)
elif path == '/admin':
self.serve_admin_login()
elif path == '/admin/dashboard':
self.serve_admin_dashboard()
elif path == '/admin/auth':
self.handle_admin_auth()
elif path == '/admin/verify':
self.handle_admin_verify()
elif path.startswith('/admin/api/'):
print(f"Admin API path detected in GET: {path}")
print(f"Extracted endpoint: {path[11:]}")
self.handle_admin_api(path[11:]) # Remove '/admin/api/' prefix
else:
print(f"No route found for path: {path}")
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 exceeded"}, 429)
return
parsed_path = urlparse(self.path)
path = parsed_path.path
if path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'):
# Handle individual team save: /teambuilder/{nickname}/team/{slot}/save
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
team_slot = parts[4]
self.handle_individual_team_save(nickname, team_slot)
else:
self.send_error(400, "Invalid individual team save path")
elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'):
# Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
team_slot = parts[4]
self.handle_individual_team_verify(nickname, team_slot)
else:
self.send_error(400, "Invalid individual team verify path")
elif 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('/testteambuilder/') and path.endswith('/save'):
nickname = path[17:-5] # Remove '/testteambuilder/' prefix and '/save' suffix
self.handle_test_team_save(nickname)
elif path.startswith('/testteambuilder/') and path.endswith('/verify'):
nickname = path[17:-7] # Remove '/testteambuilder/' prefix and '/verify' suffix
self.handle_test_team_verify(nickname)
elif path.startswith('/player/') and '/pets/rename' in path:
# Handle pet rename request: /player/{nickname}/pets/rename
nickname = path.split('/')[2]
self.handle_pet_rename_request(nickname)
elif path.startswith('/player/') and '/pets/verify' in path:
# Handle pet rename PIN verification: /player/{nickname}/pets/verify
nickname = path.split('/')[2]
self.handle_pet_rename_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")
elif path.startswith('/teambuilder/') and '/config/apply/' in path:
# Handle team configuration apply: /teambuilder/{nickname}/config/apply/{slot}
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
slot = parts[5]
self.handle_team_config_apply(nickname, slot)
else:
self.send_error(400, "Invalid configuration apply path")
elif path.startswith('/teambuilder/') and '/swap/' in path:
# Handle team swapping: /teambuilder/{nickname}/swap/{slot}
parts = path.split('/')
if len(parts) >= 5:
nickname = parts[2]
slot = parts[4]
self.handle_team_swap_request(nickname, slot)
else:
self.send_error(400, "Invalid team swap path")
elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'):
# Handle individual team save: /teambuilder/{nickname}/team/{slot}/save
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
team_slot = parts[4]
self.handle_individual_team_save(nickname, team_slot)
else:
self.send_error(400, "Invalid individual team save path")
elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'):
# Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify
parts = path.split('/')
if len(parts) >= 6:
nickname = parts[2]
team_slot = parts[4]
self.handle_individual_team_verify(nickname, team_slot)
else:
self.send_error(400, "Invalid individual team verify path")
elif path == '/admin/auth':
self.handle_admin_auth()
elif path == '/admin/verify':
self.handle_admin_verify()
elif path.startswith('/admin/api/'):
print(f"Admin API path detected: {path}")
print(f"Extracted endpoint: {path[11:]}")
self.handle_admin_api(path[11:]) # Remove '/admin/api/' prefix
else:
print(f"No route found for path: {path}")
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);
}
"""
# Load help.html content and extract both CSS and body content
try:
with open('help.html', 'r', encoding='utf-8') as f:
help_content = f.read()
import re
# Extract CSS from help.html
css_match = re.search(r'<style[^>]*>(.*?)</style>', help_content, re.DOTALL)
help_css = css_match.group(1) if css_match else ""
# Extract body content (everything between <body> tags)
body_match = re.search(r'<body[^>]*>(.*?)</body>', help_content, re.DOTALL)
if body_match:
body_content = body_match.group(1)
# Remove the back link since we'll have the navigation bar
body_content = re.sub(r'<a href="/" class="back-link">.*?</a>', '', body_content, flags=re.DOTALL)
else:
# Fallback: use original content if we can't parse it
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(help_content.encode())
return
# Create template with merged CSS
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Help & Commands</title>
<style>
{self.get_unified_css()}
/* Help page specific styles */
{help_css}
</style>
</head>
<body>
{self.get_navigation_bar("help")}
<div class="main-container">
{body_content}
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
except FileNotFoundError:
self.serve_error_page("Help", "Help file not found")
except Exception as e:
self.serve_error_page("Help", f"Error loading help file: {str(e)}")
def serve_faq(self):
"""Serve the FAQ page using unified template"""
try:
with open('faq.html', 'r', encoding='utf-8') as f:
faq_content = f.read()
import re
# Extract CSS from faq.html
css_match = re.search(r'<style[^>]*>(.*?)</style>', faq_content, re.DOTALL)
faq_css = css_match.group(1) if css_match else ""
# Extract body content (everything between <body> tags)
body_match = re.search(r'<body[^>]*>(.*?)</body>', faq_content, re.DOTALL)
if body_match:
body_content = body_match.group(1)
# Remove the back link since we'll have the navigation bar
body_content = re.sub(r'<a href="/" class="back-link">.*?</a>', '', body_content, flags=re.DOTALL)
else:
# Fallback: use original content if we can't parse it
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(faq_content.encode())
return
# Create template with merged CSS
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - FAQ</title>
<style>
{self.get_unified_css()}
/* FAQ page specific styles */
{faq_css}
</style>
</head>
<body>
{self.get_navigation_bar("faq")}
<div class="main-container">
{body_content}
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
except FileNotFoundError:
self.serve_error_page("FAQ", "FAQ file not found")
except Exception as e:
self.serve_error_page("FAQ", f"Error loading FAQ file: {str(e)}")
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))
player_locations = loop.run_until_complete(self.fetch_player_locations(database))
loop.close()
self.serve_locations_data(locations_data, player_locations)
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 []
async def fetch_player_locations(self, database):
"""Fetch player locations for the interactive map"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT p.nickname, p.current_location_id, l.name as location_name
FROM players p
JOIN locations l ON p.current_location_id = l.id
ORDER BY p.nickname
""")
rows = await cursor.fetchall()
players = []
for row in rows:
player_dict = {
'nickname': row['nickname'],
'location_id': row['current_location_id'],
'location_name': row['location_name']
}
players.append(player_dict)
return players
except Exception as e:
print(f"Database error fetching player locations: {e}")
return []
def create_interactive_map(self, locations_data, player_locations):
"""Create an interactive SVG map showing player locations"""
if not locations_data:
return ""
# Define map layout - create a unique visual design
map_positions = {
1: {"x": 200, "y": 400, "shape": "circle", "color": "#4CAF50"}, # Starter Town - central
2: {"x": 100, "y": 200, "shape": "hexagon", "color": "#2E7D32"}, # Whispering Woods - forest
3: {"x": 400, "y": 150, "shape": "diamond", "color": "#FF9800"}, # Thunder Peaks - mountain
4: {"x": 550, "y": 300, "shape": "octagon", "color": "#795548"}, # Stone Caverns - cave
5: {"x": 300, "y": 500, "shape": "star", "color": "#2196F3"}, # Frozen Lake - ice
6: {"x": 500, "y": 450, "shape": "triangle", "color": "#F44336"} # Volcanic Crater - fire
}
# Create player location groups
location_players = {}
for player in player_locations or []:
loc_id = player['location_id']
if loc_id not in location_players:
location_players[loc_id] = []
location_players[loc_id].append(player['nickname'])
# SVG map content
svg_content = ""
# Add connecting paths between locations
paths = [
(1, 2), (1, 3), (1, 5), # Starter Town connections
(2, 5), (3, 4), (4, 6), (5, 6) # Other connections
]
for start, end in paths:
if start in map_positions and end in map_positions:
start_pos = map_positions[start]
end_pos = map_positions[end]
svg_content += f"""
<line x1="{start_pos['x']}" y1="{start_pos['y']}"
x2="{end_pos['x']}" y2="{end_pos['y']}"
stroke="#444" stroke-width="2" stroke-dasharray="5,5" opacity="0.6"/>
"""
# Add location shapes
for location in locations_data:
loc_id = location['id']
if loc_id not in map_positions:
continue
pos = map_positions[loc_id]
players_here = location_players.get(loc_id, [])
player_count = len(players_here)
# Create shape based on type
shape_svg = self.create_location_shape(pos, location, player_count)
svg_content += shape_svg
# Add location label
svg_content += f"""
<text x="{pos['x']}" y="{pos['y'] - 45}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="14"
font-weight="bold"
fill="white">
{location['name']}
</text>
"""
# Add player names if any
if players_here:
player_text = ", ".join(players_here)
svg_content += f"""
<text x="{pos['x']}" y="{pos['y'] + 50}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="12"
fill="#FFD700">
{player_text}
</text>
"""
return f"""
<div class="map-section">
<h2>🗺️ Interactive World Map</h2>
<p style="text-align: center; color: var(--text-secondary); margin-bottom: 20px;">
Current player locations - shapes represent different terrain types
</p>
<div class="map-container">
<svg width="700" height="600" viewBox="0 0 700 600" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);">
{svg_content}
</svg>
</div>
<div class="map-legend">
<div class="legend-item">
<div class="legend-shape circle" style="background: #4CAF50;"></div>
<span>Towns</span>
</div>
<div class="legend-item">
<div class="legend-shape hexagon" style="background: #2E7D32;"></div>
<span>Forests</span>
</div>
<div class="legend-item">
<div class="legend-shape diamond" style="background: #FF9800;"></div>
<span>Mountains</span>
</div>
<div class="legend-item">
<div class="legend-shape octagon" style="background: #795548;"></div>
<span>Caves</span>
</div>
<div class="legend-item">
<div class="legend-shape star" style="background: #2196F3;"></div>
<span>Ice Areas</span>
</div>
<div class="legend-item">
<div class="legend-shape triangle" style="background: #F44336;"></div>
<span>Volcanic</span>
</div>
</div>
</div>
"""
def create_location_shape(self, pos, location, player_count):
"""Create SVG shape for a location based on its type"""
x, y = pos['x'], pos['y']
color = pos['color']
shape = pos['shape']
# Add glow effect if players are present
glow = 'filter="url(#glow)"' if player_count > 0 else ''
# Base size with scaling for player count
base_size = 25 + (player_count * 3)
if shape == "circle":
return f"""
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<circle cx="{x}" cy="{y}" r="{base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "hexagon":
points = []
for i in range(6):
angle = i * 60 * math.pi / 180
px = x + base_size * math.cos(angle)
py = y + base_size * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "diamond":
return f"""
<polygon points="{x},{y-base_size} {x+base_size},{y} {x},{y+base_size} {x-base_size},{y}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "triangle":
return f"""
<polygon points="{x},{y-base_size} {x+base_size},{y+base_size} {x-base_size},{y+base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "star":
# Create 5-pointed star
points = []
for i in range(10):
angle = i * 36 * math.pi / 180
radius = base_size if i % 2 == 0 else base_size * 0.5
px = x + radius * math.cos(angle)
py = y + radius * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "octagon":
points = []
for i in range(8):
angle = i * 45 * math.pi / 180
px = x + base_size * math.cos(angle)
py = y + base_size * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
else:
# Default to circle
return f"""
<circle cx="{x}" cy="{y}" r="{base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
def serve_locations_data(self, locations_data, player_locations=None):
"""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>"""
# Create interactive map HTML
map_html = self.create_interactive_map(locations_data, player_locations)
content = f"""
<div class="header">
<h1>🗺️ Game Locations</h1>
<p>Explore all areas and discover what pets await you!</p>
</div>
{map_html}
<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 = """
.map-section {
background: var(--bg-secondary);
border-radius: 15px;
padding: 30px;
margin: 30px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.map-section h2 {
color: var(--text-accent);
text-align: center;
margin-bottom: 10px;
}
.map-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.map-container svg {
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
max-width: 100%;
height: auto;
}
.map-legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-size: 0.9em;
}
.legend-shape {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid white;
}
.legend-shape.circle {
border-radius: 50%;
}
.legend-shape.hexagon {
border-radius: 3px;
transform: rotate(45deg);
}
.legend-shape.diamond {
transform: rotate(45deg);
}
.legend-shape.triangle {
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
.legend-shape.star {
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.legend-shape.octagon {
clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);
}
.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
# Parse URL parameters for sorting
parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
sort_mode = query_params.get('sort', ['rarity'])[0] # Default to rarity
search_query = query_params.get('search', [''])[0] # Default to empty search
# 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, sort_mode, search_query)
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 remove_pet_duplicates(self, pets_list):
"""Remove duplicate pets based on ID and sort by name"""
seen_ids = set()
unique_pets = []
for pet in pets_list:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
return sorted(unique_pets, key=lambda x: x['name'])
def serve_petdex_data(self, petdex_data, sort_mode='rarity', search_query=''):
"""Serve petdex page with all pet species data"""
# Remove duplicates from input data first
petdex_data = self.remove_pet_duplicates(petdex_data)
# Apply search filter if provided
if search_query:
search_query = search_query.lower()
filtered_data = []
for pet in petdex_data:
# Search in name, type1, type2
if (search_query in pet['name'].lower() or
search_query in pet['type1'].lower() or
(pet['type2'] and search_query in pet['type2'].lower())):
filtered_data.append(pet)
petdex_data = filtered_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>
"""
# Sort and group pets based on sort_mode
petdex_html = ""
total_species = len(petdex_data)
if sort_mode == 'type':
# Group by type1
pets_by_type = {}
for pet in petdex_data:
pet_type = pet['type1']
if pet_type not in pets_by_type:
pets_by_type[pet_type] = []
# Check for duplicates within this type
if pet not in pets_by_type[pet_type]:
pets_by_type[pet_type].append(pet)
# Sort each type group by name and remove any remaining duplicates
for type_name in pets_by_type:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_type[type_name]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_type[type_name] = sorted(unique_pets, key=lambda x: x['name'])
type_colors = {
'Fire': '#F08030', 'Water': '#6890F0', 'Grass': '#78C850', 'Electric': '#F8D030',
'Psychic': '#F85888', 'Ice': '#98D8D8', 'Dragon': '#7038F8', 'Dark': '#705848',
'Fighting': '#C03028', 'Poison': '#A040A0', 'Ground': '#E0C068', 'Flying': '#A890F0',
'Bug': '#A8B820', 'Rock': '#B8A038', 'Ghost': '#705898', 'Steel': '#B8B8D0',
'Normal': '#A8A878', 'Fairy': '#EE99AC'
}
for type_name in sorted(pets_by_type.keys()):
pets_in_type = pets_by_type[type_name]
type_color = type_colors.get(type_name, '#A8A878')
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: {type_color}; border-bottom: 2px solid {type_color}; padding-bottom: 10px;">
{type_name} Type ({len(pets_in_type)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_in_type:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {type_color};">
<div class="pet-header">
<h3 style="color: {type_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-rarity">
<span style="color: {rarity_colors.get(pet['rarity'], '#ffffff')};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'name':
# Sort alphabetically by name (duplicates already removed)
sorted_pets = sorted(petdex_data, key=lambda x: x['name'])
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: var(--text-accent); border-bottom: 2px solid var(--text-accent); padding-bottom: 10px;">
All Species (A-Z) ({len(sorted_pets)} total)
</h2>
<div class="pets-grid">"""
for pet in sorted_pets:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
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-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-rarity">
<span style="color: {rarity_color};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'location':
# Group by spawn locations
pets_by_location = {}
pets_no_location = []
for pet in petdex_data:
if pet['spawn_locations']:
for location in pet['spawn_locations']:
loc_name = location['location_name']
if loc_name not in pets_by_location:
pets_by_location[loc_name] = []
# Check for duplicates within this location
if pet not in pets_by_location[loc_name]:
pets_by_location[loc_name].append(pet)
else:
pets_no_location.append(pet)
# Sort each location group by name and remove any remaining duplicates
for location_name in pets_by_location:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_location[location_name]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_location[location_name] = sorted(unique_pets, key=lambda x: x['name'])
location_colors = {
'Starter Town': '#4CAF50',
'Whispering Woods': '#2E7D32',
'Thunder Peaks': '#FF9800',
'Stone Caverns': '#795548',
'Frozen Lake': '#2196F3',
'Volcanic Crater': '#F44336'
}
for location_name in sorted(pets_by_location.keys()):
pets_in_location = pets_by_location[location_name]
location_color = location_colors.get(location_name, '#A8A878')
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: {location_color}; border-bottom: 2px solid {location_color}; padding-bottom: 10px;">
🗺️ {location_name} ({len(pets_in_location)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_in_location:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
# Get level range for this location
level_range = ""
for location in pet['spawn_locations']:
if location['location_name'] == location_name:
level_range = f"Lv.{location['min_level']}-{location['max_level']}"
break
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {location_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-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: {location_color};">
📍 {level_range} | {'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
# Add pets with no location at the end (remove duplicates)
if pets_no_location:
seen_ids = set()
unique_no_location = []
for pet in pets_no_location:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_no_location.append(pet)
pets_no_location = sorted(unique_no_location, key=lambda x: x['name'])
if pets_no_location:
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: #888; border-bottom: 2px solid #888; padding-bottom: 10px;">
❓ Unknown Locations ({len(pets_no_location)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_no_location:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid #888;">
<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-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: #888;">
❓ Location Unknown | {'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'all':
# Show all pets in a grid format without grouping (duplicates already removed)
sorted_pets = sorted(petdex_data, key=lambda x: (x['rarity'], x['name']))
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: var(--text-accent); border-bottom: 2px solid var(--text-accent); padding-bottom: 10px;">
📋 All Pet Species ({len(sorted_pets)} total)
</h2>
<div class="pets-grid">"""
for pet in sorted_pets:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
# Get all spawn locations
location_text = ""
if pet['spawn_locations']:
locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})"
for loc in pet['spawn_locations'][:2]]
if len(pet['spawn_locations']) > 2:
locations.append(f"+{len(pet['spawn_locations']) - 2} more")
location_text = f"📍 {', '.join(locations)}"
else:
location_text = "📍 Location Unknown"
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-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: var(--text-secondary); font-size: 0.9em;">
{location_text}
</span>
</div>
<div class="pet-rarity">
<span style="color: {rarity_color};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
else: # Default to rarity sorting
pets_by_rarity = {}
for pet in petdex_data:
rarity = pet['rarity']
if rarity not in pets_by_rarity:
pets_by_rarity[rarity] = []
# Check for duplicates within this rarity
if pet not in pets_by_rarity[rarity]:
pets_by_rarity[rarity].append(pet)
# Sort each rarity group by name and remove any remaining duplicates
for rarity in pets_by_rarity:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_rarity[rarity]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_rarity[rarity] = sorted(unique_pets, key=lambda x: x['name'])
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>"""
# Create search interface
search_interface = f"""
<div class="card" id="search">
<h2>🔍 Search & Filter</h2>
<form method="GET" action="/petdex" style="margin-bottom: 20px;">
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="text" name="search" placeholder="Search pets by name or type..."
value="{search_query}" style="flex: 1; min-width: 200px; padding: 8px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary);">
<select name="sort" style="padding: 8px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary);">
<option value="rarity"{'selected' if sort_mode == 'rarity' else ''}>🌟 By Rarity</option>
<option value="type"{'selected' if sort_mode == 'type' else ''}>🔷 By Type</option>
<option value="name"{'selected' if sort_mode == 'name' else ''}>🔤 By Name</option>
<option value="location"{'selected' if sort_mode == 'location' else ''}>🗺️ By Location</option>
<option value="all"{'selected' if sort_mode == 'all' else ''}>📋 Show All</option>
</select>
<button type="submit" style="padding: 8px 16px; background: var(--accent-blue); color: white; border: none; border-radius: 5px; cursor: pointer;">Search</button>
</div>
</form>
{f'<p style="color: var(--text-accent);">🔍 Found {len(petdex_data)} pets matching "{search_query}"</p>' if search_query else ''}
</div>
"""
# Determine header text based on sort mode
if sort_mode == 'type':
header_text = "📊 Pet Collection by Type"
description = "🔷 Pets are organized by their primary type. Each type has different strengths and weaknesses!"
elif sort_mode == 'name':
header_text = "📊 Pet Collection (A-Z)"
description = "🔤 All pets sorted alphabetically by name. Perfect for finding specific species!"
elif sort_mode == 'location':
header_text = "📊 Pet Collection by Location"
description = "🗺️ Pets are organized by where they can be found. Use <code>!travel &lt;location&gt;</code> to visit these areas!"
elif sort_mode == 'all':
header_text = "📊 Complete Pet Collection"
description = "📋 All pets displayed in a comprehensive grid view with locations and stats!"
else:
header_text = "📊 Pet Collection by Rarity"
description = "🌟 Pets are organized by rarity. Use <code>!wild &lt;location&gt;</code> in #petz to see what spawns where!"
# Combine all content
content = f"""
<div class="header">
<h1>📖 Petdex</h1>
<p>Complete encyclopedia of all available pets</p>
</div>
{stats_content}
{search_interface}
<div class="card">
<h2>{header_text}</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">{description}</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:
db.row_factory = aiosqlite.Row
# 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 Row to dict
player_dict = dict(player)
# 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 = [dict(row) for row in pets_rows]
# 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 = [dict(row) for row in achievements_rows]
# 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 = [dict(row) for row in inventory_rows]
# 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 = [dict(row) for row in gym_badges_rows]
# 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_pets(self, nickname):
"""Serve pet management page for a player"""
try:
print(f"DEBUG: serve_player_pets called with nickname: '{nickname}'")
# Get player data using database method directly
player = asyncio.run(self.database.get_player(nickname))
print(f"DEBUG: Player result: {player}")
if not player:
print(f"DEBUG: Player not found for: '{nickname}'")
self.serve_player_not_found(nickname)
return
# Get player pets for management
player_pets = asyncio.run(self.database.get_player_pets_for_rename(player['id']))
# Render the pets management page
self.serve_pets_management_interface(nickname, player_pets)
except Exception as e:
print(f"Error serving player pets page: {e}")
self.serve_player_error(nickname, str(e))
def get_iv_grade(self, iv_value):
"""Get color grade for IV value"""
if iv_value >= 27: # 27-31 (Perfect)
return "perfect"
elif iv_value >= 21: # 21-26 (Excellent)
return "excellent"
elif iv_value >= 15: # 15-20 (Good)
return "good"
elif iv_value >= 10: # 10-14 (Fair)
return "fair"
else: # 0-9 (Poor)
return "poor"
def serve_pets_management_interface(self, nickname, pets):
"""Serve the pet management interface"""
if not pets:
self.serve_no_pets_error(nickname)
return
# Generate pet cards
pet_cards = []
for pet in pets:
status_badge = ""
if pet.get('is_active'):
team_order = pet.get('team_order', 0)
if team_order > 0:
status_badge = f'<span class="team-badge">Team #{team_order}</span>'
else:
status_badge = '<span class="active-badge">Active</span>'
else:
status_badge = '<span class="storage-badge">Storage</span>'
fainted_badge = ""
if pet.get('fainted_at'):
fainted_badge = '<span class="fainted-badge">💀 Fainted</span>'
current_name = pet.get('nickname') or pet.get('species_name')
pet_id = pet.get('id')
pet_card = f"""
<div class="pet-card" data-pet-id="{pet_id}">
<div class="pet-header">
<div class="pet-info">
<div class="pet-name">{pet.get('emoji', '🐾')} {current_name}</div>
<div class="pet-species">Level {pet.get('level', 1)} {pet.get('species_name')}</div>
</div>
<div class="pet-badges">
{status_badge}
{fainted_badge}
</div>
</div>
<div class="pet-stats">
<div class="stat-group">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet.get('hp', 0)}/{pet.get('max_hp', 0)}</span>
</div>
<div class="stat-group">
<span class="stat-label">ATK:</span>
<span class="stat-value">{pet.get('attack', 0)}</span>
</div>
<div class="stat-group">
<span class="stat-label">DEF:</span>
<span class="stat-value">{pet.get('defense', 0)}</span>
</div>
<div class="stat-group">
<span class="stat-label">SPD:</span>
<span class="stat-value">{pet.get('speed', 0)}</span>
</div>
</div>
<div class="pet-ivs">
<div class="iv-header">
<span class="iv-title">Individual Values (IVs)</span>
<span class="iv-help" title="IVs determine how strong your pet's stats can become. Higher is better! (0-31)"></span>
</div>
<div class="iv-grid">
<div class="iv-stat">
<span class="iv-label">HP:</span>
<span class="iv-value iv-{self.get_iv_grade(pet.get('iv_hp', 15))}">{pet.get('iv_hp', 15)}</span>
</div>
<div class="iv-stat">
<span class="iv-label">ATK:</span>
<span class="iv-value iv-{self.get_iv_grade(pet.get('iv_attack', 15))}">{pet.get('iv_attack', 15)}</span>
</div>
<div class="iv-stat">
<span class="iv-label">DEF:</span>
<span class="iv-value iv-{self.get_iv_grade(pet.get('iv_defense', 15))}">{pet.get('iv_defense', 15)}</span>
</div>
<div class="iv-stat">
<span class="iv-label">SPD:</span>
<span class="iv-value iv-{self.get_iv_grade(pet.get('iv_speed', 15))}">{pet.get('iv_speed', 15)}</span>
</div>
</div>
<div class="iv-total">
<span class="iv-total-label">Total IV:</span>
<span class="iv-total-value">{pet.get('iv_hp', 15) + pet.get('iv_attack', 15) + pet.get('iv_defense', 15) + pet.get('iv_speed', 15)}/124</span>
</div>
</div>
<div class="pet-actions">
<button class="rename-btn" onclick="startRename({pet_id}, '{current_name}')">
🏷️ Rename
</button>
</div>
<div class="rename-form" id="rename-form-{pet_id}" style="display: none;">
<input type="text" id="new-name-{pet_id}" placeholder="Enter new nickname" maxlength="20" value="{current_name}">
<div class="form-actions">
<button class="save-btn" onclick="submitRename({pet_id})">Save</button>
<button class="cancel-btn" onclick="cancelRename({pet_id})">Cancel</button>
</div>
</div>
</div>
"""
pet_cards.append(pet_card)
pets_html = "".join(pet_cards)
content = f"""
<div class="header">
<h1>🐾 My Pets - {nickname}</h1>
<p>Manage your pet collection and customize their names</p>
</div>
<div class="pets-container">
{pets_html}
</div>
<div class="controls">
<a href="/player/{nickname}" class="back-btn">← Back to Profile</a>
<div class="help-text">
<p>💡 <strong>Tips:</strong></p>
<ul>
<li>Click "Rename" to change a pet's nickname</li>
<li>You'll receive a PIN via IRC for security</li>
<li>PIN expires in 15 seconds</li>
<li>Names must be unique among your pets</li>
</ul>
</div>
</div>
<!-- PIN Verification Modal -->
<div id="pin-modal" class="modal" style="display: none;">
<div class="modal-content">
<h3>🔐 PIN Verification</h3>
<p>A PIN has been sent to your IRC channel. Please enter it below:</p>
<input type="text" id="pin-input" placeholder="Enter 6-digit PIN" maxlength="6">
<div class="modal-actions">
<button class="verify-btn" onclick="verifyPin()">Verify</button>
<button class="cancel-btn" onclick="cancelPin()">Cancel</button>
</div>
<div id="pin-timer" class="timer"></div>
</div>
</div>
<style>
.pets-container {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 20px 0;
}}
.pet-card {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 15px;
transition: transform 0.2s, box-shadow 0.2s;
}}
.pet-card:hover {{
transform: translateY(-2px);
box-shadow: var(--shadow-glow);
}}
.pet-header {{
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}}
.pet-name {{
font-size: 1.2em;
font-weight: bold;
color: var(--text-primary);
}}
.pet-species {{
font-size: 0.9em;
color: var(--text-secondary);
}}
.pet-badges {{
display: flex;
flex-direction: column;
gap: 5px;
}}
.team-badge {{
background: var(--gradient-primary);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}}
.active-badge {{
background: var(--gradient-tertiary);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}}
.storage-badge {{
background: #666;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}}
.fainted-badge {{
background: #8B0000;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
}}
.pet-stats {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 10px 0;
padding: 10px;
background: var(--bg-tertiary);
border-radius: 8px;
}}
.stat-group {{
display: flex;
justify-content: space-between;
}}
.stat-label {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.stat-value {{
color: var(--text-primary);
font-weight: bold;
}}
.pet-actions {{
text-align: center;
margin-top: 10px;
}}
.rename-btn {{
background: var(--gradient-primary);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.9em;
transition: transform 0.2s;
}}
.rename-btn:hover {{
transform: scale(1.05);
}}
.rename-form {{
margin-top: 10px;
padding: 10px;
background: var(--bg-primary);
border-radius: 8px;
}}
.rename-form input {{
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
}}
.form-actions {{
display: flex;
gap: 10px;
}}
.save-btn {{
background: var(--gradient-tertiary);
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
flex: 1;
}}
.cancel-btn {{
background: #666;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
flex: 1;
}}
.modal {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}}
.modal-content {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 20px;
width: 400px;
max-width: 90%;
}}
.modal-content h3 {{
margin-top: 0;
color: var(--text-primary);
}}
.modal-content input {{
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
text-align: center;
font-size: 1.2em;
}}
.modal-actions {{
display: flex;
gap: 10px;
margin-top: 15px;
}}
.verify-btn {{
background: var(--gradient-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
flex: 1;
}}
.timer {{
text-align: center;
margin-top: 10px;
font-size: 0.9em;
color: var(--text-secondary);
}}
.help-text {{
margin-top: 20px;
padding: 15px;
background: var(--bg-tertiary);
border-radius: 8px;
}}
.help-text ul {{
margin: 10px 0;
padding-left: 20px;
}}
.help-text li {{
margin: 5px 0;
}}
@media (max-width: 768px) {{
.pets-container {{
grid-template-columns: 1fr;
}}
.pet-stats {{
grid-template-columns: 1fr;
}}
}}
</style>
<script>
let currentPetId = null;
let pinTimer = null;
function startRename(petId, currentName) {{
// Hide all other rename forms
document.querySelectorAll('.rename-form').forEach(form => {{
form.style.display = 'none';
}});
// Show rename form for this pet
const form = document.getElementById('rename-form-' + petId);
form.style.display = 'block';
// Focus on input
const input = document.getElementById('new-name-' + petId);
input.focus();
input.select();
}}
function cancelRename(petId) {{
document.getElementById('rename-form-' + petId).style.display = 'none';
}}
async function submitRename(petId) {{
const input = document.getElementById('new-name-' + petId);
const newName = input.value.trim();
if (!newName) {{
alert('Please enter a nickname');
return;
}}
try {{
const response = await fetch('/player/{nickname}/pets/rename', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify({{
pet_id: petId,
new_nickname: newName
}})
}});
const result = await response.json();
if (result.success) {{
currentPetId = petId;
showPinModal();
startPinTimer();
}} else {{
alert('Error: ' + result.error);
}}
}} catch (error) {{
console.error('Error:', error);
alert('Network error occurred');
}}
}}
function showPinModal() {{
document.getElementById('pin-modal').style.display = 'flex';
document.getElementById('pin-input').focus();
}}
function cancelPin() {{
document.getElementById('pin-modal').style.display = 'none';
clearInterval(pinTimer);
currentPetId = null;
}}
function startPinTimer() {{
let timeLeft = 15;
const timerElement = document.getElementById('pin-timer');
pinTimer = setInterval(() => {{
timerElement.textContent = `Time remaining: ${{timeLeft}}s`;
timeLeft--;
if (timeLeft < 0) {{
clearInterval(pinTimer);
cancelPin();
alert('PIN expired. Please try again.');
}}
}}, 1000);
}}
async function verifyPin() {{
const pin = document.getElementById('pin-input').value.trim();
if (!pin) {{
alert('Please enter the PIN');
return;
}}
try {{
const response = await fetch('/player/{nickname}/pets/verify', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify({{
pin: pin
}})
}});
const result = await response.json();
if (result.success) {{
alert('Pet renamed successfully!');
cancelPin();
location.reload(); // Refresh page to show new name
}} else {{
alert('Error: ' + result.error);
}}
}} catch (error) {{
console.error('Error:', error);
alert('Network error occurred');
}}
}}
// Allow Enter key to submit PIN
document.getElementById('pin-input').addEventListener('keypress', function(e) {{
if (e.key === 'Enter') {{
verifyPin();
}}
}});
</script>
"""
page_html = self.get_page_template("My Pets - " + nickname, content, "pets")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(page_html.encode())
def serve_no_pets_error(self, nickname):
"""Serve error page when player has no pets"""
content = f"""
<div class="header">
<h1>🐾 No Pets Found</h1>
</div>
<div class="error-message">
<h2>You don't have any pets yet!</h2>
<p>Start your journey by using <code>!start</code> in #petz to get your first pet.</p>
<p>Then explore locations and catch more pets with <code>!explore</code> and <code>!catch</code>.</p>
</div>
<div class="controls">
<a href="/player/{nickname}" class="back-btn">← Back to Profile</a>
</div>
"""
page_html = self.get_page_template("No Pets - " + nickname, content, "pets")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(page_html.encode())
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>
<a href="/testteambuilder/{nickname}" class="btn btn-secondary" style="margin-left: 10px;">
🧪 Test Team Builder
</a>
<a href="/player/{nickname}/pets" class="btn btn-secondary" style="margin-left: 10px;">
🐾 My Pets
</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']]
# Get team configurations for team selection interface
import asyncio
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)
# Get player and team configurations
player = loop.run_until_complete(database.get_player(nickname))
team_configs = []
if player:
team_configs = loop.run_until_complete(database.get_player_team_configurations(player['id']))
# 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)
# Old template removed - using new unified template system below
# 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;
}
/* Storage Controls */
.storage-controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-container input, .sort-container select {
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 8px;
font-size: 0.9em;
}
.search-container input {
min-width: 250px;
}
.sort-container select {
min-width: 180px;
}
.search-container input:focus, .sort-container select:focus {
outline: none;
border-color: var(--text-accent);
}
/* Team Selection Interface */
.team-selector-section {
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin: 30px 0;
border: 1px solid var(--border-color);
}
.team-selector-section h2 {
color: var(--text-accent);
margin-bottom: 20px;
text-align: center;
}
.team-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.team-card {
background: var(--bg-tertiary);
border-radius: 12px;
padding: 20px;
border: 2px solid var(--border-color);
transition: all 0.3s ease;
cursor: pointer;
}
.team-card:hover {
border-color: var(--text-accent);
transform: translateY(-2px);
}
.team-card.selected {
border-color: var(--text-accent);
background: var(--bg-primary);
}
.team-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.team-card h3 {
margin: 0;
color: var(--text-accent);
}
.team-status {
color: var(--text-secondary);
font-size: 0.9em;
}
.team-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin: 15px 0;
min-height: 60px;
}
.mini-pet {
background: var(--bg-primary);
border-radius: 6px;
padding: 8px;
text-align: center;
font-size: 0.8em;
border: 1px solid var(--border-color);
}
.mini-pet.empty {
color: var(--text-secondary);
font-style: italic;
}
.edit-team-btn {
width: 100%;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
cursor: pointer;
font-size: 1em;
transition: all 0.3s ease;
}
.edit-team-btn:hover {
background: var(--secondary-color);
transform: translateY(-1px);
}
.edit-team-btn.active {
background: var(--text-accent);
color: var(--bg-primary);
}
.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: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 20px;
}
.config-slot-actions {
text-align: center;
}
.config-slot-actions h4 {
color: var(--text-accent);
margin: 0 0 15px 0;
font-size: 1.1em;
}
.config-slot-actions button {
display: block;
width: 100%;
margin: 8px 0;
}
.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;
}
.config-apply-btn {
background: var(--secondary-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-apply-btn:hover:not(:disabled) {
background: #4CAF50;
transform: translateY(-1px);
}
.config-apply-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
opacity: 0.5;
}
.save-config-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 15px 0;
}
.config-save-slot-btn {
background: #FF9800;
color: white;
border: none;
border-radius: 8px;
padding: 10px 16px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
}
.config-save-slot-btn:hover {
background: #F57C00;
transform: translateY(-1px);
}
.saved-configs-list {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 15px;
margin-top: 10px;
}
.saved-config-item {
margin: 8px 0;
display: flex;
align-items: center;
}
.config-slot-label {
font-weight: bold;
color: var(--text-accent);
margin-right: 10px;
min-width: 60px;
}
.config-name {
color: var(--text-primary);
}
.config-name.empty {
color: var(--text-secondary);
font-style: italic;
}
</style>
<div class="team-builder-container">
<div class="header">
<h1>🐾 Team Builder</h1>
<p>Choose a team to edit, then drag pets between Active Team and Storage.</p>
</div>
<!-- Team Selection Interface -->
<div class="team-selector-section">
<h2>Select Team to Edit</h2>
<div class="team-cards">
<!-- Team cards will be inserted here -->
</div>
</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-controls">
<div class="search-container">
<input type="text" id="pet-search" placeholder="🔍 Search pets..." onkeyup="filterPets()">
</div>
<div class="sort-container">
<select id="pet-sort" onchange="sortPets()">
<option value="name">📝 Sort by Name</option>
<option value="level">📊 Sort by Level</option>
<option value="type">🏷️ Sort by Type</option>
<option value="species">🧬 Sort by Species</option>
</select>
</div>
</div>
<div class="storage-container" id="storage-container">
""" + storage_pets_html + active_pets_html + """
</div>
</div>
</div>
<!-- Redundant configuration section removed -->
<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
// Global variables for team management
let currentEditingTeam = 1; // Default to team 1
document.addEventListener('DOMContentLoaded', function() {
console.log('Team Builder: DOM loaded, initializing...');
initializeTeamBuilder();
});
function selectTeam(teamSlot) {
console.log('Selecting team slot:', teamSlot);
// Update UI to show which team is selected
document.querySelectorAll('.team-card').forEach(card => {
card.classList.remove('selected');
const btn = card.querySelector('.edit-team-btn');
btn.classList.remove('active');
btn.textContent = btn.textContent.replace('🟢 Currently Editing', '📝 Edit ' + card.querySelector('h3').textContent);
});
// Mark selected team
const selectedCard = document.querySelector(`[data-slot="${teamSlot}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
const btn = selectedCard.querySelector('.edit-team-btn');
btn.classList.add('active');
btn.textContent = '🟢 Currently Editing';
}
// Set current editing team
currentEditingTeam = teamSlot;
// Load team data for this slot (to be implemented)
loadTeamConfiguration(teamSlot);
}
function loadTeamConfiguration(teamSlot) {
console.log('Loading team configuration for slot:', teamSlot);
// Update dynamic headers and button text
updateDynamicElements(teamSlot);
// Clear current team slots
for (let i = 1; i <= 6; i++) {
const slot = document.getElementById(`slot-${i}`);
if (slot) {
const slotContent = slot.querySelector('.slot-content');
slotContent.innerHTML = '<div class="empty-slot">Drop pet here</div>';
}
}
// Move all pets back to storage
const storageContainer = document.getElementById('storage-container');
const allPetCards = document.querySelectorAll('.pet-card');
allPetCards.forEach(card => {
if (storageContainer && !storageContainer.contains(card)) {
storageContainer.appendChild(card);
// Update pet card status
card.classList.remove('active');
card.classList.add('storage');
const statusDiv = card.querySelector('.pet-status');
if (statusDiv) {
statusDiv.textContent = 'Storage';
statusDiv.className = 'pet-status status-storage';
}
}
});
// Reset team state BEFORE loading new data
currentTeam = {};
originalTeam = {};
// Load team data from server for the selected slot
if (teamSlot === 1) {
// For Team 1, load current active pets (default behavior)
loadCurrentActiveTeam();
} else {
// For Teams 2 and 3, load saved configuration if exists
loadSavedTeamConfiguration(teamSlot);
}
// Re-initialize team state
updateTeamState();
}
function updateDynamicElements(teamSlot) {
// Update team header
const teamHeader = document.querySelector('h2');
if (teamHeader && teamHeader.textContent.includes('Active Team')) {
teamHeader.textContent = `⚔️ Team ${teamSlot} Selection (1-6 pets)`;
}
// Update save button
const saveBtn = document.getElementById('save-btn');
if (saveBtn) {
saveBtn.textContent = `🔒 Save Changes to Team ${teamSlot}`;
}
}
function loadCurrentActiveTeam() {
// Load the player's current active pets back into team slots
console.log('Loading current active team (Team 1)');
// Find all pet cards that should be active based on their original data attributes
const allCards = document.querySelectorAll('.pet-card');
console.log(`Found ${allCards.length} total pet cards`);
allCards.forEach(card => {
const isActive = card.dataset.active === 'true';
const teamOrder = card.dataset.teamOrder;
const petId = card.dataset.petId;
console.log(`Pet ${petId}: active=${isActive}, teamOrder=${teamOrder}`);
if (isActive && teamOrder && teamOrder !== 'None' && teamOrder !== '' && teamOrder !== 'null') {
const slot = document.getElementById(`slot-${teamOrder}`);
if (slot) {
// Move pet from storage back to team slot
const slotContent = slot.querySelector('.slot-content');
slotContent.innerHTML = '';
slotContent.appendChild(card);
// Update pet visual status
card.classList.remove('storage');
card.classList.add('active');
const statusDiv = card.querySelector('.pet-status');
if (statusDiv) {
statusDiv.textContent = 'Active';
statusDiv.className = 'pet-status status-active';
}
// Update team state tracking
currentTeam[petId] = parseInt(teamOrder);
originalTeam[petId] = parseInt(teamOrder);
console.log(`✅ Restored pet ${petId} to slot ${teamOrder}`);
} else {
console.log(`❌ Could not find slot ${teamOrder} for pet ${petId}`);
}
} else {
// This pet should stay in storage
currentTeam[petId] = false;
if (!originalTeam.hasOwnProperty(petId)) {
originalTeam[petId] = false;
}
console.log(`Pet ${petId} staying in storage`);
}
});
console.log('Current team state after restoration:', currentTeam);
updateSaveButton();
}
async function loadSavedTeamConfiguration(teamSlot) {
console.log(`Loading saved configuration for team ${teamSlot}`);
try {
const response = await fetch(`/teambuilder/{nickname}/config/load/${teamSlot}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
console.error(`Failed to load team config: ${response.status} ${response.statusText}`);
return;
}
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
console.error(`Expected JSON response but got: ${contentType}`);
const text = await response.text();
console.error('Response body:', text);
return;
}
const result = await response.json();
if (result.success && result.team_data) {
// Load pets into team slots based on saved configuration
for (const [position, petData] of Object.entries(result.team_data)) {
if (petData && position >= 1 && position <= 6) {
const petCard = document.querySelector(`[data-pet-id="${petData.pet_id}"]`);
const slot = document.getElementById(`slot-${position}`);
if (petCard && slot) {
// Move pet to team slot
const slotContent = slot.querySelector('.slot-content');
slotContent.innerHTML = '';
slotContent.appendChild(petCard);
// Update pet status
petCard.classList.remove('storage');
petCard.classList.add('active');
const statusDiv = petCard.querySelector('.pet-status');
if (statusDiv) {
statusDiv.textContent = 'Active';
statusDiv.className = 'pet-status status-active';
}
// Update team state
currentTeam[petData.pet_id] = parseInt(position);
originalTeam[petData.pet_id] = parseInt(position);
}
}
}
} else {
console.log(`No saved configuration found for team ${teamSlot} - starting with empty team`);
// Team is already cleared, just update team state for empty team
currentTeam = {};
originalTeam = {};
updateSaveButton();
}
} catch (error) {
console.error('Error loading team configuration:', error);
}
}
function updateTeamState() {
// Update team state tracking
const allCards = document.querySelectorAll('.pet-card');
allCards.forEach(card => {
const petId = card.dataset.petId;
const isActive = card.classList.contains('active');
const teamOrder = card.dataset.teamOrder;
if (isActive && teamOrder) {
currentTeam[petId] = parseInt(teamOrder);
if (!originalTeam.hasOwnProperty(petId)) {
originalTeam[petId] = parseInt(teamOrder);
}
} else {
currentTeam[petId] = false;
if (!originalTeam.hasOwnProperty(petId)) {
originalTeam[petId] = false;
}
}
});
updateSaveButton();
}
function updateTeamCard(teamSlot) {
// Update the team card display to reflect current team composition
const teamCard = document.querySelector(`[data-slot="${teamSlot}"]`);
if (!teamCard) return;
// Count active pets in current team
let petCount = 0;
let petPreviews = '';
// Generate mini pet previews for the team card
for (let i = 1; i <= 6; i++) {
const slot = document.getElementById(`slot-${i}`);
if (slot) {
const petCard = slot.querySelector('.pet-card');
if (petCard) {
const petName = petCard.querySelector('.pet-name').textContent;
petPreviews += `<div class="mini-pet">${petName}</div>`;
petCount++;
} else {
petPreviews += '<div class="mini-pet empty">Empty</div>';
}
}
}
// Update team card content
const statusSpan = teamCard.querySelector('.team-status');
const previewDiv = teamCard.querySelector('.team-preview');
if (statusSpan) {
statusSpan.textContent = petCount > 0 ? `${petCount}/6 pets` : 'Empty team';
}
if (previewDiv) {
previewDiv.innerHTML = petPreviews;
}
}
function filterPets() {
const searchTerm = document.getElementById('pet-search').value.toLowerCase();
const storageContainer = document.getElementById('storage-container');
const petCards = storageContainer.querySelectorAll('.pet-card');
petCards.forEach(card => {
const petName = card.querySelector('.pet-name').textContent.toLowerCase();
const petSpecies = card.dataset.species ? card.dataset.species.toLowerCase() : '';
const petTypes = card.querySelectorAll('.type-badge');
let typeText = '';
petTypes.forEach(badge => typeText += badge.textContent.toLowerCase() + ' ');
const matches = petName.includes(searchTerm) ||
petSpecies.includes(searchTerm) ||
typeText.includes(searchTerm);
card.style.display = matches ? 'block' : 'none';
});
}
function sortPets() {
const sortBy = document.getElementById('pet-sort').value;
const storageContainer = document.getElementById('storage-container');
const petCards = Array.from(storageContainer.querySelectorAll('.pet-card'));
petCards.sort((a, b) => {
switch (sortBy) {
case 'name':
const nameA = a.querySelector('.pet-name').textContent.toLowerCase();
const nameB = b.querySelector('.pet-name').textContent.toLowerCase();
return nameA.localeCompare(nameB);
case 'level':
const levelA = parseInt(a.querySelector('.pet-level').textContent.replace('Level ', ''));
const levelB = parseInt(b.querySelector('.pet-level').textContent.replace('Level ', ''));
return levelB - levelA; // Descending order
case 'type':
const typeA = a.querySelector('.type-badge').textContent.toLowerCase();
const typeB = b.querySelector('.type-badge').textContent.toLowerCase();
return typeA.localeCompare(typeB);
case 'species':
const speciesA = a.dataset.species ? a.dataset.species.toLowerCase() : '';
const speciesB = b.dataset.species ? b.dataset.species.toLowerCase() : '';
return speciesA.localeCompare(speciesB);
default:
return 0;
}
});
// Re-append sorted cards to container
petCards.forEach(card => storageContainer.appendChild(card));
}
function initializeTeamBuilder() {
console.log('Team Builder: Starting initialization...');
// Initialize dynamic elements for Team 1 (default)
updateDynamicElements(1);
// 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;
// Preserve the dynamic team number text
if (hasChanges) {
saveBtn.textContent = `🔒 Save Changes to Team ${currentEditingTeam}`;
} else {
saveBtn.textContent = `✅ No Changes (Team ${currentEditingTeam})`;
}
}
async function saveTeam() {
const teamData = {};
Object.entries(currentTeam).forEach(([petId, position]) => {
teamData[petId] = position;
});
// Include the current editing team slot
const saveData = {
teamData: teamData,
teamSlot: currentEditingTeam
};
console.log('🔍 SAVE DEBUG: Saving team data:', saveData);
console.log('🔍 SAVE DEBUG: Current editing team:', currentEditingTeam);
console.log('🔍 SAVE DEBUG: Team data entries:', Object.keys(teamData).length);
try {
const response = await fetch('/teambuilder/""" + nickname + """/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(saveData)
});
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;
}
console.log('🔍 PIN DEBUG: Verifying PIN for team:', currentEditingTeam);
console.log('🔍 PIN DEBUG: PIN entered:', pin);
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 = '';
// Update team card display after successful save
updateTeamCard(currentEditingTeam);
// 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>
"""
# Generate team cards HTML
print(f"Debug: Generating team cards for {len(team_configs)} configs")
team_cards_html = ""
# If no team configs exist, create default slots with Team 1 showing current active team
if not team_configs:
print("Debug: No team configs found, creating default empty slots")
for slot in range(1, 4):
# For Team 1, show current active pets
if slot == 1:
pet_previews = ""
active_count = 0
for pos in range(1, 7):
# Find pet in position pos
pet_in_slot = next((pet for pet in active_pets if pet.get('team_order') == pos), None)
if pet_in_slot:
pet_name = pet_in_slot['nickname'] or pet_in_slot['species_name']
pet_emoji = pet_in_slot.get('emoji', '🐾')
pet_previews += f'<div class="mini-pet">{pet_emoji} {pet_name}</div>'
active_count += 1
else:
pet_previews += '<div class="mini-pet empty">Empty</div>'
status_text = f"{active_count}/6 pets" if active_count > 0 else "Empty team"
else:
# Teams 2 and 3 are empty by default
pet_previews = '<div class="mini-pet empty">Empty</div>' * 6
status_text = "Empty team"
team_cards_html += f'''
<div class="team-card {'active' if slot == 1 else ''}" data-slot="{slot}">
<div class="team-card-header">
<h3>Team {slot}</h3>
<span class="team-status">{status_text}</span>
</div>
<div class="team-preview">
{pet_previews}
</div>
<button class="edit-team-btn {'active' if slot == 1 else ''}" onclick="selectTeam({slot})">
{'🟢 Currently Editing' if slot == 1 else f'📝 Edit Team {slot}'}
</button>
</div>
'''
else:
for config in team_configs:
print(f"Debug: Processing config: {config}")
pet_previews = ""
# Special handling for Team 1 - show current active team instead of saved config
if config['slot'] == 1:
active_count = 0
for pos in range(1, 7):
# Find pet in position pos from current active team
pet_in_slot = next((pet for pet in active_pets if pet.get('team_order') == pos), None)
if pet_in_slot:
pet_name = pet_in_slot['nickname'] or pet_in_slot['species_name']
pet_emoji = pet_in_slot.get('emoji', '🐾')
pet_previews += f'<div class="mini-pet">{pet_emoji} {pet_name}</div>'
active_count += 1
else:
pet_previews += '<div class="mini-pet empty">Empty</div>'
status_text = f"{active_count}/6 pets" if active_count > 0 else "Empty team"
else:
# For Teams 2 and 3, use saved configuration data
if config['team_data']:
for pos in range(1, 7):
if str(pos) in config['team_data'] and config['team_data'][str(pos)]:
pet_info = config['team_data'][str(pos)]
pet_emoji = pet_info.get('emoji', '🐾')
pet_previews += f'<div class="mini-pet">{pet_emoji} {pet_info["name"]}</div>'
else:
pet_previews += '<div class="mini-pet empty">Empty</div>'
else:
pet_previews = '<div class="mini-pet empty">Empty</div>' * 6
status_text = f"{config['pet_count']}/6 pets" if config['pet_count'] > 0 else "Empty team"
active_class = "active" if config['slot'] == 1 else "" # Default to team 1 as active
team_cards_html += f'''
<div class="team-card {active_class}" data-slot="{config['slot']}">
<div class="team-card-header">
<h3>{config['name']}</h3>
<span class="team-status">{status_text}</span>
</div>
<div class="team-preview">
{pet_previews}
</div>
<button class="edit-team-btn {active_class}" onclick="selectTeam({config['slot']})">
{'🟢 Currently Editing' if config['slot'] == 1 else f'📝 Edit {config["name"]}'}
</button>
</div>
'''
# Replace placeholder with actual team cards
team_builder_content = team_builder_content.replace('<!-- Team cards will be inserted here -->', team_cards_html)
# 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 serve_test_teambuilder(self, nickname):
"""Serve the test team builder interface with simplified team management"""
from urllib.parse import unquote
nickname = unquote(nickname)
print(f"DEBUG: serve_test_teambuilder called with nickname: {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_test_teambuilder_no_pets(nickname)
return
self.serve_test_teambuilder_interface(nickname, pets)
except Exception as e:
print(f"Error loading test team builder for {nickname}: {e}")
self.serve_player_error(nickname, f"Error loading test team builder: {str(e)}")
def serve_test_teambuilder_no_pets(self, nickname):
"""Show message when player has no pets using unified template"""
content = f"""
<div class="header">
<h1>🐾 Test Team Builder</h1>
<p>Simplified team management (Test Version)</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>
"""
html_content = self.get_page_template(f"Test Team Builder - {nickname}", content, "")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
def serve_test_teambuilder_interface(self, nickname, pets):
"""Serve the simplified test team builder interface"""
# Get team configurations for this player
import asyncio
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)
print(f"Debug: Getting player data for {nickname}")
# Get player info
player = loop.run_until_complete(database.get_player(nickname))
if not player:
self.serve_player_error(nickname, "Player not found")
return
print(f"Debug: Player found: {player}")
# Get team configurations
team_configs = loop.run_until_complete(database.get_player_team_configurations(player['id']))
print(f"Debug: Team configs: {team_configs}")
# Create the simplified interface
print(f"Debug: Creating content with {len(pets)} pets")
# TEMPORARY: Use simple content to test
content = f"""
<h1>Test Team Builder - {nickname}</h1>
<p>Found {len(pets)} pets and {len(team_configs)} team configs</p>
<p>First pet: {pets[0]['nickname'] if pets else 'No pets'}</p>
"""
print("Debug: Content created successfully")
html_content = self.get_page_template(f"Test Team Builder - {nickname}", content, "")
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 in serve_test_teambuilder_interface: {e}")
self.serve_player_error(nickname, f"Error loading interface: {str(e)}")
def create_test_teambuilder_content(self, nickname, pets, team_configs):
"""Create the simplified test team builder HTML content"""
import json
# Pre-process pets data for JavaScript
pets_data = []
for pet in pets:
pets_data.append({
'id': pet['id'],
'name': pet['nickname'],
'level': pet['level'],
'type_primary': pet['type1'],
'rarity': 1
})
pets_json = json.dumps(pets_data)
# Build team cards for each configuration
team_cards_html = ""
for config in team_configs:
pet_previews = ""
if config['team_data']:
for pos in range(1, 7):
if str(pos) in config['team_data'] and config['team_data'][str(pos)]:
pet_info = config['team_data'][str(pos)]
pet_previews += f'<div class="mini-pet">{pet_info["name"]}</div>'
else:
pet_previews += '<div class="mini-pet empty">Empty</div>'
else:
pet_previews = '<div class="empty-team">No pets assigned</div>'
status_text = f"{config['pet_count']}/6 pets" if config['pet_count'] > 0 else "Empty team"
team_cards_html += f'''
<div class="team-card" data-slot="{config['slot']}">
<div class="team-header">
<h3>{config['name']}</h3>
<span class="team-status">{status_text}</span>
</div>
<div class="team-preview">
{pet_previews}
</div>
<div class="team-actions">
<button class="edit-team-btn" onclick="editTeam({config['slot']})">
📝 Edit {config['name']}
</button>
</div>
</div>
'''
return f'''
<style>
/* Test Team Builder Styles */
.test-team-container {{
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}}
.team-selector {{
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid var(--border-color);
}}
.team-cards {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}}
.team-card {{
background: var(--bg-tertiary);
border-radius: 12px;
padding: 20px;
border: 2px solid var(--border-color);
transition: all 0.3s ease;
}}
.team-card:hover {{
border-color: var(--text-accent);
transform: translateY(-2px);
}}
.team-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}}
.team-header h3 {{
margin: 0;
color: var(--text-accent);
}}
.team-status {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.team-preview {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin: 15px 0;
min-height: 60px;
}}
.mini-pet {{
background: var(--bg-primary);
border-radius: 6px;
padding: 8px;
text-align: center;
font-size: 0.8em;
border: 1px solid var(--border-color);
}}
.mini-pet.empty {{
color: var(--text-secondary);
font-style: italic;
}}
.empty-team {{
grid-column: 1 / -1;
text-align: center;
color: var(--text-secondary);
font-style: italic;
padding: 20px;
}}
.edit-team-btn {{
width: 100%;
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
cursor: pointer;
font-size: 1em;
transition: all 0.3s ease;
}}
.edit-team-btn:hover {{
background: var(--secondary-color);
transform: translateY(-1px);
}}
.editor-container {{
display: none;
background: var(--bg-secondary);
border-radius: 15px;
padding: 25px;
margin-top: 30px;
border: 1px solid var(--border-color);
}}
.editor-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}}
.back-to-selector {{
background: var(--text-secondary);
color: white;
border: none;
border-radius: 8px;
padding: 10px 15px;
cursor: pointer;
text-decoration: none;
}}
.save-team-btn {{
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
cursor: pointer;
font-size: 1.1em;
margin-top: 20px;
}}
.save-team-btn:hover {{
background: #45a049;
}}
.save-team-btn:disabled {{
background: var(--text-secondary);
cursor: not-allowed;
}}
.pin-section {{
display: none;
background: var(--bg-primary);
border-radius: 12px;
padding: 20px;
margin-top: 20px;
border: 2px solid var(--text-accent);
}}
.pin-input {{
width: 150px;
padding: 10px;
font-size: 1.2em;
text-align: center;
border: 2px solid var(--border-color);
border-radius: 8px;
background: var(--bg-tertiary);
color: var(--text-primary);
margin: 10px;
}}
.verify-btn {{
background: var(--text-accent);
color: var(--bg-primary);
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
}}
.message-area {{
margin-top: 15px;
padding: 10px;
border-radius: 6px;
text-align: center;
display: none;
}}
.message-success {{
background: #4CAF50;
color: white;
}}
.message-error {{
background: #f44336;
color: white;
}}
/* Team Editor Styles */
.team-editor-grid {{
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
margin: 20px 0;
}}
.team-slots {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}}
.team-slot {{
background: var(--bg-tertiary);
border: 2px dashed var(--border-color);
border-radius: 12px;
padding: 15px;
min-height: 120px;
position: relative;
transition: all 0.3s ease;
}}
.team-slot.drag-over {{
border-color: var(--text-accent);
background: var(--bg-primary);
}}
.slot-header {{
font-weight: bold;
color: var(--text-accent);
margin-bottom: 10px;
text-align: center;
}}
.pet-info {{
text-align: center;
}}
.pet-name {{
font-weight: bold;
color: var(--text-primary);
margin-bottom: 5px;
}}
.pet-details {{
color: var(--text-secondary);
font-size: 0.9em;
}}
.empty-slot {{
text-align: center;
color: var(--text-secondary);
font-style: italic;
margin-top: 20px;
}}
.remove-pet {{
position: absolute;
top: 5px;
right: 5px;
background: #f44336;
color: white;
border: none;
border-radius: 50%;
width: 25px;
height: 25px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}}
.available-pets {{
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px;
}}
.available-pet {{
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
cursor: grab;
transition: all 0.3s ease;
}}
.available-pet:hover {{
background: var(--bg-primary);
border-color: var(--text-accent);
}}
.available-pet.dragging {{
opacity: 0.5;
cursor: grabbing;
}}
.pet-rarity {{
color: var(--text-secondary);
font-size: 0.8em;
margin-top: 5px;
}}
.no-pets {{
text-align: center;
color: var(--text-secondary);
font-style: italic;
padding: 20px;
}}
</style>
<div class="test-team-container">
<div class="header">
<h1>🐾 Test Team Builder</h1>
<p>Choose a team to edit, make changes, and save with PIN verification</p>
</div>
<div class="team-selector" id="team-selector">
<h2>Choose Team to Edit</h2>
<p style="color: var(--text-secondary);">Select one of your 3 teams to edit. Each team can have up to 6 pets.</p>
<div class="team-cards">
{team_cards_html}
</div>
</div>
<div class="editor-container" id="editor-container">
<div class="editor-header">
<h2 id="editor-title">Editing Team 1</h2>
<button class="back-to-selector" onclick="backToSelector()">← Back to Team Selection</button>
</div>
<div id="team-editor">
<!-- Team editor will be loaded here -->
</div>
<div class="editor-controls">
<button class="save-team-btn" id="save-team-btn" onclick="saveCurrentTeam()" disabled>
💾 Save Team Changes
</button>
</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="verifyTeamPin()">✅ Verify & Save Team</button>
<div class="message-area" id="message-area"></div>
</div>
</div>
</div>
<script>
let currentEditingTeam = null;
let originalTeamData = {{}};
let currentTeamData = {{}};
function editTeam(teamSlot) {{
console.log('Editing team slot:', teamSlot);
currentEditingTeam = teamSlot;
// Hide team selector, show editor
document.getElementById('team-selector').style.display = 'none';
document.getElementById('editor-container').style.display = 'block';
// Update editor title
const teamName = document.querySelector(`[data-slot="${{teamSlot}}"] h3`).textContent;
document.getElementById('editor-title').textContent = `Editing ${{teamName}}`;
// Load team data for editing
loadTeamForEditing(teamSlot);
}}
function backToSelector() {{
document.getElementById('team-selector').style.display = 'block';
document.getElementById('editor-container').style.display = 'none';
document.getElementById('pin-section').style.display = 'none';
currentEditingTeam = null;
}}
async function loadTeamForEditing(teamSlot) {{
try {{
const response = await fetch(`/teambuilder/{nickname}/config/load/${{teamSlot}}`, {{
method: 'POST'
}});
if (response.ok) {{
const result = await response.json();
if (result.success) {{
originalTeamData = result.team_data || {{}};
currentTeamData = JSON.parse(JSON.stringify(originalTeamData));
renderTeamEditor();
}} else {{
// Empty team
originalTeamData = {{}};
currentTeamData = {{}};
renderTeamEditor();
}}
}} else {{
// Empty team
originalTeamData = {{}};
currentTeamData = {{}};
renderTeamEditor();
}}
}} catch (error) {{
console.error('Error loading team:', error);
originalTeamData = {{}};
currentTeamData = {{}};
renderTeamEditor();
}}
}}
function renderTeamEditor() {{
const editorDiv = document.getElementById('team-editor');
// Create team slots HTML
let teamSlotsHtml = '';
for (let i = 1; i <= 6; i++) {{
const pet = currentTeamData[i] || null;
const slotContent = pet ?
`<div class="pet-info">
<div class="pet-name">${{pet.name}}</div>
<div class="pet-details">Lv. ${{pet.level}} ${{pet.type_primary}}</div>
</div>` :
'<div class="empty-slot">Drop pet here</div>';
teamSlotsHtml += `
<div class="team-slot" data-position="${{i}}" ondrop="dropPet(event)" ondragover="allowDrop(event)">
<div class="slot-header">Slot ${{i}}</div>
${{slotContent}}
${{pet ? '<button class="remove-pet" onclick="removePetFromSlot(' + i + ')">×</button>' : ''}}
</div>
`;
}}
// Get available pets (not currently in team)
const availablePets = getAvailablePets();
let availablePetsHtml = '';
availablePets.forEach(pet => {{
availablePetsHtml += `
<div class="available-pet" draggable="true" ondragstart="dragPet(event)" data-pet-id="${{pet.id}}">
<div class="pet-name">${{pet.name}}</div>
<div class="pet-details">Lv. ${{pet.level}} ${{pet.type_primary}}</div>
<div class="pet-rarity">Rarity: ${{pet.rarity}}</div>
</div>
`;
}});
if (availablePets.length === 0) {{
availablePetsHtml = '<div class="no-pets">No available pets to add</div>';
}}
editorDiv.innerHTML = `
<div class="team-editor-grid">
<div class="team-slots-section">
<h3>Current Team</h3>
<div class="team-slots">
${{teamSlotsHtml}}
</div>
</div>
<div class="available-pets-section">
<h3>Available Pets</h3>
<div class="available-pets">
${{availablePetsHtml}}
</div>
</div>
</div>
`;
updateSaveButton();
}}
function updateSaveButton() {{
const saveBtn = document.getElementById('save-team-btn');
const hasChanges = JSON.stringify(originalTeamData) !== JSON.stringify(currentTeamData);
saveBtn.disabled = !hasChanges;
}}
async function saveCurrentTeam() {{
if (!currentEditingTeam) return;
try {{
const response = await fetch(`/teambuilder/{nickname}/save`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
teamSlot: currentEditingTeam,
teamData: currentTeamData
}})
}});
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 verifyTeamPin() {{
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');
setTimeout(() => {{
backToSelector();
location.reload(); // Reload to show updated team
}}, 2000);
}} else {{
showMessage('Invalid PIN: ' + result.error, 'error');
}}
}} catch (error) {{
showMessage('Network error: ' + error.message, 'error');
}}
}}
function showMessage(message, type) {{
const messageArea = document.getElementById('message-area');
messageArea.textContent = message;
messageArea.className = 'message-area message-' + type;
messageArea.style.display = 'block';
if (type === 'success') {{
setTimeout(() => {{
messageArea.style.display = 'none';
}}, 5000);
}}
}}
// Global variables for all pets data
let allPetsData = {pets_json};
function getAvailablePets() {{
// Get pets that are not currently in the team
const usedPetIds = Object.values(currentTeamData).filter(pet => pet && pet.id).map(pet => pet.id);
return allPetsData.filter(pet => !usedPetIds.includes(pet.id));
}}
function allowDrop(ev) {{
ev.preventDefault();
ev.currentTarget.classList.add('drag-over');
}}
function dragPet(ev) {{
ev.dataTransfer.setData("text", ev.currentTarget.getAttribute('data-pet-id'));
ev.currentTarget.classList.add('dragging');
}}
function dropPet(ev) {{
ev.preventDefault();
ev.currentTarget.classList.remove('drag-over');
const petId = parseInt(ev.dataTransfer.getData("text"));
const position = parseInt(ev.currentTarget.getAttribute('data-position'));
// Find the pet data
const pet = allPetsData.find(p => p.id === petId);
if (!pet) return;
// Add pet to team
currentTeamData[position] = pet;
// Re-render editor and update save button
renderTeamEditor();
}}
function removePetFromSlot(position) {{
delete currentTeamData[position];
renderTeamEditor();
}}
// Clean up drag states when drag ends
document.addEventListener('dragend', function(e) {{
document.querySelectorAll('.available-pet').forEach(pet => {{
pet.classList.remove('dragging');
}});
document.querySelectorAll('.team-slot').forEach(slot => {{
slot.classList.remove('drag-over');
}});
}});
</script>
'''
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, save_data):
"""Async handler for team save"""
try:
# Extract team data and slot from new structure
if isinstance(save_data, dict) and 'teamData' in save_data:
team_data = save_data['teamData']
team_slot = save_data.get('teamSlot', 1) # Default to slot 1
else:
# Backwards compatibility - old format
team_data = save_data
team_slot = 1
# Validate team slot
if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3:
return {"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"}
# 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 (include team slot info)
import json
change_data = {
'teamData': team_data,
'teamSlot': team_slot
}
result = await self.database.create_pending_team_change(
player["id"],
json.dumps(change_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)}
def handle_team_config_apply(self, nickname, slot):
"""Handle applying team configuration to active team"""
try:
# Validate slot number
try:
slot_num = int(slot)
if slot_num < 1 or slot_num > 3:
self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400)
return
except ValueError:
self.send_json_response({"success": False, "error": "Invalid slot number"}, 400)
return
# Run async operations
import asyncio
result = asyncio.run(self._handle_team_config_apply_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_apply: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_team_config_apply_async(self, nickname, slot_num):
"""Async handler for team configuration apply"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Apply the team configuration
result = await self.database.apply_team_configuration(player["id"], slot_num)
return result
except Exception as e:
print(f"Error in _handle_team_config_apply_async: {e}")
return {"success": False, "error": str(e)}
def handle_test_team_save(self, nickname):
"""Handle test team builder save request and generate PIN"""
try:
# 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')
try:
import json
data = json.loads(post_data)
team_slot = data.get('team_slot')
team_data = data.get('team_data', {})
# Validate team slot
if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3:
self.send_json_response({"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"}, 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_test_team_save_async(nickname, team_slot, 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_test_team_save: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_test_team_save_async(self, nickname, team_slot, team_data):
"""Async handler for test team save"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Generate PIN and store pending change
import json
team_json = json.dumps(team_data)
config_name = f"Team {team_slot}"
result = await self.database.save_team_configuration(
player["id"], team_slot, config_name, team_json
)
if result:
# Generate PIN for verification (using existing PIN system)
pin_result = await self.database.create_team_change_pin(
player["id"], team_json
)
if pin_result["success"]:
# Send PIN to IRC
if hasattr(self.server, 'bot') and self.server.bot:
self.server.bot.send_private_message(
nickname,
f"🔐 Team {team_slot} Save PIN: {pin_result['pin_code']} (expires in 10 minutes)"
)
return {"success": True, "message": "PIN sent to IRC"}
else:
return {"success": False, "error": "Failed to generate PIN"}
else:
return {"success": False, "error": "Failed to save team configuration"}
except Exception as e:
print(f"Error in _handle_test_team_save_async: {e}")
return {"success": False, "error": str(e)}
def handle_test_team_verify(self, nickname):
"""Handle test team builder PIN verification"""
try:
# Get PIN 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'))
pin_code = data.get('pin', '').strip()
except json.JSONDecodeError:
self.send_json_response({"success": False, "error": "Invalid request data"}, 400)
return
if not pin_code or len(pin_code) != 6:
self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400)
return
# Run async verification
import asyncio
result = asyncio.run(self._handle_test_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_test_team_verify: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_test_team_verify_async(self, nickname, pin_code):
"""Async handler for test team PIN verification"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Verify PIN
result = await self.database.verify_team_change_pin(player["id"], pin_code)
if result["success"]:
return {"success": True, "message": "Team configuration saved successfully!"}
else:
return {"success": False, "error": result.get("error", "Invalid PIN")}
except Exception as e:
print(f"Error in _handle_test_team_verify_async: {e}")
return {"success": False, "error": str(e)}
def handle_individual_team_save(self, nickname, team_slot):
"""Handle individual 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')
try:
import json
data = json.loads(post_data)
team_identifier = data.get('team_identifier', team_slot)
is_active_team = data.get('is_active_team', False)
pets = data.get('pets', [])
# Validate team slot
if team_slot not in ['1', '2', '3', 'active']:
self.send_json_response({"success": False, "error": "Invalid team slot"}, 400)
return
# Convert team_slot to numeric for database operations
if team_slot == 'active':
team_slot_num = 1
is_active_team = True
else:
team_slot_num = int(team_slot)
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_individual_team_save_async(nickname, team_slot_num, pets, is_active_team))
if result["success"]:
self.send_json_response({"requires_pin": True, "message": "PIN sent to IRC"}, 200)
else:
self.send_json_response(result, 400)
except Exception as e:
print(f"Error in handle_individual_team_save: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_individual_team_save_async(self, nickname, team_slot, pets, is_active_team):
"""Async handler for individual team save"""
try:
# Get player
player = await self.server.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Validate pets exist and belong to player
if pets:
player_pets = await self.server.database.get_player_pets(player['id'])
player_pet_ids = [pet['id'] for pet in player_pets]
for pet_data in pets:
pet_id = pet_data.get('pet_id')
if not pet_id:
continue
# Convert pet_id to int for comparison with database IDs
try:
pet_id_int = int(pet_id)
except (ValueError, TypeError):
return {"success": False, "error": f"Invalid pet ID: {pet_id}"}
# Check if pet belongs to player
if pet_id_int not in player_pet_ids:
return {"success": False, "error": f"Pet {pet_id} not found or doesn't belong to you"}
# Convert pets array to the expected format for database
# Expected format: {"pet_id": position, "pet_id": position, ...}
team_changes = {}
if pets: # Ensure pets is not None or empty
for pet_data in pets:
if isinstance(pet_data, dict): # Ensure pet_data is a dictionary
pet_id = str(pet_data.get('pet_id')) # Ensure pet_id is string
position = pet_data.get('position', False) # Position or False for inactive
if pet_id and pet_id != 'None': # Only add valid pet IDs
team_changes[pet_id] = position
# Generate PIN and store pending change
import json
team_data = {
'teamSlot': int(team_slot), # Convert to int and use expected key name
'teamData': team_changes, # Use the dictionary format expected by database
'is_active_team': is_active_team
}
# Generate PIN
pin_result = await self.server.database.generate_verification_pin(player["id"], "team_change", json.dumps(team_data))
pin_code = pin_result.get("pin_code")
# Send PIN via IRC
self.send_pin_via_irc(nickname, pin_code)
return {"success": True, "requires_pin": True}
except Exception as e:
print(f"Error in _handle_individual_team_save_async: {e}")
return {"success": False, "error": str(e)}
def handle_individual_team_verify(self, nickname, team_slot):
"""Handle individual team PIN verification"""
try:
# Get PIN 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'))
pin_code = data.get('pin', '').strip()
except json.JSONDecodeError:
self.send_json_response({"success": False, "error": "Invalid request data"}, 400)
return
if not pin_code or len(pin_code) != 6:
self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400)
return
# Run async verification
import asyncio
result = asyncio.run(self._handle_individual_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_individual_team_verify: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_individual_team_verify_async(self, nickname, pin_code):
"""Async handler for individual team PIN verification"""
try:
# Get player
player = await self.server.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Verify PIN and apply changes using simplified method
result = await self.server.database.apply_individual_team_change(player["id"], pin_code)
if result["success"]:
return {"success": True, "message": "Team saved successfully!"}
else:
return {"success": False, "error": result.get("error", "Invalid PIN")}
except Exception as e:
print(f"Error in _handle_individual_team_verify_async: {e}")
return {"success": False, "error": str(e)}
def handle_pet_rename_request(self, nickname):
"""Handle pet rename 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')
try:
import json
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
try:
result = asyncio.run(self._handle_pet_rename_request_async(nickname, data))
except Exception as async_error:
print(f"Async error in pet rename: {async_error}")
self.send_json_response({"success": False, "error": f"Async error: {str(async_error)}"}, 500)
return
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_pet_rename_request: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_pet_rename_request_async(self, nickname, data):
"""Async handler for pet rename request"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Validate required fields
if "pet_id" not in data or "new_nickname" not in data:
return {"success": False, "error": "Missing pet_id or new_nickname"}
pet_id = data["pet_id"]
new_nickname = data["new_nickname"]
# Request pet rename with PIN
result = await self.database.request_pet_rename(player["id"], pet_id, new_nickname)
if result["success"]:
# Send PIN via IRC
self.send_pet_rename_pin_via_irc(nickname, result["pin"])
return {
"success": True,
"message": f"PIN sent to {nickname} via IRC. Check your messages!"
}
else:
return result
except Exception as e:
print(f"Error in _handle_pet_rename_request_async: {e}")
return {"success": False, "error": str(e)}
def handle_pet_rename_verify(self, nickname):
"""Handle PIN verification for pet rename"""
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')
try:
import json
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_pet_rename_verify_async(nickname, 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_pet_rename_verify: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _handle_pet_rename_verify_async(self, nickname, data):
"""Async handler for pet rename PIN verification"""
try:
# Get player
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Validate required field
if "pin" not in data:
return {"success": False, "error": "Missing PIN"}
pin_code = data["pin"]
# Verify PIN and apply pet rename
result = await self.database.verify_pet_rename(player["id"], pin_code)
if result["success"]:
return {
"success": True,
"message": f"Pet renamed to '{result['new_nickname']}' successfully!",
"new_nickname": result["new_nickname"]
}
else:
return result
except Exception as e:
print(f"Error in _handle_pet_rename_verify_async: {e}")
return {"success": False, "error": str(e)}
def send_pet_rename_pin_via_irc(self, nickname, pin_code):
"""Send pet rename PIN to player via IRC private message"""
print(f"🔐 Pet rename 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"🔐 Pet Rename PIN: {pin_code}")
self.bot.send_message_sync(nickname, f"💡 Enter this PIN on the web page to confirm your pet rename. PIN expires in 15 seconds.")
print(f"✅ Pet rename PIN sent to {nickname} via IRC")
except Exception as e:
print(f"❌ Failed to send pet rename PIN via IRC: {e}")
else:
print(f"❌ No IRC bot available to send pet rename PIN to {nickname}")
print(f"💡 Manual pet rename PIN for {nickname}: {pin_code}")
def serve_admin_login(self):
"""Serve the admin login page"""
import sys
sys.path.append('.')
from config import ADMIN_USER
content = """
<div class="header">
<h1>🔐 Admin Control Panel</h1>
<p>Authorized access only</p>
</div>
<div class="card" style="max-width: 500px; margin: 0 auto;">
<h2>Authentication Required</h2>
<p>This area is restricted to bot administrators.</p>
<div id="auth-form" style="margin-top: 20px;">
<div style="margin-bottom: 15px;">
<label for="admin-nickname" style="display: block; margin-bottom: 5px;">IRC Nickname:</label>
<input type="text" id="admin-nickname" class="form-input" placeholder="Enter your IRC nickname" style="width: 100%; padding: 10px; border-radius: 5px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary);">
</div>
<button onclick="requestAdminAuth()" class="btn btn-primary" style="width: 100%;">Request PIN</button>
<div style="margin-top: 10px; text-align: center; color: var(--text-secondary);">
<small>A PIN will be sent to your IRC private messages</small>
</div>
</div>
<div id="pin-section" style="display: none; margin-top: 20px;">
<div style="margin-bottom: 15px;">
<label for="admin-pin" style="display: block; margin-bottom: 5px;">Enter PIN:</label>
<input type="text" id="admin-pin" class="form-input" placeholder="Enter 6-digit PIN" maxlength="6" style="width: 100%; padding: 10px; border-radius: 5px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary); font-size: 1.2em; letter-spacing: 0.2em; text-align: center;">
</div>
<button onclick="verifyAdminPin()" class="btn btn-primary" style="width: 100%;">Verify PIN</button>
<div style="margin-top: 10px; text-align: center;">
<small id="pin-timer" style="color: var(--warning-color);"></small>
</div>
</div>
<div id="message-area" style="margin-top: 20px;"></div>
</div>
<style>
.form-input:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(77, 171, 247, 0.2);
}
.message {
padding: 15px;
border-radius: 8px;
margin-top: 10px;
animation: fadeIn 0.3s ease-in;
}
.message.error {
background: rgba(255, 107, 107, 0.1);
border: 1px solid var(--error-color);
color: var(--error-color);
}
.message.success {
background: rgba(81, 207, 102, 0.1);
border: 1px solid var(--success-color);
color: var(--success-color);
}
@keyframes fadeIn {{
from {{ opacity: 0; transform: translateY(-10px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
</style>
<script>
let pinTimer = null;
let pinExpiry = null;
async function requestAdminAuth() {
const nickname = document.getElementById('admin-nickname').value.trim();
if (!nickname) {
showMessage('Please enter your IRC nickname', 'error');
return;
}
// Check if nickname matches admin user
if (nickname.toLowerCase() !== '""" + ADMIN_USER.lower() + """') {
showMessage('Access denied. You are not an administrator.', 'error');
return;
}
try {
const response = await fetch('/admin/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nickname: nickname })
});
const result = await response.json();
if (result.success) {
showMessage('PIN sent! Check your IRC private messages.', 'success');
document.getElementById('auth-form').style.display = 'none';
document.getElementById('pin-section').style.display = 'block';
document.getElementById('admin-pin').focus();
// Start countdown timer (15 minutes)
startPinTimer(15 * 60);
} else {
showMessage(result.error || 'Authentication failed', 'error');
}
} catch (error) {
showMessage('Network error: ' + error.message, 'error');
}
}
async function verifyAdminPin() {
const nickname = document.getElementById('admin-nickname').value.trim();
const pin = document.getElementById('admin-pin').value.trim();
if (!pin || pin.length !== 6) {
showMessage('Please enter a 6-digit PIN', 'error');
return;
}
try {
const response = await fetch('/admin/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nickname: nickname, pin: pin })
});
const result = await response.json();
if (result.success) {
showMessage('Authentication successful! Redirecting...', 'success');
setTimeout(() => {
window.location.href = '/admin/dashboard';
}, 1000);
} else {
showMessage(result.error || 'Invalid PIN', 'error');
document.getElementById('admin-pin').value = '';
}
} catch (error) {
showMessage('Network error: ' + error.message, 'error');
}
}
function startPinTimer(seconds) {
pinExpiry = Date.now() + (seconds * 1000);
updateTimer();
if (pinTimer) clearInterval(pinTimer);
pinTimer = setInterval(updateTimer, 1000);
}
function updateTimer() {
const remaining = Math.max(0, Math.floor((pinExpiry - Date.now()) / 1000));
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const timerEl = document.getElementById('pin-timer');
timerEl.textContent = `PIN expires in: ${minutes}:${seconds.toString().padStart(2, '0')}`;
if (remaining <= 0) {
clearInterval(pinTimer);
timerEl.textContent = 'PIN expired. Please request a new one.';
document.getElementById('pin-section').style.display = 'none';
document.getElementById('auth-form').style.display = 'block';
}
}
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);
}
}
// Enter key handlers
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('admin-nickname').addEventListener('keypress', function(e) {
if (e.key === 'Enter') requestAdminAuth();
});
document.getElementById('admin-pin').addEventListener('keypress', function(e) {
if (e.key === 'Enter') verifyAdminPin();
});
});
</script>
"""
html = self.get_page_template("Admin Login", content, "")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def handle_admin_auth(self):
"""Handle admin authentication 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')
try:
import json
data = json.loads(post_data)
except json.JSONDecodeError:
self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400)
return
nickname = data.get('nickname', '').strip()
# Verify admin user
import sys
sys.path.append('.')
from config import ADMIN_USER
if nickname.lower() != ADMIN_USER.lower():
self.send_json_response({"success": False, "error": "Access denied"}, 403)
return
# Generate PIN
import random
pin = ''.join([str(random.randint(0, 9)) for _ in range(6)])
# Store PIN with expiration (15 minutes)
import time
expiry = time.time() + (15 * 60)
# Store in database for verification
import asyncio
result = asyncio.run(self._store_admin_pin_async(nickname, pin, expiry))
if result:
# Send PIN via IRC
self.send_admin_pin_via_irc(nickname, pin)
self.send_json_response({"success": True, "message": "PIN sent via IRC"})
else:
self.send_json_response({"success": False, "error": "Failed to generate PIN"}, 500)
except Exception as e:
print(f"Error in handle_admin_auth: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _store_admin_pin_async(self, nickname, pin, expiry):
"""Store admin PIN in database"""
try:
import aiosqlite
# Create temporary admin_pins table if it doesn't exist
async with aiosqlite.connect(self.database.db_path) as db:
# Create table if it doesn't exist
await db.execute("""
CREATE TABLE IF NOT EXISTS admin_pins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT NOT NULL,
pin_code TEXT NOT NULL,
expires_at REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Insert admin PIN
await db.execute("""
INSERT INTO admin_pins (nickname, pin_code, expires_at)
VALUES (?, ?, ?)
""", (nickname, pin, expiry))
await db.commit()
return True
except Exception as e:
print(f"Error storing admin PIN: {e}")
return False
def handle_admin_verify(self):
"""Handle admin PIN verification"""
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')
try:
import json
data = json.loads(post_data)
except json.JSONDecodeError:
self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400)
return
nickname = data.get('nickname', '').strip()
pin = data.get('pin', '').strip()
# Verify PIN
import asyncio
result = asyncio.run(self._verify_admin_pin_async(nickname, pin))
if result:
# Create session token
import hashlib
import time
session_token = hashlib.sha256(f"{nickname}:{pin}:{time.time()}".encode()).hexdigest()
# Store session
self.admin_sessions[session_token] = {
'nickname': nickname,
'expires': time.time() + (60 * 60) # 1 hour session
}
# Set cookie
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Set-Cookie', f'admin_session={session_token}; Path=/admin; HttpOnly')
self.end_headers()
self.wfile.write(json.dumps({"success": True}).encode())
else:
self.send_json_response({"success": False, "error": "Invalid or expired PIN"}, 401)
except Exception as e:
print(f"Error in handle_admin_verify: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _verify_admin_pin_async(self, nickname, pin):
"""Verify admin PIN from database"""
try:
import aiosqlite
import time
current_time = time.time()
# Check for valid PIN
async with aiosqlite.connect(self.database.db_path) as db:
cursor = await db.execute("""
SELECT pin_code FROM admin_pins
WHERE nickname = ? AND pin_code = ? AND expires_at > ?
""", (nickname, pin, current_time))
result = await cursor.fetchone()
if result:
# Delete used PIN
await db.execute("""
DELETE FROM admin_pins
WHERE nickname = ? AND pin_code = ?
""", (nickname, pin))
await db.commit()
return True
return False
except Exception as e:
print(f"Error verifying admin PIN: {e}")
return False
def send_admin_pin_via_irc(self, nickname, pin_code):
"""Send admin PIN to user via IRC private message"""
print(f"🔐 Admin 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
self.bot.send_message_sync(nickname, f"🔐 Admin Panel PIN: {pin_code}")
self.bot.send_message_sync(nickname, f"⚠️ This PIN expires in 15 minutes. Do not share it with anyone!")
self.bot.send_message_sync(nickname, f"💡 Enter this PIN at the admin login page to access the control panel.")
print(f"✅ Admin PIN sent to {nickname} via IRC")
except Exception as e:
print(f"❌ Failed to send admin PIN via IRC: {e}")
else:
print(f"❌ No IRC bot available to send admin PIN to {nickname}")
print(f"💡 Manual admin PIN for {nickname}: {pin_code}")
def check_admin_session(self):
"""Check if user has valid admin session"""
# Get cookie
cookie_header = self.headers.get('Cookie', '')
session_token = None
for cookie in cookie_header.split(';'):
if cookie.strip().startswith('admin_session='):
session_token = cookie.strip()[14:]
break
if not session_token:
return None
# Check if session is valid
import time
session = self.admin_sessions.get(session_token)
if session and session['expires'] > time.time():
# Extend session
session['expires'] = time.time() + (60 * 60)
return session['nickname']
# Invalid or expired session
if session_token in self.admin_sessions:
del self.admin_sessions[session_token]
return None
def serve_admin_dashboard(self):
"""Serve the admin dashboard page"""
# Check admin session
admin_user = self.check_admin_session()
if not admin_user:
# Redirect to login
self.send_response(302)
self.send_header('Location', '/admin')
self.end_headers()
return
# Get system statistics
import asyncio
stats = asyncio.run(self._get_admin_stats_async())
content = f"""
<div class="header">
<h1>🎮 Admin Control Panel</h1>
<p>Welcome, {admin_user}!</p>
</div>
<!-- System Statistics -->
<div class="card">
<h2>📊 System Statistics</h2>
<div class="grid grid-4">
<div class="stat-card">
<h3>👥 Total Players</h3>
<div class="stat-value">{stats['total_players']}</div>
</div>
<div class="stat-card">
<h3>🐾 Total Pets</h3>
<div class="stat-value">{stats['total_pets']}</div>
</div>
<div class="stat-card">
<h3>⚔️ Active Battles</h3>
<div class="stat-value">{stats['active_battles']}</div>
</div>
<div class="stat-card">
<h3>💾 Database Size</h3>
<div class="stat-value">{stats['db_size']}</div>
</div>
</div>
</div>
<!-- Player Management -->
<div class="card">
<h2>👥 Player Management</h2>
<div class="control-section">
<h3>Search Player</h3>
<input type="text" id="player-search" placeholder="Enter player nickname" class="form-input">
<button onclick="searchPlayer()" class="btn btn-primary">Search</button>
<div id="player-results" class="results-area"></div>
</div>
</div>
<!-- System Controls -->
<div class="card">
<h2>🔧 System Controls</h2>
<div class="grid grid-2">
<div class="control-section">
<h3>Database Management</h3>
<button onclick="createBackup()" class="btn btn-success">📂 Create Backup</button>
<button onclick="viewBackups()" class="btn btn-info">📋 View Backups</button>
<div id="backup-results" class="results-area"></div>
</div>
<div class="control-section">
<h3>IRC Management</h3>
<input type="text" id="irc-message" placeholder="Message to broadcast" class="form-input">
<button onclick="sendBroadcast()" class="btn btn-warning">📢 Broadcast</button>
<button onclick="getIrcStatus()" class="btn btn-info">📊 IRC Status</button>
<div id="irc-results" class="results-area"></div>
</div>
</div>
</div>
<!-- Game Management -->
<div class="card">
<h2>🌍 Game Management</h2>
<div class="grid grid-2">
<div class="control-section">
<h3>Weather Control</h3>
<select id="weather-location" class="form-input">
<option value="">Select Location</option>
<option value="Starter Town">Starter Town</option>
<option value="Whispering Woods">Whispering Woods</option>
<option value="Electric Canyon">Electric Canyon</option>
<option value="Crystal Caves">Crystal Caves</option>
<option value="Frozen Tundra">Frozen Tundra</option>
<option value="Dragon's Peak">Dragon's Peak</option>
</select>
<select id="weather-type" class="form-input">
<option value="">Select Weather</option>
<option value="sunny">☀️ Sunny</option>
<option value="rainy">🌧️ Rainy</option>
<option value="storm">⛈️ Storm</option>
<option value="blizzard">❄️ Blizzard</option>
<option value="earthquake">🌍 Earthquake</option>
<option value="calm">🌤️ Calm</option>
</select>
<button onclick="setWeather()" class="btn btn-primary">🌤️ Set Weather</button>
<div id="weather-results" class="results-area"></div>
</div>
<div class="control-section">
<h3>Rate Limiting</h3>
<input type="text" id="rate-user" placeholder="Username (optional)" class="form-input">
<button onclick="getRateStats()" class="btn btn-info">📊 Rate Stats</button>
<button onclick="resetRates()" class="btn btn-warning">🔄 Reset Rates</button>
<div id="rate-results" class="results-area"></div>
</div>
</div>
</div>
<style>
.stat-card {{
background: var(--bg-tertiary);
padding: 20px;
border-radius: 10px;
text-align: center;
border: 1px solid var(--border-color);
}}
.stat-card h3 {{
margin: 0 0 10px 0;
color: var(--accent-blue);
font-size: 1em;
}}
.stat-value {{
font-size: 2em;
font-weight: bold;
color: var(--text-accent);
}}
.control-section {{
margin-bottom: 20px;
}}
.control-section h3 {{
margin: 0 0 15px 0;
color: var(--accent-blue);
border-bottom: 1px solid var(--border-color);
padding-bottom: 5px;
}}
.form-input {{
width: 100%;
padding: 10px;
margin: 5px 0;
border: 1px solid var(--border-color);
border-radius: 5px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}}
.btn {{
padding: 10px 15px;
margin: 5px 5px 5px 0;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-block;
transition: background-color 0.3s;
}}
.btn-primary {{
background: var(--accent-blue);
color: white;
}}
.btn-success {{
background: #28a745;
color: white;
}}
.btn-warning {{
background: #ffc107;
color: #212529;
}}
.btn-info {{
background: #17a2b8;
color: white;
}}
.btn:hover {{
opacity: 0.8;
}}
.results-area {{
margin-top: 15px;
padding: 10px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 5px;
min-height: 40px;
display: none;
}}
.results-area.show {{
display: block;
}}
.irc-status-display {{
font-family: monospace;
font-size: 14px;
}}
.status-section {{
margin-bottom: 20px;
padding: 15px;
background: var(--bg-tertiary);
border-radius: 8px;
border: 1px solid var(--border-color);
}}
.status-section h4 {{
margin: 0 0 10px 0;
color: var(--accent-blue);
border-bottom: 1px solid var(--border-color);
padding-bottom: 5px;
}}
.status-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}}
.status-grid span {{
padding: 5px;
background: var(--bg-primary);
border-radius: 4px;
font-size: 13px;
}}
.expandable-section {{
margin: 10px 0;
}}
.expand-btn {{
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
width: 100%;
text-align: left;
font-size: 14px;
}}
.expand-btn:hover {{
background: var(--bg-tertiary);
}}
.expandable-content {{
margin-top: 10px;
padding: 10px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}}
.player-list {{
color: var(--text-secondary);
}}
.player-item {{
padding: 3px 0;
color: var(--text-secondary);
font-size: 13px;
}}
.activity-item {{
padding: 2px 0;
color: var(--text-secondary);
font-size: 12px;
border-bottom: 1px solid var(--border-color);
}}
.activity-item:last-child {{
border-bottom: none;
}}
.player-edit-form {{
background: var(--bg-tertiary);
border-radius: 8px;
padding: 15px;
border: 1px solid var(--border-color);
}}
.player-header {{
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}}
.player-header h4 {{
margin: 0 0 8px 0;
color: var(--accent-blue);
}}
.player-stats {{
display: flex;
gap: 15px;
font-size: 14px;
color: var(--text-secondary);
}}
.edit-section {{
margin-top: 15px;
}}
.edit-section h5 {{
margin: 0 0 10px 0;
color: var(--accent-blue);
font-size: 16px;
}}
.edit-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 15px;
}}
.edit-field {{
display: flex;
flex-direction: column;
}}
.edit-field label {{
font-weight: bold;
margin-bottom: 5px;
color: var(--text-primary);
font-size: 14px;
}}
.edit-input {{
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}}
.edit-input:focus {{
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.3);
}}
.edit-actions {{
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
}}
.success-message {{
color: #28a745;
font-weight: bold;
}}
.error-message {{
color: #dc3545;
font-weight: bold;
}}
.info-message {{
color: var(--accent-blue);
}}
</style>
<script>
function showResults(elementId, content, type = 'info') {{
const resultsEl = document.getElementById(elementId);
resultsEl.innerHTML = `<div class="${{type}}-message">${{content}}</div>`;
resultsEl.classList.add('show');
}}
function hideResults(elementId) {{
const resultsEl = document.getElementById(elementId);
resultsEl.classList.remove('show');
}}
async function searchPlayer() {{
const nickname = document.getElementById('player-search').value.trim();
if (!nickname) {{
showResults('player-results', 'Please enter a player nickname', 'error');
return;
}}
try {{
showResults('player-results', 'Searching...', 'info');
const response = await fetch(`/admin/api/player/${{nickname}}`);
const result = await response.json();
if (result.success) {{
const player = result.player;
let html = `<div class="player-edit-form">`;
// Player info header (read-only)
html += `<div class="player-header">`;
html += `<h4>👤 Player: ${{player.nickname}}</h4>`;
html += `<div class="player-stats">`;
html += `<span>Pets: ${{player.active_pets}}/${{player.total_pets}} active</span>`;
html += `<span>Location: ${{player.current_location}}</span>`;
html += `</div></div>`;
// Editable fields
html += `<div class="edit-section">`;
html += `<h5>✏️ Edit Player Data</h5>`;
html += `<div class="edit-grid">`;
html += `<div class="edit-field">`;
html += `<label>Level:</label>`;
html += `<input type="number" id="edit-level" value="${{player.level}}" min="1" max="100" class="edit-input">`;
html += `</div>`;
html += `<div class="edit-field">`;
html += `<label>Experience:</label>`;
html += `<input type="number" id="edit-experience" value="${{player.experience}}" min="0" class="edit-input">`;
html += `</div>`;
html += `<div class="edit-field">`;
html += `<label>Money:</label>`;
html += `<input type="number" id="edit-money" value="${{player.money}}" min="0" class="edit-input">`;
html += `</div>`;
html += `</div>`;
// Save button
html += `<div class="edit-actions">`;
html += `<button onclick="savePlayerChanges('${{player.nickname}}')" class="btn btn-success">💾 Save Changes</button>`;
html += `<button onclick="searchPlayer()" class="btn btn-info">🔄 Refresh</button>`;
html += `</div>`;
html += `</div></div>`;
showResults('player-results', html, 'success');
}} else {{
showResults('player-results', result.error || 'Player not found', 'error');
}}
}} catch (error) {{
showResults('player-results', 'Error searching player: ' + error.message, 'error');
}}
}}
async function savePlayerChanges(nickname) {{
try {{
const level = parseInt(document.getElementById('edit-level').value);
const experience = parseInt(document.getElementById('edit-experience').value);
const money = parseInt(document.getElementById('edit-money').value);
// Basic validation
if (isNaN(level) || level < 1 || level > 100) {{
showResults('player-results', 'Level must be between 1 and 100', 'error');
return;
}}
if (isNaN(experience) || experience < 0) {{
showResults('player-results', 'Experience cannot be negative', 'error');
return;
}}
if (isNaN(money) || money < 0) {{
showResults('player-results', 'Money cannot be negative', 'error');
return;
}}
showResults('player-results', 'Saving changes...', 'info');
const response = await fetch(`/admin/api/player/${{nickname}}/update`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
level: level,
experience: experience,
money: money
}})
}});
const result = await response.json();
if (result.success) {{
showResults('player-results', 'Changes saved successfully! Click Refresh to see updated data.', 'success');
}} else {{
showResults('player-results', result.error || 'Failed to save changes', 'error');
}}
}} catch (error) {{
showResults('player-results', 'Error saving changes: ' + error.message, 'error');
}}
}}
async function createBackup() {{
try {{
showResults('backup-results', 'Creating backup...', 'info');
const response = await fetch('/admin/api/backup', {{ method: 'POST' }});
const result = await response.json();
if (result.success) {{
showResults('backup-results', `Backup created: ${{result.filename}}`, 'success');
}} else {{
showResults('backup-results', result.error || 'Backup failed', 'error');
}}
}} catch (error) {{
showResults('backup-results', 'Error creating backup: ' + error.message, 'error');
}}
}}
async function viewBackups() {{
try {{
showResults('backup-results', 'Loading backups...', 'info');
const response = await fetch('/admin/api/backups');
const result = await response.json();
if (result.success) {{
let html = '<strong>Available Backups:</strong><br>';
result.backups.forEach(backup => {{
html += `${{backup.name}} (${{backup.size}})<br>`;
}});
showResults('backup-results', html, 'success');
}} else {{
showResults('backup-results', result.error || 'Failed to load backups', 'error');
}}
}} catch (error) {{
showResults('backup-results', 'Error loading backups: ' + error.message, 'error');
}}
}}
async function sendBroadcast() {{
const message = document.getElementById('irc-message').value.trim();
if (!message) {{
showResults('irc-results', 'Please enter a message', 'error');
return;
}}
try {{
showResults('irc-results', 'Sending broadcast...', 'info');
const response = await fetch('/admin/api/broadcast', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ message: message }})
}});
const result = await response.json();
if (result.success) {{
showResults('irc-results', 'Broadcast sent successfully!', 'success');
document.getElementById('irc-message').value = '';
}} else {{
showResults('irc-results', result.error || 'Broadcast failed', 'error');
}}
}} catch (error) {{
showResults('irc-results', 'Error sending broadcast: ' + error.message, 'error');
}}
}}
async function getIrcStatus() {{
try {{
showResults('irc-results', 'Getting IRC status...', 'info');
const response = await fetch('/admin/api/irc-status');
const result = await response.json();
if (result.success && result.irc_status) {{
const status = result.irc_status;
let html = `<div class="irc-status-display">`;
// Connection Status
html += `<div class="status-section">`;
html += `<h4>🔗 Connection Status</h4>`;
html += `<div class="status-grid">`;
html += `<span>Status: <strong>${{status.connected ? '🟢 Connected' : '🔴 Disconnected'}}</strong></span>`;
html += `<span>State: <strong>${{status.state || 'unknown'}}</strong></span>`;
html += `<span>Server: <strong>${{status.server || 'N/A'}}</strong></span>`;
html += `<span>Channel: <strong>${{status.channel || 'N/A'}}</strong></span>`;
html += `<span>Uptime: <strong>${{status.uptime || 'N/A'}}</strong></span>`;
html += `</div></div>`;
// Performance Metrics
html += `<div class="status-section">`;
html += `<h4>📊 Performance Metrics</h4>`;
html += `<div class="status-grid">`;
html += `<span>Messages: <strong>${{status.message_count || 0}}</strong></span>`;
html += `<span>Commands: <strong>${{status.command_count || 0}}</strong></span>`;
html += `<span>Commands/min: <strong>${{status.commands_per_minute || 0}}</strong></span>`;
html += `<span>Reconnections: <strong>${{status.total_reconnections || 0}}</strong></span>`;
html += `<span>Failures: <strong>${{status.connection_failures || 0}}</strong></span>`;
html += `</div></div>`;
// Channel Activity
html += `<div class="status-section">`;
html += `<h4>👥 Channel Activity</h4>`;
html += `<div class="status-grid">`;
html += `<span>Active Players: <strong>${{status.active_players_count || 0}}</strong></span>`;
html += `<span>Recent Players: <strong>${{status.recent_players_count || 0}}</strong></span>`;
if (status.channel_topic) {{
html += `<span>Topic: <strong>${{status.channel_topic}}</strong></span>`;
}}
html += `</div>`;
// Expandable Active Players List
if (status.active_players && status.active_players.length > 0) {{
html += `<div class="expandable-section">`;
html += `<button class="expand-btn" onclick="toggleSection('active-players')">`;
html += `<span id="active-players-icon">+</span> Active Players (${{status.active_players.length}})`;
html += `</button>`;
html += `<div id="active-players" class="expandable-content" style="display: none;">`;
html += `<div class="player-list">${{status.active_players.join(', ')}}</div>`;
html += `</div></div>`;
}}
// Expandable Recent Players List
if (status.recent_players && status.recent_players.length > 0) {{
html += `<div class="expandable-section">`;
html += `<button class="expand-btn" onclick="toggleSection('recent-players')">`;
html += `<span id="recent-players-icon">+</span> Recent Players (${{status.recent_players.length}})`;
html += `</button>`;
html += `<div id="recent-players" class="expandable-content" style="display: none;">`;
status.recent_players.forEach(player => {{
const activeIcon = player.is_active ? '🟢' : '';
html += `<div class="player-item">${{activeIcon}} ${{player.nickname}} (${{player.minutes_ago}}m ago)</div>`;
}});
html += `</div></div>`;
}}
html += `</div>`;
// Recent Activity
html += `<div class="status-section">`;
html += `<h4>📝 Recent Activity</h4>`;
// Recent Commands
if (status.recent_commands && status.recent_commands.length > 0) {{
html += `<div class="expandable-section">`;
html += `<button class="expand-btn" onclick="toggleSection('recent-commands')">`;
html += `<span id="recent-commands-icon">+</span> Recent Commands (${{status.recent_commands.length}})`;
html += `</button>`;
html += `<div id="recent-commands" class="expandable-content" style="display: none;">`;
status.recent_commands.slice(-10).reverse().forEach(cmd => {{
const time = new Date(cmd.timestamp).toLocaleTimeString();
html += `<div class="activity-item">${{time}} - ${{cmd.nickname}}: ${{cmd.command}}</div>`;
}});
html += `</div></div>`;
}}
// Recent General Activity
if (status.recent_activity && status.recent_activity.length > 0) {{
html += `<div class="expandable-section">`;
html += `<button class="expand-btn" onclick="toggleSection('recent-activity')">`;
html += `<span id="recent-activity-icon">+</span> Recent Messages (${{status.recent_activity.length}})`;
html += `</button>`;
html += `<div id="recent-activity" class="expandable-content" style="display: none;">`;
status.recent_activity.slice(-10).reverse().forEach(activity => {{
const time = new Date(activity.timestamp).toLocaleTimeString();
const content = activity.content ? `: ${{activity.content.substring(0, 50)}}...` : '';
html += `<div class="activity-item">${{time}} - ${{activity.nickname}}${{content}}</div>`;
}});
html += `</div></div>`;
}}
html += `</div>`;
html += `</div>`;
showResults('irc-results', html, 'success');
}} else {{
showResults('irc-results', result.error || 'Failed to get IRC status', 'error');
}}
}} catch (error) {{
showResults('irc-results', 'Error getting IRC status: ' + error.message, 'error');
}}
}}
function toggleSection(sectionId) {{
const content = document.getElementById(sectionId);
const icon = document.getElementById(sectionId + '-icon');
if (content.style.display === 'none') {{
content.style.display = 'block';
icon.textContent = '-';
}} else {{
content.style.display = 'none';
icon.textContent = '+';
}}
}}
async function setWeather() {{
const location = document.getElementById('weather-location').value;
const weather = document.getElementById('weather-type').value;
if (!location || !weather) {{
showResults('weather-results', 'Please select both location and weather type', 'error');
return;
}}
try {{
showResults('weather-results', 'Setting weather...', 'info');
const response = await fetch('/admin/api/weather', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ location: location, weather: weather }})
}});
const result = await response.json();
if (result.success) {{
showResults('weather-results', `Weather set to ${{weather}} in ${{location}}`, 'success');
}} else {{
showResults('weather-results', result.error || 'Failed to set weather', 'error');
}}
}} catch (error) {{
showResults('weather-results', 'Error setting weather: ' + error.message, 'error');
}}
}}
async function getRateStats() {{
const username = document.getElementById('rate-user').value.trim();
try {{
showResults('rate-results', 'Loading rate stats...', 'info');
let url = '/admin/api/rate-stats';
if (username) {{
url += `?user=${{username}}`;
}}
const response = await fetch(url);
const result = await response.json();
if (result.success) {{
let html = '<strong>Rate Limiting Stats:</strong><br>';
if (username) {{
html += `User: ${{username}}<br>`;
html += `Violations: ${{result.stats.violations || 0}}<br>`;
html += `Banned: ${{result.stats.banned ? 'Yes' : 'No'}}`;
}} else {{
html += `Total Users: ${{result.stats.total_users || 0}}<br>`;
html += `Active Bans: ${{result.stats.active_bans || 0}}<br>`;
html += `Total Violations: ${{result.stats.total_violations || 0}}`;
}}
showResults('rate-results', html, 'success');
}} else {{
showResults('rate-results', result.error || 'Failed to get rate stats', 'error');
}}
}} catch (error) {{
showResults('rate-results', 'Error getting rate stats: ' + error.message, 'error');
}}
}}
async function resetRates() {{
const username = document.getElementById('rate-user').value.trim();
try {{
showResults('rate-results', 'Resetting rate limits...', 'info');
const response = await fetch('/admin/api/rate-reset', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ user: username || null }})
}});
const result = await response.json();
if (result.success) {{
showResults('rate-results', result.message || 'Rate limits reset successfully', 'success');
}} else {{
showResults('rate-results', result.error || 'Failed to reset rate limits', 'error');
}}
}} catch (error) {{
showResults('rate-results', 'Error resetting rate limits: ' + error.message, 'error');
}}
}}
</script>
"""
html = self.get_page_template("Admin Dashboard", content, "")
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def _get_location_options(self, locations):
"""Generate HTML options for locations"""
options = ""
for location in locations:
options += f'<option value="{location["name"]}">{location["name"]}</option>'
return options
async def _get_admin_stats_async(self):
"""Get admin dashboard statistics"""
import aiosqlite
import os
stats = {
'total_players': 0,
'total_pets': 0,
'active_battles': 0,
'db_size': '0 MB',
'total_achievements': 0,
'total_badges': 0,
'total_items': 0,
'locations': []
}
try:
async with aiosqlite.connect(self.database.db_path) as db:
# Get player count
cursor = await db.execute("SELECT COUNT(*) FROM players")
stats['total_players'] = (await cursor.fetchone())[0]
# Get pet count
cursor = await db.execute("SELECT COUNT(*) FROM pets")
stats['total_pets'] = (await cursor.fetchone())[0]
# Get active battles (if table exists)
try:
cursor = await db.execute("SELECT COUNT(*) FROM active_battles")
stats['active_battles'] = (await cursor.fetchone())[0]
except:
stats['active_battles'] = 0
# Get achievement count
try:
cursor = await db.execute("SELECT COUNT(*) FROM player_achievements")
stats['total_achievements'] = (await cursor.fetchone())[0]
except:
stats['total_achievements'] = 0
# Get badge count (if table exists)
try:
cursor = await db.execute("SELECT COUNT(*) FROM player_badges")
stats['total_badges'] = (await cursor.fetchone())[0]
except:
stats['total_badges'] = 0
# Get item count (if table exists)
try:
cursor = await db.execute("SELECT COUNT(*) FROM player_items")
stats['total_items'] = (await cursor.fetchone())[0]
except:
stats['total_items'] = 0
# Get locations
cursor = await db.execute("SELECT id, name FROM locations ORDER BY name")
locations = await cursor.fetchall()
stats['locations'] = [{'id': loc[0], 'name': loc[1]} for loc in locations]
# Get database size
if os.path.exists(self.database.db_path):
size_bytes = os.path.getsize(self.database.db_path)
stats['db_size'] = f"{size_bytes / 1024 / 1024:.2f} MB"
except Exception as e:
print(f"Error getting admin stats: {e}")
return stats
def handle_admin_api(self, endpoint):
"""Handle admin API requests"""
print(f"Admin API request: {endpoint}")
# Check admin session
admin_user = self.check_admin_session()
if not admin_user:
print(f"Unauthorized admin API request for endpoint: {endpoint}")
self.send_json_response({"success": False, "error": "Unauthorized"}, 401)
return
print(f"Authorized admin API request from {admin_user} for endpoint: {endpoint}")
# Get POST data if it's a POST request
content_length = int(self.headers.get('Content-Length', 0))
post_data = {}
if content_length > 0 and self.command == 'POST':
try:
import json
post_data = json.loads(self.rfile.read(content_length).decode('utf-8'))
except:
pass
# Parse query parameters for GET requests
query_params = {}
if '?' in endpoint:
endpoint, query_string = endpoint.split('?', 1)
for param in query_string.split('&'):
if '=' in param:
key, value = param.split('=', 1)
query_params[key] = value
# Route to appropriate handler
if endpoint.startswith('player/'):
# Handle player endpoints - support both info and updates
player_path = endpoint[7:] # Remove 'player/' prefix
if player_path.endswith('/update'):
# Player update endpoint
player_name = player_path[:-7] # Remove '/update' suffix
if self.command == 'POST':
self.handle_admin_player_update(player_name, post_data)
else:
self.send_json_response({"success": False, "error": "Method not allowed"}, 405)
else:
# Player info endpoint
self.handle_admin_player_get(player_path)
elif endpoint == 'backup':
if self.command == 'POST':
self.handle_admin_backup_create()
else:
self.send_json_response({"success": False, "error": "Method not allowed"}, 405)
elif endpoint == 'backups':
self.handle_admin_backups_list()
elif endpoint == 'broadcast':
self.handle_admin_broadcast(post_data)
elif endpoint == 'irc-status':
print(f"IRC status endpoint hit!")
# Add a simple test first
try:
print(f"Calling handle_admin_irc_status...")
self.handle_admin_irc_status()
print(f"handle_admin_irc_status completed")
except Exception as e:
print(f"Exception in IRC status handler: {e}")
import traceback
traceback.print_exc()
# Send a simple fallback response
self.send_json_response({
"success": False,
"error": "IRC status handler failed",
"details": str(e)
}, 500)
elif endpoint == 'weather':
self.handle_admin_weather_set(post_data)
elif endpoint == 'rate-stats':
self.handle_admin_rate_stats(query_params)
elif endpoint == 'rate-reset':
self.handle_admin_rate_reset(post_data)
elif endpoint == 'test':
# Simple test endpoint
import datetime
self.send_json_response({
"success": True,
"message": "Test endpoint working",
"timestamp": str(datetime.datetime.now())
})
else:
self.send_json_response({"success": False, "error": "Unknown endpoint"}, 404)
def handle_admin_player_search(self, data):
"""Search for a player"""
nickname = data.get('nickname', '').strip()
if not nickname:
self.send_json_response({"success": False, "error": "No nickname provided"})
return
import asyncio
player = asyncio.run(self.database.get_player(nickname))
if player:
# Get additional stats
pet_count = asyncio.run(self._get_player_pet_count_async(player['id']))
self.send_json_response({
"success": True,
"player": {
"nickname": player['nickname'],
"level": player['level'],
"money": player['money'],
"pet_count": pet_count,
"experience": player['experience']
}
})
else:
self.send_json_response({"success": False, "error": "Player not found"})
async def _get_player_pet_count_async(self, player_id):
"""Get player's pet count"""
import aiosqlite
async with aiosqlite.connect(self.database.db_path) as db:
cursor = await db.execute("SELECT COUNT(*) FROM pets WHERE player_id = ?", (player_id,))
return (await cursor.fetchone())[0]
def handle_admin_backup_create(self):
"""Create a database backup"""
if self.bot and hasattr(self.bot, 'backup_manager'):
import asyncio
result = asyncio.run(self.bot.backup_manager.create_backup("manual", "Admin web interface"))
if result['success']:
self.send_json_response({
"success": True,
"message": f"Backup created: {result['filename']}"
})
else:
self.send_json_response({
"success": False,
"error": result.get('error', 'Backup failed')
})
else:
self.send_json_response({
"success": False,
"error": "Backup system not available"
})
def handle_admin_weather_set(self, data):
"""Set weather for a location"""
location = data.get('location', '').strip()
weather = data.get('weather', '').strip()
if not location or not weather:
self.send_json_response({"success": False, "error": "Missing location or weather"})
return
# Execute weather change using database directly
try:
import asyncio
result = asyncio.run(self._set_weather_for_location_async(location, weather))
if result.get("success"):
self.send_json_response({
"success": True,
"message": result.get("message", f"Weather set to {weather} in {location}")
})
else:
self.send_json_response({
"success": False,
"error": result.get("error", "Failed to set weather")
})
except Exception as e:
print(f"Error setting weather: {e}")
self.send_json_response({
"success": False,
"error": f"Weather system error: {str(e)}"
})
async def _set_weather_for_location_async(self, location, weather):
"""Async helper to set weather for location"""
try:
import json
import datetime
import random
# Load weather patterns
try:
with open("config/weather_patterns.json", "r") as f:
weather_data = json.load(f)
except FileNotFoundError:
return {
"success": False,
"error": "Weather configuration file not found"
}
# Validate weather type
weather_types = list(weather_data["weather_types"].keys())
if weather not in weather_types:
return {
"success": False,
"error": f"Invalid weather type. Valid types: {', '.join(weather_types)}"
}
weather_config = weather_data["weather_types"][weather]
# Calculate duration (3 hours = 180 minutes)
duration_range = weather_config.get("duration_minutes", [90, 180])
duration = random.randint(duration_range[0], duration_range[1])
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration)
# Set weather for the location
result = await self.database.set_weather_for_location(
location, weather, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
)
if result.get("success"):
# Announce weather change if it actually changed and bot is available
if result.get("changed") and self.bot and hasattr(self.bot, 'game_engine'):
await self.bot.game_engine.announce_weather_change(
location, result.get("previous_weather"), weather, "web"
)
return {
"success": True,
"message": f"Weather set to {weather} in {location} for {duration} minutes"
}
else:
return {
"success": False,
"error": result.get("error", "Failed to set weather")
}
except Exception as e:
print(f"Error in _set_weather_for_location_async: {e}")
return {
"success": False,
"error": str(e)
}
def handle_admin_announce(self, data):
"""Send announcement to IRC"""
message = data.get('message', '').strip()
if not message:
self.send_json_response({"success": False, "error": "No message provided"})
return
if self.bot and hasattr(self.bot, 'send_message_sync'):
try:
# Send to main channel
self.bot.send_message_sync("#petz", f"📢 ANNOUNCEMENT: {message}")
self.send_json_response({"success": True, "message": "Announcement sent"})
except Exception as e:
self.send_json_response({"success": False, "error": str(e)})
else:
self.send_json_response({"success": False, "error": "IRC not available"})
def handle_admin_monitor(self, monitor_type):
"""Get monitoring data"""
# TODO: Implement real monitoring data
self.send_json_response({
"success": True,
"type": monitor_type,
"data": []
})
def handle_admin_player_get(self, nickname):
"""Get player information"""
if not nickname:
self.send_json_response({"success": False, "error": "No nickname provided"}, 400)
return
try:
import asyncio
result = asyncio.run(self._get_player_info_async(nickname))
if result["success"]:
self.send_json_response(result)
else:
self.send_json_response(result, 404)
except Exception as e:
print(f"Error in handle_admin_player_get: {e}")
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
def handle_admin_player_update(self, nickname, data):
"""Update player information"""
if not nickname:
self.send_json_response({"success": False, "error": "No nickname provided"}, 400)
return
if not data:
self.send_json_response({"success": False, "error": "No update data provided"}, 400)
return
try:
import asyncio
result = asyncio.run(self._update_player_async(nickname, data))
if result["success"]:
self.send_json_response(result)
else:
self.send_json_response(result, 400)
except Exception as e:
print(f"Error in handle_admin_player_update: {e}")
import traceback
traceback.print_exc()
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
async def _update_player_async(self, nickname, data):
"""Update player information asynchronously"""
try:
# Validate input data
allowed_fields = ['level', 'experience', 'money']
updates = {}
for field in allowed_fields:
if field in data:
value = data[field]
# Validate each field
if field == 'level':
if not isinstance(value, int) or value < 1 or value > 100:
return {"success": False, "error": "Level must be between 1 and 100"}
updates[field] = value
elif field == 'experience':
if not isinstance(value, int) or value < 0:
return {"success": False, "error": "Experience cannot be negative"}
updates[field] = value
elif field == 'money':
if not isinstance(value, int) or value < 0:
return {"success": False, "error": "Money cannot be negative"}
updates[field] = value
if not updates:
return {"success": False, "error": "No valid fields to update"}
# Check if player exists
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Update player data
success = await self.database.update_player_admin(player["id"], updates)
if success:
return {
"success": True,
"message": f"Updated {', '.join(updates.keys())} for player {nickname}",
"updated_fields": list(updates.keys())
}
else:
return {"success": False, "error": "Failed to update player data"}
except Exception as e:
print(f"Error updating player: {e}")
return {"success": False, "error": str(e)}
async def _get_player_info_async(self, nickname):
"""Get player information asynchronously"""
try:
player = await self.database.get_player(nickname)
if not player:
return {"success": False, "error": "Player not found"}
# Get additional stats
pets = await self.database.get_player_pets(player["id"])
# Get location name if current_location_id is set
location_name = "Unknown"
if player.get("current_location_id"):
try:
location_data = await self.database.get_location_by_id(player["current_location_id"])
if location_data:
location_name = location_data["name"]
else:
location_name = f"Location ID {player['current_location_id']}"
except Exception as loc_error:
print(f"Error resolving location: {loc_error}")
location_name = f"Location ID {player['current_location_id']}"
# Get team composition
team_info = await self.database.get_team_composition(player["id"])
return {
"success": True,
"player": {
"nickname": player["nickname"],
"level": player["level"],
"experience": player["experience"],
"money": player["money"],
"current_location": location_name,
"pet_count": len(pets),
"active_pets": team_info.get("active_pets", 0),
"total_pets": team_info.get("total_pets", 0)
}
}
except Exception as e:
print(f"Error getting player info: {e}")
return {"success": False, "error": str(e)}
def handle_admin_backups_list(self):
"""List available backups"""
try:
import os
backup_dir = "backups"
if not os.path.exists(backup_dir):
self.send_json_response({
"success": True,
"backups": []
})
return
backups = []
for filename in os.listdir(backup_dir):
if filename.endswith('.gz'):
filepath = os.path.join(backup_dir, filename)
size = os.path.getsize(filepath)
# Convert size to human readable format
if size < 1024:
size_str = f"{size} B"
elif size < 1024 * 1024:
size_str = f"{size / 1024:.1f} KB"
else:
size_str = f"{size / (1024 * 1024):.1f} MB"
backups.append({
"name": filename,
"size": size_str
})
# Sort by name (newest first)
backups.sort(key=lambda x: x['name'], reverse=True)
self.send_json_response({
"success": True,
"backups": backups[:10] # Show only last 10 backups
})
except Exception as e:
print(f"Error listing backups: {e}")
self.send_json_response({"success": False, "error": str(e)}, 500)
def handle_admin_broadcast(self, data):
"""Send IRC broadcast message"""
message = data.get('message', '').strip()
if not message:
self.send_json_response({"success": False, "error": "No message provided"}, 400)
return
try:
# Send to IRC channel via bot
if self.bot and hasattr(self.bot, 'send_message_sync'):
from config import IRC_CONFIG
channel = IRC_CONFIG.get("channel", "#petz")
self.bot.send_message_sync(channel, f"📢 Admin Announcement: {message}")
self.send_json_response({
"success": True,
"message": "Broadcast sent successfully"
})
else:
self.send_json_response({"success": False, "error": "IRC bot not available"}, 500)
except Exception as e:
print(f"Error sending broadcast: {e}")
self.send_json_response({"success": False, "error": str(e)}, 500)
def handle_admin_irc_status(self):
"""Get comprehensive IRC connection status and activity"""
try:
print(f"IRC status request - checking bot availability...")
bot = self.bot
print(f"Bot instance: {bot}")
if bot and hasattr(bot, 'connection_manager') and bot.connection_manager:
connection_manager = bot.connection_manager
print(f"Connection manager: {connection_manager}")
if hasattr(connection_manager, 'get_connection_stats'):
try:
stats = connection_manager.get_connection_stats()
print(f"Got comprehensive connection stats")
# Return comprehensive IRC status
response_data = {
"success": True,
"irc_status": stats
}
print(f"Sending comprehensive IRC response")
self.send_json_response(response_data)
except Exception as stats_error:
print(f"Error getting connection stats: {stats_error}")
import traceback
traceback.print_exc()
self.send_json_response({
"success": False,
"error": f"Failed to get connection stats: {str(stats_error)}"
}, 500)
else:
print("Connection manager has no get_connection_stats method")
self.send_json_response({
"success": False,
"error": "Connection manager missing get_connection_stats method"
}, 500)
else:
print("No bot instance or connection manager available")
self.send_json_response({
"success": True,
"irc_status": {
"connected": False,
"state": "disconnected",
"error": "Bot instance or connection manager not available"
}
})
except Exception as e:
print(f"Error getting IRC status: {e}")
import traceback
traceback.print_exc()
try:
self.send_json_response({"success": False, "error": str(e)}, 500)
except Exception as json_error:
print(f"Failed to send JSON error response: {json_error}")
# Send a basic HTTP error response if JSON fails
self.send_error(500, f"IRC status error: {str(e)}")
def handle_admin_rate_stats(self, query_params):
"""Get rate limiting statistics"""
try:
username = query_params.get('user', '').strip()
if self.bot and hasattr(self.bot, 'rate_limiter') and self.bot.rate_limiter:
if username:
# Get stats for specific user
user_stats = self.bot.rate_limiter.get_user_stats(username)
self.send_json_response({
"success": True,
"stats": {
"violations": user_stats.get("violations", 0),
"banned": user_stats.get("banned", False),
"last_violation": user_stats.get("last_violation", "Never")
}
})
else:
# Get global stats
global_stats = self.bot.rate_limiter.get_global_stats()
self.send_json_response({
"success": True,
"stats": {
"total_users": global_stats.get("total_users", 0),
"active_bans": global_stats.get("active_bans", 0),
"total_violations": global_stats.get("total_violations", 0)
}
})
else:
self.send_json_response({
"success": True,
"stats": {
"total_users": 0,
"active_bans": 0,
"total_violations": 0
}
})
except Exception as e:
print(f"Error getting rate stats: {e}")
self.send_json_response({"success": False, "error": str(e)}, 500)
def handle_admin_rate_reset(self, data):
"""Reset rate limiting for user or globally"""
try:
username = data.get('user', '').strip() if data.get('user') else None
if self.bot and hasattr(self.bot, 'rate_limiter') and self.bot.rate_limiter:
if username:
# Reset for specific user
success = self.bot.rate_limiter.reset_user(username)
if success:
self.send_json_response({
"success": True,
"message": f"Rate limits reset for user: {username}"
})
else:
self.send_json_response({"success": False, "error": f"User {username} not found"}, 404)
else:
# Global reset
self.bot.rate_limiter.reset_all()
self.send_json_response({
"success": True,
"message": "All rate limits reset successfully"
})
else:
self.send_json_response({"success": False, "error": "Rate limiter not available"}, 500)
except Exception as e:
print(f"Error resetting rate limits: {e}")
self.send_json_response({"success": False, "error": str(e)}, 500)
# ================================================================
# NEW TEAM BUILDER METHODS - Separated Team Management
# ================================================================
def serve_team_selection_hub(self, nickname):
"""Serve the team selection hub showing all teams with swap options."""
try:
# Get database and bot from server
database = self.server.database if hasattr(self.server, 'database') else None
bot = self.server.bot if hasattr(self.server, 'bot') else None
if not database:
self.send_error(500, "Database not available")
return
# Get team management service
if not hasattr(self, 'team_service'):
from src.team_management import TeamManagementService
from src.pin_authentication import PinAuthenticationService
pin_service = PinAuthenticationService(database, bot)
self.team_service = TeamManagementService(database, pin_service)
# Get team overview
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
player = loop.run_until_complete(database.get_player(nickname))
if not player:
self.send_error(404, "Player not found")
return
team_overview = loop.run_until_complete(self.team_service.get_team_overview(player["id"]))
if not team_overview["success"]:
self.send_error(500, f"Failed to load teams: {team_overview['error']}")
return
teams = team_overview["teams"]
finally:
loop.close()
# Generate team hub HTML
content = self.generate_team_hub_content(nickname, teams)
full_page = self.get_page_template(f"Team Management - {nickname}", content, "teambuilder")
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(full_page.encode('utf-8'))
except Exception as e:
print(f"Error serving team selection hub: {e}")
self.send_error(500, "Internal server error")
def serve_individual_team_editor(self, nickname, team_identifier):
"""Serve individual team editor page."""
try:
# Get database and bot from server
database = self.server.database if hasattr(self.server, 'database') else None
bot = self.server.bot if hasattr(self.server, 'bot') else None
if not database:
self.send_error(500, "Database not available")
return
# Get team management service
if not hasattr(self, 'team_service'):
from src.team_management import TeamManagementService
from src.pin_authentication import PinAuthenticationService
pin_service = PinAuthenticationService(database, bot)
self.team_service = TeamManagementService(database, pin_service)
# Get team data
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
player = loop.run_until_complete(database.get_player(nickname))
if not player:
self.send_error(404, "Player not found")
return
team_data = loop.run_until_complete(
self.team_service.get_individual_team_data(player["id"], team_identifier)
)
if not team_data["success"]:
self.send_error(500, f"Failed to load team: {team_data['error']}")
return
# Get player's pets for the editor
player_pets = loop.run_until_complete(database.get_player_pets(player["id"]))
finally:
loop.close()
# Generate individual team editor HTML
content = self.generate_individual_team_editor_content(nickname, team_identifier, team_data, player_pets)
full_page = self.get_page_template(f"{team_data['team_name']} - {nickname}", content, "teambuilder")
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(full_page.encode('utf-8'))
except Exception as e:
print(f"Error serving individual team editor: {e}")
self.send_error(500, "Internal server error")
def generate_team_hub_content(self, nickname, teams):
"""Generate HTML content for team selection hub."""
return f'''
<div class="team-hub-container">
<div class="hub-header">
<h1>🏆 Team Management Hub</h1>
<p>Manage your teams and swap configurations with PIN verification</p>
</div>
<div class="active-team-section">
<h2>⚡ Current Battle Team</h2>
<div class="team-preview">
{self._generate_active_team_display(teams.get('active', {}), nickname)}
</div>
</div>
<div class="saved-teams-section">
<h2>💾 Saved Team Configurations</h2>
<div class="teams-grid">
{self._generate_team_preview(teams.get('slot_1', {}), '1')}
{self._generate_team_preview(teams.get('slot_2', {}), '2')}
{self._generate_team_preview(teams.get('slot_3', {}), '3')}
</div>
</div>
<div class="hub-footer">
<p>💡 <strong>Tip:</strong> Use "Make Active" to swap any saved team to your active battle team!</p>
<p>🔒 All team changes require PIN verification sent to your IRC private messages.</p>
</div>
</div>
<style>
.team-hub-container {{
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}}
.hub-header {{
text-align: center;
margin-bottom: 30px;
}}
.team-preview {{
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin: 10px 0;
}}
.teams-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}}
.team-card {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
}}
.btn {{
padding: 8px 16px;
border: none;
border-radius: 6px;
text-decoration: none;
font-weight: bold;
cursor: pointer;
display: inline-block;
margin: 5px;
}}
.btn-primary {{
background: var(--accent-color);
color: white;
}}
.btn-success {{
background: #28a745;
color: white;
}}
</style>
'''
def _generate_team_preview(self, team_data, team_identifier):
"""Generate preview for a single team."""
team_name = team_data.get('name', f'Team {team_identifier}')
pet_count = team_data.get('count', 0)
is_active = team_data.get('is_active', False)
if is_active:
actions = f'''
<a href="/teambuilder/{self.path.split('/')[2]}/team/active" class="btn btn-primary">
✏️ Edit Active Team
</a>
'''
else:
actions = f'''
<a href="/teambuilder/{self.path.split('/')[2]}/team/{team_identifier}" class="btn btn-primary">
✏️ Edit Team {team_identifier}
</a>
<button class="btn btn-success" onclick="alert('Team swap coming soon!')">
🔄 Make Active
</button>
'''
status = "🏆 ACTIVE TEAM" if is_active else f"💾 Saved Team"
return f'''
<div class="team-card">
<h3>{team_name}</h3>
<div class="team-status">{status}</div>
<div class="team-info">🐾 {pet_count} pets</div>
<div class="team-actions">
{actions}
</div>
</div>
'''
def _generate_active_team_display(self, active_team_data, nickname):
"""Generate detailed display for active team with individual pet cards."""
pets_dict = active_team_data.get('pets', {})
pet_count = active_team_data.get('count', 0)
# Convert dictionary format to list for consistent processing
pets = []
if isinstance(pets_dict, dict):
# Active team format: {"1": {pet_data}, "2": {pet_data}}
for position, pet_data in pets_dict.items():
if pet_data:
pet_data['team_order'] = int(position)
pets.append(pet_data)
elif isinstance(pets_dict, list):
# Saved team format: [{pet_data}, {pet_data}]
pets = pets_dict
if not pets or pet_count == 0:
return f'''
<div class="active-team-empty">
<h3>🏆 Active Team</h3>
<div class="team-status">No pets in active team</div>
<div class="team-actions">
<a href="/teambuilder/{nickname}/team/active" class="btn btn-primary">
✏️ Set Up Active Team
</a>
</div>
</div>
'''
# Generate individual pet cards for active team
pet_cards = []
for pet in pets:
# Handle both active team format and saved team format
name = pet.get('nickname') or pet.get('name') or pet.get('species_name', 'Unknown')
level = pet.get('level', 1)
hp = pet.get('hp', 0)
max_hp = pet.get('max_hp', 0)
attack = pet.get('attack', 0)
defense = pet.get('defense', 0)
speed = pet.get('speed', 0)
happiness = pet.get('happiness', 50)
species_name = pet.get('species_name', 'Unknown')
# Handle type field variations between active and saved teams
type1 = (pet.get('type1') or pet.get('type_primary') or
pet.get('type1', 'Normal'))
type2 = (pet.get('type2') or pet.get('type_secondary') or
pet.get('type2'))
team_order = pet.get('team_order', 0)
# Calculate HP percentage for health bar
hp_percent = (hp / max_hp) * 100 if max_hp > 0 else 0
hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336"
# Happiness emoji
if happiness >= 80:
happiness_emoji = "😊"
elif happiness >= 60:
happiness_emoji = "🙂"
elif happiness >= 40:
happiness_emoji = "😐"
elif happiness >= 20:
happiness_emoji = "😕"
else:
happiness_emoji = "😢"
# Type display
type_display = type1
if type2:
type_display += f"/{type2}"
pet_card = f'''
<div class="active-pet-card">
<div class="pet-header">
<h4 class="pet-name">#{team_order} {name}</h4>
<div class="pet-level">Lv.{level}</div>
</div>
<div class="pet-species">{species_name}</div>
<div class="pet-type">{type_display}</div>
<div class="hp-section">
<div class="hp-label">
<span>HP</span>
<span>{hp}/{max_hp}</span>
</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">{attack}</span>
</div>
<div class="stat">
<span class="stat-label">DEF</span>
<span class="stat-value">{defense}</span>
</div>
<div class="stat">
<span class="stat-label">SPD</span>
<span class="stat-value">{speed}</span>
</div>
</div>
<div class="pet-happiness">
<span>{happiness_emoji}</span>
<span>Happiness: {happiness}/100</span>
</div>
</div>
'''
pet_cards.append(pet_card)
pets_html = "".join(pet_cards)
return f'''
<div class="active-team-display">
<div class="active-team-header">
<h3>🏆 Active Battle Team ({pet_count} pets)</h3>
<a href="/teambuilder/{nickname}/team/active" class="btn btn-primary">
✏️ Edit Active Team
</a>
</div>
<div class="active-pets-grid">
{pets_html}
</div>
</div>
<style>
.active-team-display {{
background: var(--bg-secondary);
border: 2px solid var(--text-accent);
border-radius: 12px;
padding: 20px;
margin: 10px 0;
}}
.active-team-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}}
.active-team-empty {{
background: var(--bg-secondary);
border: 2px dashed var(--border-color);
border-radius: 12px;
padding: 30px;
text-align: center;
color: var(--text-secondary);
}}
.active-pets-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 15px;
}}
.active-pet-card {{
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
transition: transform 0.2s ease;
}}
.active-pet-card:hover {{
transform: translateY(-2px);
box-shadow: 0 4px 15px var(--shadow-color);
}}
.pet-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}}
.pet-name {{
margin: 0;
color: var(--text-accent);
font-size: 1.1em;
}}
.pet-level {{
background: var(--accent-blue);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
font-weight: bold;
}}
.pet-species {{
color: var(--text-secondary);
font-size: 0.9em;
margin-bottom: 4px;
}}
.pet-type {{
color: var(--accent-purple);
font-weight: bold;
font-size: 0.85em;
margin-bottom: 10px;
}}
.hp-section {{
margin: 10px 0;
}}
.hp-label {{
display: flex;
justify-content: space-between;
font-size: 0.85em;
margin-bottom: 4px;
}}
.hp-bar {{
width: 100%;
height: 6px;
background: var(--bg-primary);
border-radius: 3px;
overflow: hidden;
}}
.hp-fill {{
height: 100%;
transition: width 0.3s ease;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin: 10px 0;
}}
.stat {{
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-primary);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
}}
.stat-label {{
color: var(--text-secondary);
}}
.stat-value {{
color: var(--text-primary);
font-weight: bold;
}}
.pet-happiness {{
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
color: var(--text-secondary);
margin-top: 8px;
}}
</style>
'''
def generate_individual_team_editor_content(self, nickname, team_identifier, team_data, player_pets):
"""Generate HTML content for individual team editor."""
team_name = team_data.get('team_name', 'Unknown Team')
is_active_team = team_data.get('is_active_team', False)
team_pets = team_data.get('pets', [])
# Separate pets into team and storage
team_pet_ids = [p['id'] for p in team_pets]
storage_pets = [p for p in player_pets if p['id'] not in team_pet_ids]
# Helper function to create detailed pet card
def make_pet_card(pet, in_team=False):
name = pet.get('nickname') or pet.get('species_name', 'Unknown')
pet_id = pet.get('id', 0)
level = pet.get('level', 1)
species = pet.get('species_name', 'Unknown')
# Type info
type_str = pet.get('type1', 'Normal')
if pet.get('type2'):
type_str += f"/{pet['type2']}"
# HP calculation
hp = pet.get('hp', 0)
max_hp = pet.get('max_hp', 1)
hp_percent = (hp / max_hp * 100) if max_hp > 0 else 0
hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336"
# Get pet moves
moves = pet.get('moves', [])
moves_html = ''
if moves:
moves_html = '<div class="pet-moves"><strong>Moves:</strong> ' + ', '.join([m.get('name', 'Unknown') for m in moves[:4]]) + '</div>'
return f"""
<div class="pet-card {'team-pet' if in_team else 'storage-pet'}"
draggable="true"
data-pet-id="{pet_id}"
data-pet-name="{name}"
data-in-team="{str(in_team).lower()}">
<div class="pet-header">
<h3 class="pet-name">{pet.get('emoji', '🐾')} {name}</h3>
<div class="pet-level">Lv.{level}</div>
</div>
<div class="pet-species">{species}</div>
<div class="pet-type">{type_str}</div>
<div class="hp-section">
<div class="hp-label">
<span>HP</span>
<span>{hp}/{max_hp}</span>
</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.get('attack', 0)}</span>
</div>
<div class="stat">
<span class="stat-label">DEF</span>
<span class="stat-value">{pet.get('defense', 0)}</span>
</div>
<div class="stat">
<span class="stat-label">SPD</span>
<span class="stat-value">{pet.get('speed', 0)}</span>
</div>
</div>
{moves_html}
<div class="pet-happiness">
<span>{'😊' if pet.get('happiness', 50) > 70 else '😐' if pet.get('happiness', 50) > 40 else '😞'}</span>
<span>Happiness: {pet.get('happiness', 50)}/100</span>
</div>
</div>
"""
# Create team slots (6 slots)
team_slots_html = ''
for i in range(1, 7):
# Find pet in this slot
slot_pet = None
for pet in team_pets:
if pet.get('team_order') == i or (i == 1 and not any(p.get('team_order') == 1 for p in team_pets) and team_pets and pet == team_pets[0]):
slot_pet = pet
break
if slot_pet:
team_slots_html += f"""
<div class="team-slot occupied" id="slot-{i}" data-slot="{i}">
<div class="slot-number">#{i}</div>
{make_pet_card(slot_pet, True)}
</div>
"""
else:
team_slots_html += f"""
<div class="team-slot empty" id="slot-{i}" data-slot="{i}">
<div class="slot-number">#{i}</div>
<div class="empty-slot">
<span class="empty-icon"></span>
<span class="empty-text">Drop pet here</span>
</div>
</div>
"""
# Create storage pet cards
storage_cards_html = ''.join([make_pet_card(pet, False) for pet in storage_pets])
if not storage_cards_html:
storage_cards_html = '<div class="no-pets-message">No pets in storage. All your pets are in teams!</div>'
return f'''
<div class="individual-team-editor">
<div class="editor-header">
<h1>✏️ {team_name}</h1>
<p>{"⚡ Active battle team" if is_active_team else f"💾 Saved team configuration (Slot {team_identifier})"}</p>
<a href="/teambuilder/{nickname}" class="btn btn-secondary">← Back to Hub</a>
</div>
<div class="editor-content">
<div class="team-section">
<h2>🏆 Team Composition</h2>
<div class="team-slots" id="team-slots">
{team_slots_html}
</div>
<div class="team-actions">
<button class="btn btn-primary" id="save-team-btn" onclick="saveTeam()">💾 Save Team</button>
<button class="btn btn-secondary" onclick="resetTeam()">🔄 Reset Changes</button>
</div>
<div id="pin-section" style="display: none; margin-top: 20px;">
<h3>🔐 Enter Verification PIN</h3>
<p>Check your IRC private messages for the PIN</p>
<input type="text" id="pin-input" placeholder="Enter 6-digit PIN" maxlength="6">
<button class="btn btn-primary" onclick="verifyPin()">Verify</button>
</div>
</div>
<div class="pets-section">
<h2>🐾 Pet Storage ({len(storage_pets)} available)</h2>
<div class="storage-grid" id="storage-grid">
{storage_cards_html}
</div>
</div>
</div>
</div>
<style>
.individual-team-editor {{
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}}
.editor-header {{
text-align: center;
margin-bottom: 30px;
}}
.editor-content {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}}
.team-section, .pets-section {{
background: var(--bg-secondary);
padding: 25px;
border-radius: 15px;
border: 2px solid var(--border-color);
}}
.team-slots {{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}}
.team-slot {{
background: var(--bg-tertiary);
border: 2px dashed var(--border-color);
border-radius: 12px;
padding: 15px;
min-height: 300px;
position: relative;
transition: all 0.3s ease;
}}
.team-slot.occupied {{
border-style: solid;
border-color: var(--accent-blue);
}}
.team-slot.drag-over {{
background: var(--hover-color);
border-color: var(--accent-green);
transform: scale(1.02);
}}
.slot-number {{
position: absolute;
top: 5px;
left: 10px;
font-weight: bold;
color: var(--text-secondary);
font-size: 0.9em;
}}
.empty-slot {{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}}
.empty-icon {{
font-size: 2em;
margin-bottom: 10px;
}}
.storage-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
max-height: 600px;
overflow-y: auto;
padding: 10px;
}}
.pet-card {{
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 0;
cursor: grab;
transition: all 0.3s ease;
user-select: none;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
min-width: 320px;
}}
.pet-card:hover {{
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}}
.pet-card.dragging {{
opacity: 0.5;
cursor: grabbing;
}}
.pet-card.team-pet {{
border-color: var(--accent-blue);
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(66, 165, 245, 0.1) 100%);
}}
.pet-header {{
background: var(--bg-tertiary);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
}}
.pet-name {{
font-weight: bold;
color: var(--text-accent);
font-size: 1.3em;
margin: 0;
}}
.pet-level {{
background: var(--accent-purple);
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: 500;
}}
.pet-species, .pet-type {{
color: var(--text-secondary);
font-size: 0.9em;
margin-bottom: 5px;
padding: 0 20px;
}}
.hp-section {{
padding: 10px 20px;
background: var(--bg-secondary);
}}
.hp-label {{
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9em;
}}
.hp-bar {{
background: var(--bg-primary);
height: 12px;
border-radius: 6px;
overflow: hidden;
margin-top: 5px;
}}
.hp-fill {{
height: 100%;
transition: width 0.3s ease;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 15px 20px;
background: var(--bg-secondary);
}}
.stat {{
text-align: center;
background: var(--bg-tertiary);
padding: 10px 8px;
border-radius: 8px;
}}
.stat-label {{
display: block;
font-size: 0.8em;
color: var(--text-secondary);
margin-bottom: 4px;
}}
.stat-value {{
display: block;
font-weight: bold;
color: var(--text-primary);
font-size: 1.1em;
}}
.pet-moves {{
font-size: 0.9em;
color: var(--text-secondary);
padding: 10px 20px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}}
.pet-happiness {{
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
font-size: 0.9em;
}}
.team-actions {{
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}}
.no-pets-message {{
text-align: center;
color: var(--text-secondary);
padding: 40px;
}}
@media (max-width: 1200px) {{
.editor-content {{
grid-template-columns: 1fr;
}}
.storage-grid {{
grid-template-columns: repeat(2, 1fr);
}}
}}
</style>
<script>
let draggedElement = null;
let originalTeam = [];
let currentTeam = [];
let teamIdentifier = '{team_identifier}';
let isActiveTeam = {str(is_active_team).lower()};
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {{
console.log('Team Editor: Initializing for team', teamIdentifier);
initializeTeamEditor();
}});
function initializeTeamEditor() {{
// Capture initial team state
captureInitialTeam();
// Initialize drag and drop
initializeDragAndDrop();
// Initialize double-click handlers
initializeDoubleClick();
// Update save button state
updateSaveButton();
}}
function captureInitialTeam() {{
originalTeam = [];
currentTeam = [];
document.querySelectorAll('.team-slot').forEach((slot, index) => {{
const petCard = slot.querySelector('.pet-card');
if (petCard) {{
const petData = {{
slot: index + 1,
petId: parseInt(petCard.dataset.petId),
petName: petCard.dataset.petName
}};
originalTeam.push(petData);
currentTeam.push(petData);
}}
}});
console.log('Initial team captured:', originalTeam);
}}
function initializeDragAndDrop() {{
// Make all pet cards draggable
document.querySelectorAll('.pet-card').forEach(card => {{
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
}});
// Set up team slots as drop zones
document.querySelectorAll('.team-slot').forEach(slot => {{
slot.addEventListener('dragover', handleDragOver);
slot.addEventListener('drop', handleDrop);
slot.addEventListener('dragenter', handleDragEnter);
slot.addEventListener('dragleave', handleDragLeave);
}});
// Set up storage area as drop zone
const storageGrid = document.getElementById('storage-grid');
if (storageGrid) {{
storageGrid.addEventListener('dragover', handleDragOver);
storageGrid.addEventListener('drop', handleDropToStorage);
}}
}}
function initializeDoubleClick() {{
document.querySelectorAll('.pet-card').forEach(card => {{
card.addEventListener('dblclick', handleDoubleClick);
}});
}}
function handleDragStart(e) {{
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
console.log('Drag started:', this.dataset.petName);
}}
function handleDragEnd(e) {{
this.classList.remove('dragging');
draggedElement = null;
// Remove all drag-over classes
document.querySelectorAll('.drag-over').forEach(el => {{
el.classList.remove('drag-over');
}});
}}
function handleDragOver(e) {{
if (e.preventDefault) {{
e.preventDefault();
}}
e.dataTransfer.dropEffect = 'move';
return false;
}}
function handleDragEnter(e) {{
if (this.classList.contains('team-slot')) {{
this.classList.add('drag-over');
}}
}}
function handleDragLeave(e) {{
if (this.classList.contains('team-slot')) {{
this.classList.remove('drag-over');
}}
}}
function handleDrop(e) {{
if (e.stopPropagation) {{
e.stopPropagation();
}}
e.preventDefault();
this.classList.remove('drag-over');
if (!draggedElement || !this.classList.contains('team-slot')) {{
return false;
}}
const slotNumber = parseInt(this.dataset.slot);
movePetToSlot(draggedElement, slotNumber);
return false;
}}
function handleDropToStorage(e) {{
if (e.stopPropagation) {{
e.stopPropagation();
}}
e.preventDefault();
if (!draggedElement) {{
return false;
}}
movePetToStorage(draggedElement);
return false;
}}
function handleDoubleClick(e) {{
const petCard = this;
const inTeam = petCard.dataset.inTeam === 'true';
if (inTeam) {{
// Move to storage
movePetToStorage(petCard);
}} else {{
// Find first empty slot
const emptySlot = document.querySelector('.team-slot.empty');
if (emptySlot) {{
const slotNumber = parseInt(emptySlot.dataset.slot);
movePetToSlot(petCard, slotNumber);
}} else {{
alert('Team is full! Remove a pet first.');
}}
}}
}}
function movePetToSlot(petCard, slotNumber) {{
const targetSlot = document.getElementById(`slot-${{slotNumber}}`);
if (!targetSlot) return;
// Check if slot is occupied
const existingPet = targetSlot.querySelector('.pet-card');
if (existingPet) {{
// Swap pets if dragging from another team slot
const sourceSlot = petCard.parentElement;
if (sourceSlot && sourceSlot.classList.contains('team-slot')) {{
// Perform swap
sourceSlot.appendChild(existingPet);
existingPet.dataset.inTeam = 'true';
}} else {{
// Can't place in occupied slot from storage
alert('This slot is already occupied!');
return;
}}
}}
// Clear empty slot content if present
const emptySlotDiv = targetSlot.querySelector('.empty-slot');
if (emptySlotDiv) {{
emptySlotDiv.remove();
}}
// Move pet to slot
targetSlot.appendChild(petCard);
targetSlot.classList.remove('empty');
targetSlot.classList.add('occupied');
// Update pet card state
petCard.dataset.inTeam = 'true';
petCard.classList.add('team-pet');
petCard.classList.remove('storage-pet');
// Update current team state
updateCurrentTeam();
updateSaveButton();
}}
function movePetToStorage(petCard) {{
const sourceSlot = petCard.parentElement;
// Add back to storage
document.getElementById('storage-grid').appendChild(petCard);
// Update pet card state
petCard.dataset.inTeam = 'false';
petCard.classList.remove('team-pet');
petCard.classList.add('storage-pet');
// If moved from a team slot, make it empty
if (sourceSlot && sourceSlot.classList.contains('team-slot')) {{
sourceSlot.classList.remove('occupied');
sourceSlot.classList.add('empty');
sourceSlot.innerHTML = `
<div class="slot-number">#${{sourceSlot.dataset.slot}}</div>
<div class="empty-slot">
<span class="empty-icon"></span>
<span class="empty-text">Drop pet here</span>
</div>
`;
}}
// Update current team state
updateCurrentTeam();
updateSaveButton();
}}
function updateCurrentTeam() {{
currentTeam = [];
document.querySelectorAll('.team-slot').forEach((slot, index) => {{
const petCard = slot.querySelector('.pet-card');
if (petCard) {{
currentTeam.push({{
slot: index + 1,
petId: parseInt(petCard.dataset.petId),
petName: petCard.dataset.petName
}});
}}
}});
console.log('Current team updated:', currentTeam);
}}
function updateSaveButton() {{
const saveBtn = document.getElementById('save-team-btn');
const hasChanges = JSON.stringify(currentTeam) !== JSON.stringify(originalTeam);
if (hasChanges) {{
saveBtn.classList.add('btn-success');
saveBtn.textContent = '💾 Save Changes';
}} else {{
saveBtn.classList.remove('btn-success');
saveBtn.textContent = '💾 Save Team';
}}
}}
function resetTeam() {{
if (!confirm('Reset all changes to this team?')) {{
return;
}}
// Reload the page to reset
window.location.reload();
}}
async function saveTeam() {{
const teamData = {{
team_identifier: teamIdentifier,
is_active_team: isActiveTeam,
pets: currentTeam.map(p => ({{
pet_id: p.petId,
position: p.slot
}}))
}};
console.log('Saving team:', teamData);
try {{
const response = await fetch(`/teambuilder/{nickname}/team/${{teamIdentifier}}/save`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(teamData)
}});
const result = await response.json();
if (result.requires_pin) {{
document.getElementById('pin-section').style.display = 'block';
document.getElementById('pin-input').focus();
}} else if (result.success) {{
alert('Team saved successfully!');
originalTeam = [...currentTeam];
updateSaveButton();
}} else {{
alert('Error: ' + (result.error || 'Failed to save team'));
}}
}} catch (error) {{
console.error('Error saving team:', error);
alert('Failed to save team. Please try again.');
}}
}}
async function verifyPin() {{
const pin = document.getElementById('pin-input').value;
if (!pin || pin.length !== 6) {{
alert('Please enter a valid 6-digit PIN');
return;
}}
try {{
const response = await fetch(`/teambuilder/{nickname}/team/${{teamIdentifier}}/verify`, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ pin: pin }})
}});
const result = await response.json();
if (result.success) {{
alert('Team saved successfully!');
document.getElementById('pin-section').style.display = 'none';
document.getElementById('pin-input').value = '';
originalTeam = [...currentTeam];
updateSaveButton();
}} else {{
alert('Error: ' + (result.error || 'Invalid PIN'));
document.getElementById('pin-input').value = '';
}}
}} catch (error) {{
console.error('Error verifying PIN:', error);
alert('Failed to verify PIN. Please try again.');
}}
}}
</script>
'''
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=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')
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('')
try:
self.server.serve_forever()
except KeyboardInterrupt:
print('\n🛑 Server stopped')
finally:
self.server.server_close()
def start_in_thread(self):
"""Start the web server in a separate thread"""
import threading
def run_server():
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
self.server.database = self.database
self.server.bot = self.bot
try:
self.server.serve_forever()
except Exception as e:
print(f"Web server error: {e}")
finally:
self.server.server_close()
self.server_thread = threading.Thread(target=run_server, daemon=True)
self.server_thread.start()
def stop(self):
"""Stop the web server"""
if self.server:
print('🛑 Stopping web 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=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('📱 Example Player Profile:')
print(' http://petz.rdx4.com/player/megasconed')
print('')
print('⚙️ Press Ctrl+C to stop the server')
print('')
try:
server.run()
except KeyboardInterrupt:
print('\n🛑 Shutting down web server...')
server.stop()
if __name__ == '__main__':
run_standalone()