- 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>
11580 lines
444 KiB
Python
11580 lines
444 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
PetBot Web Server
|
||
Provides web interface for bot data including help, player stats, and pet collections
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import asyncio
|
||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||
from urllib.parse import urlparse, parse_qs
|
||
from threading import Thread
|
||
import time
|
||
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 <location></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 <move></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 "<name>"</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 "<name>"</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 <pet></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 <pet></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 <pet> <new_name></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 <item name></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 <weather> [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 <location></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 <location></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 <location></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 <location></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()
|