- Completely rewrote team builder with unified template system - Fixed center alignment issues with proper CSS layout (max-width: 1200px, margin: 0 auto) - Implemented working drag-and-drop between storage and numbered team slots (1-6) - Added double-click backup method for moving pets - Fixed JavaScript initialization and DOM loading issues - Added proper visual feedback during drag operations - Fixed CSS syntax errors that were breaking f-string templates - Added missing send_json_response method for AJAX requests - Integrated IRC PIN delivery system for secure team changes - Updated PetBotWebServer constructor to accept bot instance for IRC messaging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
4839 lines
178 KiB
Python
4839 lines
178 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
PetBot Web Server
|
||
Provides web interface for bot data including help, player stats, and pet collections
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import asyncio
|
||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||
from urllib.parse import urlparse, parse_qs
|
||
from threading import Thread
|
||
import time
|
||
|
||
# Add the project directory to the path
|
||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from src.database import Database
|
||
|
||
class PetBotRequestHandler(BaseHTTPRequestHandler):
|
||
"""HTTP request handler for PetBot web server"""
|
||
|
||
@property
|
||
def database(self):
|
||
"""Get database instance from server"""
|
||
return self.server.database
|
||
|
||
@property
|
||
def bot(self):
|
||
"""Get bot instance from server"""
|
||
return getattr(self.server, 'bot', None)
|
||
|
||
def send_json_response(self, data, status_code=200):
|
||
"""Send a JSON response"""
|
||
import json
|
||
self.send_response(status_code)
|
||
self.send_header('Content-type', 'application/json')
|
||
self.end_headers()
|
||
self.wfile.write(json.dumps(data).encode())
|
||
|
||
def get_unified_css(self):
|
||
"""Return unified CSS theme for all pages"""
|
||
return """
|
||
:root {
|
||
--bg-primary: #0f0f23;
|
||
--bg-secondary: #1e1e3f;
|
||
--bg-tertiary: #2a2a4a;
|
||
--text-primary: #cccccc;
|
||
--text-secondary: #aaaaaa;
|
||
--text-accent: #66ff66;
|
||
--accent-blue: #4dabf7;
|
||
--accent-purple: #845ec2;
|
||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
--gradient-secondary: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||
--border-color: #444466;
|
||
--hover-color: #3a3a5a;
|
||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||
--success-color: #51cf66;
|
||
--warning-color: #ffd43b;
|
||
--error-color: #ff6b6b;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.main-container {
|
||
padding: 20px;
|
||
min-height: calc(100vh - 80px);
|
||
}
|
||
|
||
/* Navigation Bar */
|
||
.navbar {
|
||
background: var(--gradient-primary);
|
||
padding: 15px 20px;
|
||
box-shadow: 0 2px 10px var(--shadow-color);
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.nav-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.nav-brand {
|
||
font-size: 1.5em;
|
||
font-weight: bold;
|
||
color: white;
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-link {
|
||
color: white;
|
||
text-decoration: none;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
transition: all 0.3s ease;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.nav-link:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.nav-link.active {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* Dropdown Navigation */
|
||
.nav-dropdown {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.dropdown-arrow {
|
||
font-size: 0.8em;
|
||
margin-left: 5px;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.nav-dropdown:hover .dropdown-arrow {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.dropdown-content {
|
||
display: none;
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
background: var(--bg-secondary);
|
||
min-width: 180px;
|
||
box-shadow: 0 8px 16px var(--shadow-color);
|
||
border-radius: 8px;
|
||
z-index: 1000;
|
||
border: 1px solid var(--border-color);
|
||
overflow: hidden;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.nav-dropdown:hover .dropdown-content {
|
||
display: block;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(-10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.dropdown-item {
|
||
color: var(--text-primary);
|
||
padding: 12px 16px;
|
||
text-decoration: none;
|
||
display: block;
|
||
transition: background-color 0.3s ease;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.dropdown-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-accent);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.nav-content {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.nav-links {
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.dropdown-content {
|
||
position: static;
|
||
display: none;
|
||
width: 100%;
|
||
box-shadow: none;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: var(--bg-tertiary);
|
||
margin-top: 0;
|
||
}
|
||
|
||
.nav-dropdown:hover .dropdown-content {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
/* Header styling */
|
||
.header {
|
||
text-align: center;
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
padding: 40px 20px;
|
||
border-radius: 15px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 5px 20px var(--shadow-color);
|
||
}
|
||
|
||
.header h1 {
|
||
margin: 0 0 10px 0;
|
||
font-size: 2.5em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.header p {
|
||
margin: 0;
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* Card styling */
|
||
.card {
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 4px 15px var(--shadow-color);
|
||
border: 1px solid var(--border-color);
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 25px var(--shadow-color);
|
||
}
|
||
|
||
.card h2 {
|
||
margin-top: 0;
|
||
color: var(--text-accent);
|
||
border-bottom: 2px solid var(--text-accent);
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.card h3 {
|
||
color: var(--accent-blue);
|
||
margin-top: 25px;
|
||
}
|
||
|
||
/* Grid layouts */
|
||
.grid {
|
||
display: grid;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); }
|
||
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
||
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
display: inline-block;
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 25px;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin: 5px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px var(--shadow-color);
|
||
}
|
||
|
||
/* Badges and tags */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 15px;
|
||
font-size: 0.85em;
|
||
font-weight: 500;
|
||
margin: 2px;
|
||
}
|
||
|
||
.badge-primary { background: var(--accent-blue); color: white; }
|
||
.badge-secondary { background: var(--bg-tertiary); color: var(--text-primary); }
|
||
.badge-success { background: var(--success-color); color: white; }
|
||
.badge-warning { background: var(--warning-color); color: #333; }
|
||
.badge-error { background: var(--error-color); color: white; }
|
||
|
||
/* Tables */
|
||
.table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
background: var(--bg-secondary);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table th, .table td {
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.table th {
|
||
background: var(--bg-tertiary);
|
||
font-weight: bold;
|
||
color: var(--text-accent);
|
||
}
|
||
|
||
.table tr:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
/* Loading and status messages */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.error-message {
|
||
background: var(--error-color);
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.success-message {
|
||
background: var(--success-color);
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
/* Pet-specific styles for petdex */
|
||
.pets-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pet-card {
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 15px var(--shadow-color);
|
||
border: 1px solid var(--border-color);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.pet-card:hover {
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
.pet-header {
|
||
background: var(--bg-tertiary);
|
||
padding: 15px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.pet-header h3 {
|
||
margin: 0;
|
||
font-size: 1.3em;
|
||
}
|
||
|
||
.type-badge {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
padding: 4px 12px;
|
||
border-radius: 15px;
|
||
font-size: 0.85em;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pet-stats {
|
||
padding: 15px 20px;
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.total-stats {
|
||
text-align: center;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--border-color);
|
||
font-weight: 600;
|
||
color: var(--text-accent);
|
||
}
|
||
|
||
.pet-info {
|
||
padding: 15px 20px;
|
||
background: var(--bg-tertiary);
|
||
font-size: 0.9em;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.rarity-section {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
/* Responsive design */
|
||
@media (max-width: 768px) {
|
||
.main-container {
|
||
padding: 10px;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2em;
|
||
}
|
||
|
||
.card {
|
||
padding: 15px;
|
||
}
|
||
|
||
.grid-2, .grid-3, .grid-4 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
"""
|
||
|
||
def get_navigation_bar(self, current_page=""):
|
||
"""Return unified navigation bar HTML with dropdown menus"""
|
||
|
||
# Define navigation structure with dropdowns
|
||
nav_structure = [
|
||
("", "🏠 Home", []),
|
||
("players", "👥 Players", [
|
||
("leaderboard", "🏆 Leaderboard"),
|
||
("players", "📊 Statistics")
|
||
]),
|
||
("locations", "🗺️ Locations", [
|
||
("locations", "🌤️ Weather"),
|
||
("locations", "🎯 Spawns"),
|
||
("locations", "🏛️ Gyms")
|
||
]),
|
||
("petdex", "📚 Petdex", [
|
||
("petdex", "🔷 by Type"),
|
||
("petdex", "⭐ by Rarity"),
|
||
("petdex", "🔍 Search")
|
||
]),
|
||
("help", "📖 Help", [
|
||
("help", "⚡ Commands"),
|
||
("help", "📖 Web Guide"),
|
||
("help", "❓ FAQ")
|
||
])
|
||
]
|
||
|
||
nav_links = ""
|
||
for page_path, page_name, subpages in nav_structure:
|
||
active_class = " active" if current_page == page_path else ""
|
||
href = f"/{page_path}" if page_path else "/"
|
||
|
||
if subpages:
|
||
# Create dropdown menu
|
||
dropdown_items = ""
|
||
for sub_path, sub_name in subpages:
|
||
sub_href = f"/{sub_path}" if sub_path else "/"
|
||
dropdown_items += f'<a href="{sub_href}" class="dropdown-item">{sub_name}</a>'
|
||
|
||
nav_links += f'''
|
||
<div class="nav-dropdown">
|
||
<a href="{href}" class="nav-link{active_class}">{page_name} <span class="dropdown-arrow">▼</span></a>
|
||
<div class="dropdown-content">
|
||
{dropdown_items}
|
||
</div>
|
||
</div>'''
|
||
else:
|
||
# Regular nav link
|
||
nav_links += f'<a href="{href}" class="nav-link{active_class}">{page_name}</a>'
|
||
|
||
return f"""
|
||
<nav class="navbar">
|
||
<div class="nav-content">
|
||
<a href="/" class="nav-brand">🐾 PetBot</a>
|
||
<div class="nav-links">
|
||
{nav_links}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
"""
|
||
|
||
def get_page_template(self, title, content, current_page=""):
|
||
"""Return complete page HTML with unified theme"""
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{title} - PetBot</title>
|
||
<style>{self.get_unified_css()}</style>
|
||
</head>
|
||
<body>
|
||
{self.get_navigation_bar(current_page)}
|
||
<div class="main-container">
|
||
{content}
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
def do_GET(self):
|
||
"""Handle GET requests"""
|
||
parsed_path = urlparse(self.path)
|
||
path = parsed_path.path
|
||
|
||
# Route handling
|
||
if path == '/':
|
||
self.serve_index()
|
||
elif path == '/help':
|
||
self.serve_help()
|
||
elif path == '/players':
|
||
self.serve_players()
|
||
elif path.startswith('/player/'):
|
||
nickname = path[8:] # Remove '/player/' prefix
|
||
self.serve_player_profile(nickname)
|
||
elif path == '/leaderboard':
|
||
self.serve_leaderboard()
|
||
elif path == '/locations':
|
||
self.serve_locations()
|
||
elif path == '/petdex':
|
||
self.serve_petdex()
|
||
elif path.startswith('/teambuilder/'):
|
||
nickname = path[13:] # Remove '/teambuilder/' prefix
|
||
self.serve_teambuilder(nickname)
|
||
else:
|
||
self.send_error(404, "Page not found")
|
||
|
||
def do_POST(self):
|
||
"""Handle POST requests"""
|
||
parsed_path = urlparse(self.path)
|
||
path = parsed_path.path
|
||
|
||
if path.startswith('/teambuilder/') and path.endswith('/save'):
|
||
nickname = path[13:-5] # Remove '/teambuilder/' prefix and '/save' suffix
|
||
self.handle_team_save(nickname)
|
||
elif path.startswith('/teambuilder/') and path.endswith('/verify'):
|
||
nickname = path[13:-7] # Remove '/teambuilder/' prefix and '/verify' suffix
|
||
self.handle_team_verify(nickname)
|
||
else:
|
||
self.send_error(404, "Page not found")
|
||
|
||
def serve_index(self):
|
||
"""Serve the main index page"""
|
||
content = """
|
||
<div class="header">
|
||
<h1>🐾 PetBot Game Hub</h1>
|
||
<p>Welcome to the PetBot web interface!</p>
|
||
<p><em>Connect to irc.libera.chat #petz to play</em></p>
|
||
</div>
|
||
|
||
<div class="grid grid-3">
|
||
<div class="card">
|
||
<h3>📚 Command Help</h3>
|
||
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
|
||
<a href="/help" class="btn btn-primary">View Help</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>👥 Player List</h3>
|
||
<p>View all registered players and their basic stats</p>
|
||
<a href="/players" class="btn btn-primary">Browse Players</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🏆 Leaderboard</h3>
|
||
<p>Top players by level, pets caught, and achievements earned</p>
|
||
<a href="/leaderboard" class="btn btn-primary">View Rankings</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🗺️ Locations</h3>
|
||
<p>Explore all game locations and see what pets can be found where</p>
|
||
<a href="/locations" class="btn btn-primary">Explore World</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>📖 Petdex</h3>
|
||
<p>Complete encyclopedia of all available pets with stats, types, and evolution info</p>
|
||
<a href="/petdex" class="btn btn-primary">Browse Petdex</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🤖 Bot Status</h3>
|
||
<p><span class="badge badge-success">Online</span> and ready for commands!</p>
|
||
<p>Use <code>!help</code> in #petz for quick command reference</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
html = self.get_page_template("PetBot Game Hub", content, "")
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html.encode())
|
||
|
||
def serve_help(self):
|
||
"""Serve the help page using unified template"""
|
||
content = """
|
||
<div class="header">
|
||
<h1>📚 PetBot Commands</h1>
|
||
<p>Complete guide to Pokemon-style pet collecting in IRC</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">🚀 Getting Started</div>
|
||
<div class="section-content">
|
||
<div class="command-grid">
|
||
<div class="command">
|
||
<div class="command-name">!start</div>
|
||
<div class="command-desc">Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.</div>
|
||
<div class="command-example">Example: !start</div>
|
||
</div>
|
||
<div class="command">
|
||
<div class="command-name">!help</div>
|
||
<div class="command-desc">Get a link to this comprehensive command reference page.</div>
|
||
<div class="command-example">Example: !help</div>
|
||
</div>
|
||
<div class="command">
|
||
<div class="command-name">!stats</div>
|
||
<div class="command-desc">View your basic trainer information including level, experience, and money.</div>
|
||
<div class="command-example">Example: !stats</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">🌍 Exploration & Travel</div>
|
||
<div class="section-content">
|
||
<div class="command-grid">
|
||
<div class="command">
|
||
<div class="command-name">!explore</div>
|
||
<div class="command-desc">Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!</div>
|
||
<div class="command-example">Example: !explore</div>
|
||
</div>
|
||
<div class="command">
|
||
<div class="command-name">!travel <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">!weather</div>
|
||
<div class="command-desc">Check the current weather effects in your location. Weather affects which pet types spawn more frequently.</div>
|
||
<div class="command-example">Example: !weather</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">View your active team of pets with their levels, HP, and status.</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>
|
||
</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. 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">🌐 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</li>
|
||
<li><strong>Leaderboard</strong> - Top players by level and achievements</li>
|
||
<li><strong>Locations Guide</strong> - All areas with spawn information</li>
|
||
<li><strong>Gym Badges</strong> - Display your earned badges and progress</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p><strong>🎮 PetBot v0.2.0</strong> - Pokemon-style pet collecting for IRC</p>
|
||
<p>Catch pets • Battle gyms • Collect items • Earn badges • Explore locations</p>
|
||
<p style="margin-top: 15px; opacity: 0.7;">
|
||
Need help? Ask in the channel or visit the web dashboard for detailed information!
|
||
</p>
|
||
</div>
|
||
"""
|
||
|
||
# Add command-specific CSS to the unified styles
|
||
additional_css = """
|
||
.section {
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border-color);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.section-header {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
padding: 20px 25px;
|
||
font-size: 1.3em;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.section-content {
|
||
padding: 25px;
|
||
}
|
||
|
||
.command-grid {
|
||
display: grid;
|
||
gap: 20px;
|
||
}
|
||
|
||
.command {
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.command:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15);
|
||
border-color: var(--text-accent);
|
||
}
|
||
|
||
.command-name {
|
||
background: var(--bg-primary);
|
||
padding: 15px 20px;
|
||
font-family: 'Fira Code', 'Courier New', monospace;
|
||
font-weight: bold;
|
||
color: var(--text-accent);
|
||
border-bottom: 1px solid var(--border-color);
|
||
font-size: 1.2em;
|
||
text-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
|
||
}
|
||
|
||
.command-desc {
|
||
padding: 20px;
|
||
line-height: 1.7;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.command-example {
|
||
background: var(--bg-primary);
|
||
padding: 12px 20px;
|
||
font-family: 'Fira Code', 'Courier New', monospace;
|
||
color: var(--text-secondary);
|
||
border-top: 1px solid var(--border-color);
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
.info-box {
|
||
background: var(--bg-tertiary);
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
margin: 20px 0;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.info-box h4 {
|
||
margin: 0 0 15px 0;
|
||
color: var(--text-accent);
|
||
font-size: 1.1em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-box ul {
|
||
margin: 0;
|
||
padding-left: 25px;
|
||
}
|
||
|
||
.info-box li {
|
||
margin: 8px 0;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.info-box strong {
|
||
color: var(--text-accent);
|
||
}
|
||
|
||
.footer {
|
||
text-align: center;
|
||
margin-top: 50px;
|
||
padding: 30px;
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
color: var(--text-secondary);
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.tip {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
margin: 20px 0;
|
||
font-weight: 500;
|
||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.gym-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 15px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.gym-card {
|
||
background: var(--bg-primary);
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.gym-card strong {
|
||
color: var(--text-accent);
|
||
}
|
||
"""
|
||
|
||
# Get the unified template with additional CSS
|
||
html_content = self.get_page_template("Command Help", content, "help")
|
||
# Insert additional CSS before closing </style> tag
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_players(self):
|
||
"""Serve the players page with real data"""
|
||
# Get database instance from the server class
|
||
database = self.server.database if hasattr(self.server, 'database') else None
|
||
|
||
if not database:
|
||
self.serve_error_page("Players", "Database not available")
|
||
return
|
||
|
||
# Fetch players data
|
||
try:
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
players_data = loop.run_until_complete(self.fetch_players_data(database))
|
||
loop.close()
|
||
|
||
self.serve_players_data(players_data)
|
||
|
||
except Exception as e:
|
||
print(f"Error fetching players data: {e}")
|
||
self.serve_error_page("Players", f"Error loading players: {str(e)}")
|
||
|
||
async def fetch_players_data(self, database):
|
||
"""Fetch all players data from database"""
|
||
try:
|
||
import aiosqlite
|
||
async with aiosqlite.connect(database.db_path) as db:
|
||
# Get all players with basic stats
|
||
cursor = await db.execute("""
|
||
SELECT p.nickname, p.level, p.experience, p.money, p.created_at,
|
||
l.name as location_name,
|
||
(SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count,
|
||
(SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets,
|
||
(SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count
|
||
FROM players p
|
||
LEFT JOIN locations l ON p.current_location_id = l.id
|
||
ORDER BY p.level DESC, p.experience DESC
|
||
""")
|
||
|
||
rows = await cursor.fetchall()
|
||
# Convert SQLite rows to dictionaries properly
|
||
players = []
|
||
for row in rows:
|
||
player_dict = {
|
||
'nickname': row[0],
|
||
'level': row[1],
|
||
'experience': row[2],
|
||
'money': row[3],
|
||
'created_at': row[4],
|
||
'location_name': row[5],
|
||
'pet_count': row[6],
|
||
'active_pets': row[7],
|
||
'achievement_count': row[8]
|
||
}
|
||
players.append(player_dict)
|
||
return players
|
||
|
||
except Exception as e:
|
||
print(f"Database error fetching players: {e}")
|
||
return []
|
||
|
||
def serve_players_data(self, players_data):
|
||
"""Serve players page with real data"""
|
||
|
||
# Calculate statistics
|
||
total_players = len(players_data)
|
||
total_pets = sum(p['pet_count'] for p in players_data) if players_data else 0
|
||
total_achievements = sum(p['achievement_count'] for p in players_data) if players_data else 0
|
||
highest_level = max((p['level'] for p in players_data), default=0) if players_data else 0
|
||
|
||
# Build statistics cards
|
||
stats_content = f"""
|
||
<div class="grid grid-4">
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">📊 Total Players</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_players}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">🐾 Total Pets</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_pets}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">🏆 Achievements</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_achievements}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">⭐ Highest Level</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{highest_level}</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
# Build players table HTML
|
||
if players_data:
|
||
players_html = ""
|
||
for i, player in enumerate(players_data, 1):
|
||
rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.")
|
||
|
||
players_html += f"""
|
||
<tr onclick="window.location.href='/player/{player['nickname']}'" style="cursor: pointer;">
|
||
<td><strong>{rank_emoji}</strong></td>
|
||
<td><a href="/player/{player['nickname']}" style="color: var(--text-accent); text-decoration: none;"><strong>{player['nickname']}</strong></a></td>
|
||
<td>{player['level']}</td>
|
||
<td>{player['experience']}</td>
|
||
<td>${player['money']}</td>
|
||
<td>{player['pet_count']}</td>
|
||
<td>{player['active_pets']}</td>
|
||
<td>{player['achievement_count']}</td>
|
||
<td>{player.get('location_name', 'Unknown')}</td>
|
||
</tr>"""
|
||
else:
|
||
players_html = """
|
||
<tr>
|
||
<td colspan="9" style="text-align: center; padding: 40px;">
|
||
No players found. Be the first to use !start in #petz!
|
||
</td>
|
||
</tr>"""
|
||
|
||
# Build table content
|
||
table_content = f"""
|
||
<div class="card">
|
||
<h2>🏆 Player Rankings</h2>
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Rank</th>
|
||
<th>Player</th>
|
||
<th>Level</th>
|
||
<th>Experience</th>
|
||
<th>Money</th>
|
||
<th>Pets</th>
|
||
<th>Active</th>
|
||
<th>Achievements</th>
|
||
<th>Location</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{players_html}
|
||
</tbody>
|
||
</table>
|
||
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary);">
|
||
<p>💡 Click on any player name to view their detailed profile</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
# Combine all content
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>👥 Registered Players</h1>
|
||
<p>All trainers on their pet collection journey</p>
|
||
</div>
|
||
|
||
{stats_content}
|
||
{table_content}
|
||
"""
|
||
|
||
html = self.get_page_template("Players", content, "players")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html.encode())
|
||
|
||
def serve_error_page(self, page_name, error_msg):
|
||
"""Serve a generic error page using unified template"""
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>⚠️ Error Loading {page_name}</h1>
|
||
</div>
|
||
|
||
<div class="error-message">
|
||
<h2>Unable to load page</h2>
|
||
<p>{error_msg}</p>
|
||
<p>Please try again later or contact an administrator.</p>
|
||
</div>
|
||
"""
|
||
|
||
# Add error-specific CSS
|
||
additional_css = """
|
||
.main-container {
|
||
text-align: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.error-message {
|
||
background: var(--bg-secondary);
|
||
padding: 40px;
|
||
border-radius: 15px;
|
||
border: 2px solid var(--error-color);
|
||
margin-top: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.error-message h2 {
|
||
color: var(--error-color);
|
||
margin-top: 0;
|
||
}
|
||
"""
|
||
|
||
html_content = self.get_page_template("Error", content, "")
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(500)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_leaderboard(self):
|
||
"""Serve the leaderboard page - redirect to players for now"""
|
||
# For now, leaderboard is the same as players page since they're ranked
|
||
# In the future, this could have different categories
|
||
self.send_response(302) # Temporary redirect
|
||
self.send_header('Location', '/players')
|
||
self.end_headers()
|
||
|
||
def serve_locations(self):
|
||
"""Serve the locations page with real data"""
|
||
# Get database instance from the server class
|
||
database = self.server.database if hasattr(self.server, 'database') else None
|
||
|
||
if not database:
|
||
self.serve_error_page("Locations", "Database not available")
|
||
return
|
||
|
||
# Fetch locations data
|
||
try:
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
|
||
loop.close()
|
||
|
||
self.serve_locations_data(locations_data)
|
||
|
||
except Exception as e:
|
||
print(f"Error fetching locations data: {e}")
|
||
self.serve_error_page("Locations", f"Error loading locations: {str(e)}")
|
||
|
||
async def fetch_locations_data(self, database):
|
||
"""Fetch all locations and their spawn data from database"""
|
||
try:
|
||
import aiosqlite
|
||
async with aiosqlite.connect(database.db_path) as db:
|
||
# Get all locations
|
||
cursor = await db.execute("""
|
||
SELECT l.*,
|
||
GROUP_CONCAT(DISTINCT ps.name || ' (' || ps.type1 ||
|
||
CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns
|
||
FROM locations l
|
||
LEFT JOIN location_spawns ls ON l.id = ls.location_id
|
||
LEFT JOIN pet_species ps ON ls.species_id = ps.id
|
||
GROUP BY l.id
|
||
ORDER BY l.id
|
||
""")
|
||
|
||
rows = await cursor.fetchall()
|
||
# Convert SQLite rows to dictionaries properly
|
||
locations = []
|
||
for row in rows:
|
||
location_dict = {
|
||
'id': row[0],
|
||
'name': row[1],
|
||
'description': row[2],
|
||
'level_min': row[3],
|
||
'level_max': row[4],
|
||
'spawns': row[5] if len(row) > 5 else None
|
||
}
|
||
locations.append(location_dict)
|
||
return locations
|
||
|
||
except Exception as e:
|
||
print(f"Database error fetching locations: {e}")
|
||
return []
|
||
|
||
def serve_locations_data(self, locations_data):
|
||
"""Serve locations page with real data using unified template"""
|
||
|
||
# Build locations HTML
|
||
locations_html = ""
|
||
if locations_data:
|
||
for location in locations_data:
|
||
spawns = location.get('spawns', 'No pets found')
|
||
if not spawns or spawns == 'None':
|
||
spawns = "No pets spawn here yet"
|
||
|
||
# Split spawns into a readable list and remove duplicates
|
||
if spawns != "No pets spawn here yet":
|
||
spawn_list = list(set([spawn.strip() for spawn in spawns.split(',') if spawn.strip()]))
|
||
spawn_list.sort() # Sort alphabetically for consistency
|
||
else:
|
||
spawn_list = []
|
||
|
||
spawn_badges = ""
|
||
visible_spawns = spawn_list[:6] # Show first 6
|
||
hidden_spawns = spawn_list[6:] # Hide the rest
|
||
|
||
# Add visible spawn badges
|
||
for spawn in visible_spawns:
|
||
spawn_badges += f'<span class="spawn-badge">{spawn}</span>'
|
||
|
||
# Add hidden spawn badges (initially hidden)
|
||
if hidden_spawns:
|
||
location_id = location['id']
|
||
for spawn in hidden_spawns:
|
||
spawn_badges += f'<span class="spawn-badge hidden-spawn" id="hidden-{location_id}">{spawn}</span>'
|
||
# Add functional "show more" button
|
||
spawn_badges += f'<span class="spawn-badge more-button" onclick="toggleSpawns({location_id})">+{len(hidden_spawns)} more</span>'
|
||
|
||
if not spawn_badges:
|
||
spawn_badges = '<em style="color: var(--text-secondary);">No pets spawn here yet</em>'
|
||
|
||
locations_html += f"""
|
||
<div class="location-card">
|
||
<div class="location-header">
|
||
<h3>🗺️ {location['name']}</h3>
|
||
<div class="location-id">ID: {location['id']}</div>
|
||
</div>
|
||
<div class="location-description">
|
||
{location['description']}
|
||
</div>
|
||
<div class="location-levels">
|
||
<strong>Level Range:</strong> {location['level_min']}-{location['level_max']}
|
||
</div>
|
||
<div class="location-spawns">
|
||
<strong>Wild Pets:</strong><br>
|
||
{spawn_badges}
|
||
</div>
|
||
</div>"""
|
||
else:
|
||
locations_html = """
|
||
<div class="location-card">
|
||
<div class="location-header">
|
||
<h3>No Locations Found</h3>
|
||
</div>
|
||
<div class="location-description">
|
||
No game locations are configured yet.
|
||
</div>
|
||
</div>"""
|
||
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>🗺️ Game Locations</h1>
|
||
<p>Explore all areas and discover what pets await you!</p>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<h2>🎯 How Locations Work</h2>
|
||
<p><strong>Travel:</strong> Use <code>!travel <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 = """
|
||
.locations-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||
gap: 25px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.location-card {
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border-color);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.location-card:hover {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.location-header {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
padding: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.location-header h3 {
|
||
margin: 0;
|
||
font-size: 1.3em;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.location-id {
|
||
background: rgba(255,255,255,0.2);
|
||
padding: 5px 10px;
|
||
border-radius: 20px;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.location-description {
|
||
padding: 20px;
|
||
color: var(--text-primary);
|
||
font-style: italic;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.location-levels {
|
||
padding: 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.location-spawns {
|
||
padding: 20px;
|
||
}
|
||
|
||
.spawn-badge {
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-accent);
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 0.85em;
|
||
margin: 3px;
|
||
display: inline-block;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.hidden-spawn {
|
||
display: none;
|
||
}
|
||
|
||
.more-button {
|
||
background: var(--gradient-primary) !important;
|
||
color: white !important;
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.more-button:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.less-button {
|
||
background: #ff6b6b !important;
|
||
color: white !important;
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.less-button:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.info-section {
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.info-section h2 {
|
||
color: var(--text-accent);
|
||
margin-top: 0;
|
||
}
|
||
"""
|
||
|
||
# Get the unified template with additional CSS
|
||
html_content = self.get_page_template("Game Locations", content, "locations")
|
||
# Insert additional CSS before closing </style> tag
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_petdex(self):
|
||
"""Serve the petdex page with all pet species data"""
|
||
# Get database instance from the server class
|
||
database = self.server.database if hasattr(self.server, 'database') else None
|
||
|
||
if not database:
|
||
self.serve_error_page("Petdex", "Database not available")
|
||
return
|
||
|
||
# Fetch petdex data
|
||
try:
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
petdex_data = loop.run_until_complete(self.fetch_petdex_data(database))
|
||
loop.close()
|
||
|
||
self.serve_petdex_data(petdex_data)
|
||
|
||
except Exception as e:
|
||
print(f"Error fetching petdex data: {e}")
|
||
self.serve_error_page("Petdex", f"Error loading petdex: {str(e)}")
|
||
|
||
async def fetch_petdex_data(self, database):
|
||
"""Fetch all pet species data from database"""
|
||
try:
|
||
import aiosqlite
|
||
async with aiosqlite.connect(database.db_path) as db:
|
||
# Get all pet species with evolution information (no duplicates)
|
||
cursor = await db.execute("""
|
||
SELECT DISTINCT ps.*,
|
||
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],
|
||
'evolves_to_name': row[11], 'location_count': row[12]
|
||
}
|
||
pets.append(pet_dict)
|
||
|
||
# Get spawn locations for each pet
|
||
for pet in pets:
|
||
cursor = await db.execute("""
|
||
SELECT l.name, ls.min_level, ls.max_level, ls.spawn_rate
|
||
FROM location_spawns ls
|
||
JOIN locations l ON ls.location_id = l.id
|
||
WHERE ls.species_id = ?
|
||
ORDER BY l.name ASC
|
||
""", (pet['id'],))
|
||
spawn_rows = await cursor.fetchall()
|
||
pet['spawn_locations'] = []
|
||
for spawn_row in spawn_rows:
|
||
spawn_dict = {
|
||
'location_name': spawn_row[0], 'min_level': spawn_row[1],
|
||
'max_level': spawn_row[2], 'spawn_rate': spawn_row[3]
|
||
}
|
||
pet['spawn_locations'].append(spawn_dict)
|
||
|
||
return pets
|
||
|
||
except Exception as e:
|
||
print(f"Database error fetching petdex: {e}")
|
||
return []
|
||
|
||
def serve_petdex_data(self, petdex_data):
|
||
"""Serve petdex page with all pet species data"""
|
||
|
||
# Build pet cards HTML grouped by rarity
|
||
rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"}
|
||
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
|
||
|
||
# Calculate statistics
|
||
total_species = len(petdex_data)
|
||
type_counts = {}
|
||
for pet in petdex_data:
|
||
if pet['type1'] not in type_counts:
|
||
type_counts[pet['type1']] = 0
|
||
type_counts[pet['type1']] += 1
|
||
if pet['type2'] and pet['type2'] not in type_counts:
|
||
type_counts[pet['type2']] = 0
|
||
if pet['type2']:
|
||
type_counts[pet['type2']] += 1
|
||
|
||
# Build statistics section
|
||
stats_content = f"""
|
||
<div class="grid grid-4">
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">📊 Total Species</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{total_species}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">🎨 Types</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len(type_counts)}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">⭐ Rarities</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len(set(pet['rarity'] for pet in petdex_data))}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="color: var(--accent-blue); margin-top: 0;">🧬 Evolutions</h3>
|
||
<div style="font-size: 2.5em; font-weight: bold; color: var(--text-accent);">{len([p for p in petdex_data if p['evolution_level']])}</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
pets_by_rarity = {}
|
||
for pet in petdex_data:
|
||
rarity = pet['rarity']
|
||
if rarity not in pets_by_rarity:
|
||
pets_by_rarity[rarity] = []
|
||
pets_by_rarity[rarity].append(pet)
|
||
|
||
petdex_html = ""
|
||
total_species = len(petdex_data)
|
||
|
||
for rarity in sorted(pets_by_rarity.keys()):
|
||
pets_in_rarity = pets_by_rarity[rarity]
|
||
rarity_name = rarity_names.get(rarity, f"Rarity {rarity}")
|
||
rarity_color = rarity_colors.get(rarity, "#ffffff")
|
||
|
||
petdex_html += f"""
|
||
<div class="rarity-section">
|
||
<h2 style="color: {rarity_color}; border-bottom: 2px solid {rarity_color}; padding-bottom: 10px;">
|
||
{rarity_name} ({len(pets_in_rarity)} species)
|
||
</h2>
|
||
<div class="pets-grid">"""
|
||
|
||
for pet in pets_in_rarity:
|
||
# Build type display
|
||
type_str = pet['type1']
|
||
if pet['type2']:
|
||
type_str += f"/{pet['type2']}"
|
||
|
||
# Build evolution info
|
||
evolution_info = ""
|
||
if pet['evolution_level'] and pet['evolves_to_name']:
|
||
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']} → {pet['evolves_to_name']}"
|
||
elif pet['evolution_level']:
|
||
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']}"
|
||
|
||
# Build spawn locations
|
||
spawn_info = ""
|
||
if pet['spawn_locations']:
|
||
locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})"
|
||
for loc in pet['spawn_locations'][:3]]
|
||
if len(pet['spawn_locations']) > 3:
|
||
locations.append(f"+{len(pet['spawn_locations']) - 3} more")
|
||
spawn_info = f"<br><strong>Found in:</strong> {', '.join(locations)}"
|
||
else:
|
||
spawn_info = "<br><strong>Found in:</strong> Not yet available"
|
||
|
||
# Calculate total base stats
|
||
total_stats = pet['base_hp'] + pet['base_attack'] + pet['base_defense'] + pet['base_speed']
|
||
|
||
petdex_html += f"""
|
||
<div class="pet-card" style="border-left: 4px solid {rarity_color};">
|
||
<div class="pet-header">
|
||
<h3 style="color: {rarity_color};">{pet['name']}</h3>
|
||
<span class="type-badge">{type_str}</span>
|
||
</div>
|
||
<div class="pet-stats">
|
||
<div class="stat-row">
|
||
<span>HP: {pet['base_hp']}</span>
|
||
<span>ATK: {pet['base_attack']}</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span>DEF: {pet['base_defense']}</span>
|
||
<span>SPD: {pet['base_speed']}</span>
|
||
</div>
|
||
<div class="total-stats">Total: {total_stats}</div>
|
||
</div>
|
||
<div class="pet-info">
|
||
<strong>Rarity:</strong> {rarity_name}{evolution_info}{spawn_info}
|
||
</div>
|
||
</div>"""
|
||
|
||
petdex_html += """
|
||
</div>
|
||
</div>"""
|
||
|
||
if not petdex_data:
|
||
petdex_html = """
|
||
<div style="text-align: center; padding: 60px; color: var(--text-secondary);">
|
||
<h2>No pet species found!</h2>
|
||
<p>The petdex appears to be empty. Contact an administrator.</p>
|
||
</div>"""
|
||
|
||
# Combine all content
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>📖 Petdex</h1>
|
||
<p>Complete encyclopedia of all available pets</p>
|
||
</div>
|
||
|
||
{stats_content}
|
||
|
||
<div class="card">
|
||
<h2>📊 Pet Collection by Rarity</h2>
|
||
<p style="color: var(--text-secondary); margin-bottom: 20px;">🎯 Pets are organized by rarity. Use <code>!wild <location></code> in #petz to see what spawns where!</p>
|
||
|
||
{petdex_html}
|
||
</div>
|
||
"""
|
||
|
||
html = self.get_page_template("Petdex", content, "petdex")
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html.encode())
|
||
|
||
def serve_player_profile(self, nickname):
|
||
"""Serve individual player profile page"""
|
||
# URL decode the nickname in case it has special characters
|
||
from urllib.parse import unquote
|
||
nickname = unquote(nickname)
|
||
|
||
# Get database instance from the server class
|
||
database = self.server.database if hasattr(self.server, 'database') else None
|
||
|
||
if not database:
|
||
self.serve_player_error(nickname, "Database not available")
|
||
return
|
||
|
||
# Fetch player data
|
||
try:
|
||
import asyncio
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
|
||
loop.close()
|
||
|
||
if player_data is None:
|
||
self.serve_player_not_found(nickname)
|
||
return
|
||
|
||
self.serve_player_data(nickname, player_data)
|
||
|
||
except Exception as e:
|
||
print(f"Error fetching player data for {nickname}: {e}")
|
||
self.serve_player_error(nickname, f"Error loading player data: {str(e)}")
|
||
return
|
||
|
||
async def fetch_player_data(self, database, nickname):
|
||
"""Fetch all player data from database"""
|
||
try:
|
||
# Get player info
|
||
import aiosqlite
|
||
async with aiosqlite.connect(database.db_path) as db:
|
||
# Get player basic info
|
||
cursor = await db.execute("""
|
||
SELECT p.*, l.name as location_name, l.description as location_desc
|
||
FROM players p
|
||
LEFT JOIN locations l ON p.current_location_id = l.id
|
||
WHERE p.nickname = ?
|
||
""", (nickname,))
|
||
player = await cursor.fetchone()
|
||
if not player:
|
||
return None
|
||
|
||
# Convert to dict manually
|
||
player_dict = {
|
||
'id': player[0],
|
||
'nickname': player[1],
|
||
'created_at': player[2],
|
||
'last_active': player[3],
|
||
'level': player[4],
|
||
'experience': player[5],
|
||
'money': player[6],
|
||
'current_location_id': player[7],
|
||
'location_name': player[8],
|
||
'location_desc': player[9]
|
||
}
|
||
|
||
# Get player pets
|
||
cursor = await db.execute("""
|
||
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
|
||
FROM pets p
|
||
JOIN pet_species ps ON p.species_id = ps.id
|
||
WHERE p.player_id = ?
|
||
ORDER BY p.is_active DESC, p.level DESC, p.id ASC
|
||
""", (player_dict['id'],))
|
||
pets_rows = await cursor.fetchall()
|
||
pets = []
|
||
for row in pets_rows:
|
||
pet_dict = {
|
||
'id': row[0], 'player_id': row[1], 'species_id': row[2],
|
||
'nickname': row[3], 'level': row[4], 'experience': row[5],
|
||
'hp': row[6], 'max_hp': row[7], 'attack': row[8],
|
||
'defense': row[9], 'speed': row[10], 'happiness': row[11],
|
||
'caught_at': row[12], 'is_active': bool(row[13]), # Convert to proper boolean
|
||
'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17]
|
||
}
|
||
pets.append(pet_dict)
|
||
|
||
# Get player achievements
|
||
cursor = await db.execute("""
|
||
SELECT pa.*, a.name as achievement_name, a.description as achievement_desc
|
||
FROM player_achievements pa
|
||
JOIN achievements a ON pa.achievement_id = a.id
|
||
WHERE pa.player_id = ?
|
||
ORDER BY pa.completed_at DESC
|
||
""", (player_dict['id'],))
|
||
achievements_rows = await cursor.fetchall()
|
||
achievements = []
|
||
for row in achievements_rows:
|
||
achievement_dict = {
|
||
'id': row[0], 'player_id': row[1], 'achievement_id': row[2],
|
||
'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5]
|
||
}
|
||
achievements.append(achievement_dict)
|
||
|
||
# Get player inventory
|
||
cursor = await db.execute("""
|
||
SELECT i.name, i.description, i.category, i.rarity, pi.quantity
|
||
FROM player_inventory pi
|
||
JOIN items i ON pi.item_id = i.id
|
||
WHERE pi.player_id = ?
|
||
ORDER BY i.rarity DESC, i.name ASC
|
||
""", (player_dict['id'],))
|
||
inventory_rows = await cursor.fetchall()
|
||
inventory = []
|
||
for row in inventory_rows:
|
||
item_dict = {
|
||
'name': row[0], 'description': row[1], 'category': row[2],
|
||
'rarity': row[3], 'quantity': row[4]
|
||
}
|
||
inventory.append(item_dict)
|
||
|
||
# Get player gym badges
|
||
cursor = await db.execute("""
|
||
SELECT g.name, g.badge_name, g.badge_icon, l.name as location_name,
|
||
pgb.victories, pgb.first_victory_date, pgb.highest_difficulty
|
||
FROM player_gym_battles pgb
|
||
JOIN gyms g ON pgb.gym_id = g.id
|
||
JOIN locations l ON g.location_id = l.id
|
||
WHERE pgb.player_id = ? AND pgb.victories > 0
|
||
ORDER BY pgb.first_victory_date ASC
|
||
""", (player_dict['id'],))
|
||
gym_badges_rows = await cursor.fetchall()
|
||
gym_badges = []
|
||
for row in gym_badges_rows:
|
||
badge_dict = {
|
||
'gym_name': row[0], 'badge_name': row[1], 'badge_icon': row[2],
|
||
'location_name': row[3], 'victories': row[4],
|
||
'first_victory_date': row[5], 'highest_difficulty': row[6]
|
||
}
|
||
gym_badges.append(badge_dict)
|
||
|
||
# Get player encounters using database method
|
||
encounters = []
|
||
try:
|
||
# Use the existing database method which handles row factory properly
|
||
temp_encounters = await database.get_player_encounters(player_dict['id'])
|
||
for enc in temp_encounters:
|
||
encounter_dict = {
|
||
'species_name': enc['species_name'],
|
||
'type1': enc['type1'],
|
||
'type2': enc['type2'],
|
||
'rarity': enc['rarity'],
|
||
'total_encounters': enc['total_encounters'],
|
||
'caught_count': enc['caught_count'],
|
||
'first_encounter_date': enc['first_encounter_date']
|
||
}
|
||
encounters.append(encounter_dict)
|
||
except Exception as e:
|
||
print(f"Error fetching encounters: {e}")
|
||
encounters = []
|
||
|
||
# Get encounter stats
|
||
try:
|
||
encounter_stats = await database.get_encounter_stats(player_dict['id'])
|
||
except Exception as e:
|
||
print(f"Error fetching encounter stats: {e}")
|
||
encounter_stats = {
|
||
'species_encountered': 0,
|
||
'total_encounters': 0,
|
||
'total_species': 0,
|
||
'completion_percentage': 0.0
|
||
}
|
||
|
||
return {
|
||
'player': player_dict,
|
||
'pets': pets,
|
||
'achievements': achievements,
|
||
'inventory': inventory,
|
||
'gym_badges': gym_badges,
|
||
'encounters': encounters,
|
||
'encounter_stats': encounter_stats
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"Database error fetching player {nickname}: {e}")
|
||
return None
|
||
|
||
def serve_player_not_found(self, nickname):
|
||
"""Serve player not found page using unified template"""
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>🚫 Player Not Found</h1>
|
||
</div>
|
||
|
||
<div class="error-message">
|
||
<h2>Player "{nickname}" not found</h2>
|
||
<p>This player hasn't started their journey yet or doesn't exist.</p>
|
||
<p>Players can use <code>!start</code> in #petz to begin their adventure!</p>
|
||
</div>
|
||
"""
|
||
|
||
# Add error-specific CSS
|
||
additional_css = """
|
||
.main-container {
|
||
text-align: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.error-message {
|
||
background: var(--bg-secondary);
|
||
padding: 40px;
|
||
border-radius: 15px;
|
||
border: 2px solid var(--error-color);
|
||
margin-top: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.error-message h2 {
|
||
color: var(--error-color);
|
||
margin-top: 0;
|
||
}
|
||
"""
|
||
|
||
html_content = self.get_page_template("Player Not Found", content, "players")
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(404)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_player_error(self, nickname, error_msg):
|
||
"""Serve player error page using unified template"""
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>⚠️ Error</h1>
|
||
</div>
|
||
|
||
<div class="error-message">
|
||
<h2>Unable to load player data</h2>
|
||
<p>{error_msg}</p>
|
||
<p>Please try again later or contact an administrator.</p>
|
||
</div>
|
||
"""
|
||
|
||
# Add error-specific CSS
|
||
additional_css = """
|
||
.main-container {
|
||
text-align: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.error-message {
|
||
background: var(--bg-secondary);
|
||
padding: 40px;
|
||
border-radius: 15px;
|
||
border: 2px solid var(--error-color);
|
||
margin-top: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.error-message h2 {
|
||
color: var(--error-color);
|
||
margin-top: 0;
|
||
}
|
||
"""
|
||
|
||
html_content = self.get_page_template("Player Error", content, "players")
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(500)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_player_data(self, nickname, player_data):
|
||
"""Serve player profile page with real data"""
|
||
player = player_data['player']
|
||
pets = player_data['pets']
|
||
achievements = player_data['achievements']
|
||
inventory = player_data.get('inventory', [])
|
||
gym_badges = player_data.get('gym_badges', [])
|
||
encounters = player_data.get('encounters', [])
|
||
encounter_stats = player_data.get('encounter_stats', {})
|
||
|
||
# Calculate stats
|
||
active_pets = [pet for pet in pets if pet['is_active']]
|
||
total_pets = len(pets)
|
||
active_count = len(active_pets)
|
||
|
||
# Build pets table HTML
|
||
pets_html = ""
|
||
if pets:
|
||
for pet in pets:
|
||
status = "⭐ Active" if pet['is_active'] else "📦 Storage"
|
||
status_class = "pet-active" if pet['is_active'] else "pet-stored"
|
||
name = pet['nickname'] or pet['species_name']
|
||
|
||
type_str = pet['type1']
|
||
if pet['type2']:
|
||
type_str += f"/{pet['type2']}"
|
||
|
||
pets_html += f"""
|
||
<tr>
|
||
<td class="{status_class}">{status}</td>
|
||
<td><strong>{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>"""
|
||
else:
|
||
inventory_html = """
|
||
<div class="empty-state">
|
||
<div class="empty-icon">🎒</div>
|
||
<h3>No items yet</h3>
|
||
<p>Try exploring to find useful items!</p>
|
||
</div>"""
|
||
|
||
# Build gym badges HTML
|
||
badges_html = ""
|
||
if gym_badges:
|
||
for badge in gym_badges:
|
||
# Safely handle date formatting
|
||
try:
|
||
if badge['first_victory_date'] and isinstance(badge['first_victory_date'], str):
|
||
badge_date = badge['first_victory_date'].split()[0]
|
||
else:
|
||
badge_date = 'Unknown'
|
||
except (AttributeError, IndexError):
|
||
badge_date = 'Unknown'
|
||
badges_html += f"""
|
||
<div class="badge-card">
|
||
<div class="badge-icon">{badge['badge_icon']}</div>
|
||
<div class="badge-content">
|
||
<h4>{badge['badge_name']}</h4>
|
||
<p>Earned from {badge['gym_name']} ({badge['location_name']})</p>
|
||
<div class="badge-stats">
|
||
<span>First victory: {badge_date}</span>
|
||
<span>Total victories: {badge['victories']}</span>
|
||
<span>Highest difficulty: Level {badge['highest_difficulty']}</span>
|
||
</div>
|
||
</div>
|
||
</div>"""
|
||
else:
|
||
badges_html = """
|
||
<div class="empty-state">
|
||
<div class="empty-icon">🏆</div>
|
||
<h3>No gym badges yet</h3>
|
||
<p>Challenge gyms to earn badges and prove your training skills!</p>
|
||
</div>"""
|
||
|
||
# Build encounters HTML
|
||
encounters_html = ""
|
||
if encounters:
|
||
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
|
||
for encounter in encounters:
|
||
rarity_color = rarity_colors.get(encounter['rarity'], "#ffffff")
|
||
type_str = encounter['type1']
|
||
if encounter['type2']:
|
||
type_str += f"/{encounter['type2']}"
|
||
|
||
# Safely handle date formatting
|
||
try:
|
||
if encounter['first_encounter_date'] and isinstance(encounter['first_encounter_date'], str):
|
||
encounter_date = encounter['first_encounter_date'].split()[0]
|
||
else:
|
||
encounter_date = 'Unknown'
|
||
except (AttributeError, IndexError):
|
||
encounter_date = 'Unknown'
|
||
|
||
encounters_html += f"""
|
||
<div class="encounter-card" style="border-left-color: {rarity_color};">
|
||
<div class="encounter-header">
|
||
<strong style="color: {rarity_color};">{encounter['species_name']}</strong>
|
||
<span class="type-badge">{type_str}</span>
|
||
</div>
|
||
<div class="encounter-stats">
|
||
<span>Encountered {encounter['total_encounters']} times</span>
|
||
<span>Caught {encounter['caught_count']} times</span>
|
||
</div>
|
||
<div class="encounter-date">First seen: {encounter_date}</div>
|
||
</div>"""
|
||
else:
|
||
encounters_html = """
|
||
<div class="empty-state">
|
||
<div class="empty-icon">👁️</div>
|
||
<h3>No pets encountered yet</h3>
|
||
<p>Use !explore to discover wild pets!</p>
|
||
</div>"""
|
||
|
||
# Build content for the unified template
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>🐾 {nickname}'s Profile</h1>
|
||
<p>Level {player['level']} Trainer</p>
|
||
<p><em>Currently in {player.get('location_name', 'Unknown Location')}</em></p>
|
||
<div style="margin-top: 20px;">
|
||
<a href="/teambuilder/{nickname}" class="btn btn-primary">
|
||
🔧 Team Builder
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Navigation -->
|
||
<div class="quick-nav">
|
||
<a href="#stats" class="nav-pill">📊 Stats</a>
|
||
<a href="#pets" class="nav-pill">🐾 Pets</a>
|
||
<a href="#achievements" class="nav-pill">🏆 Achievements</a>
|
||
<a href="#inventory" class="nav-pill">🎒 Inventory</a>
|
||
<a href="#gym-badges" class="nav-pill">🏆 Badges</a>
|
||
<a href="#encounters" class="nav-pill">👁️ Encounters</a>
|
||
</div>
|
||
|
||
<div class="section" id="stats">
|
||
<div class="section-header">📊 Player Statistics</div>
|
||
<div class="section-content">
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-value">{player['level']}</div>
|
||
<div class="stat-label">Level</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{player['experience']}</div>
|
||
<div class="stat-label">Experience</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">${player['money']}</div>
|
||
<div class="stat-label">Money</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{total_pets}</div>
|
||
<div class="stat-label">Pets Caught</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{active_count}</div>
|
||
<div class="stat-label">Active Pets</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{len(achievements)}</div>
|
||
<div class="stat-label">Achievements</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{encounter_stats.get('species_encountered', 0)}</div>
|
||
<div class="stat-label">Species Seen</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value">{encounter_stats.get('completion_percentage', 0)}%</div>
|
||
<div class="stat-label">Petdex Complete</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="pets">
|
||
<div class="section-header">🐾 Pet Collection</div>
|
||
<div class="section-content">
|
||
<div class="pets-container">
|
||
<div class="pets-table-wrapper">
|
||
<table class="pets-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Status</th>
|
||
<th>Name</th>
|
||
<th>Species</th>
|
||
<th>Type</th>
|
||
<th>Level</th>
|
||
<th>HP</th>
|
||
<th>Stats</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{pets_html}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="achievements">
|
||
<div class="section-header">🏆 Achievements</div>
|
||
<div class="section-content">
|
||
<div class="achievements-grid">
|
||
{achievements_html}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="inventory">
|
||
<div class="section-header">🎒 Inventory</div>
|
||
<div class="section-content">
|
||
<div class="inventory-grid">
|
||
{inventory_html}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="gym-badges">
|
||
<div class="section-header">🏆 Gym Badges</div>
|
||
<div class="section-content">
|
||
<div class="badges-grid">
|
||
{badges_html}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="encounters">
|
||
<div class="section-header">👁️ Pet Encounters</div>
|
||
<div class="section-content">
|
||
<div class="encounters-summary">
|
||
<p>Species discovered: {encounter_stats.get('species_encountered', 0)}/{encounter_stats.get('total_species', 0)}
|
||
({encounter_stats.get('completion_percentage', 0)}% complete)</p>
|
||
<p>Total encounters: {encounter_stats.get('total_encounters', 0)}</p>
|
||
</div>
|
||
<div class="encounters-grid">
|
||
{encounters_html}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
|
||
# Add custom CSS for the profile page
|
||
additional_css = """
|
||
|
||
/* Profile Page Styles */
|
||
html {
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
.quick-nav {
|
||
background: var(--bg-secondary);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
box-shadow: 0 2px 10px var(--shadow-color);
|
||
}
|
||
|
||
.nav-pill {
|
||
display: inline-block;
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
text-decoration: none;
|
||
margin: 5px;
|
||
font-size: 0.9em;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.nav-pill:hover {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--bg-tertiary);
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
border: 1px solid var(--border-color);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: var(--text-accent);
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.pets-table-wrapper {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.pets-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
min-width: 600px;
|
||
}
|
||
|
||
.pets-table th, .pets-table td {
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.pets-table th {
|
||
background: var(--bg-primary);
|
||
color: var(--text-accent);
|
||
font-weight: 600;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.pets-table tr:hover {
|
||
background: var(--hover-color);
|
||
}
|
||
|
||
.pet-active {
|
||
color: var(--text-accent);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.pet-stored {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.type-badge {
|
||
background: var(--bg-primary);
|
||
color: var(--text-accent);
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 0.8em;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.achievements-grid, .badges-grid, .encounters-grid, .inventory-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.achievement-card, .badge-card, .encounter-card, .inventory-item {
|
||
background: var(--bg-tertiary);
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid var(--text-accent);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.achievement-card:hover, .badge-card:hover, .encounter-card:hover, .inventory-item:hover {
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
.achievement-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 15px;
|
||
}
|
||
|
||
.achievement-icon {
|
||
font-size: 1.5em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.achievement-content h4 {
|
||
margin: 0 0 8px 0;
|
||
color: var(--text-accent);
|
||
}
|
||
|
||
.achievement-content p {
|
||
margin: 0 0 8px 0;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.achievement-date {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.badge-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 15px;
|
||
border-left-color: gold;
|
||
}
|
||
|
||
.badge-icon {
|
||
font-size: 1.5em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.badge-content h4 {
|
||
margin: 0 0 8px 0;
|
||
color: gold;
|
||
}
|
||
|
||
.badge-content p {
|
||
margin: 0 0 10px 0;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.badge-stats {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.badge-stats span {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.encounter-card {
|
||
border-left: 4px solid var(--text-accent);
|
||
}
|
||
|
||
.encounter-header {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.encounter-stats {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.encounter-stats span {
|
||
color: var(--text-primary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.encounter-date {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.inventory-item {
|
||
border-left: 4px solid var(--text-accent);
|
||
}
|
||
|
||
.item-header {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.item-description {
|
||
color: var(--text-primary);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.item-meta {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 3em;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.empty-state h3 {
|
||
margin: 0 0 10px 0;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.empty-state p {
|
||
margin: 0;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.encounters-summary {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.encounters-summary p {
|
||
margin: 5px 0;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.btn {
|
||
display: inline-block;
|
||
padding: 12px 24px;
|
||
border-radius: 6px;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--gradient-primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
/* Mobile Responsive */
|
||
@media (max-width: 768px) {
|
||
.stats-grid {
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 15px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.achievements-grid, .badges-grid, .encounters-grid, .inventory-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.nav-pill {
|
||
padding: 6px 12px;
|
||
font-size: 0.8em;
|
||
margin: 3px;
|
||
}
|
||
|
||
.pets-table {
|
||
min-width: 500px;
|
||
}
|
||
|
||
.pets-table th, .pets-table td {
|
||
padding: 8px 10px;
|
||
font-size: 0.9em;
|
||
}
|
||
}
|
||
"""
|
||
|
||
# Get the unified template with the additional CSS
|
||
html_content = self.get_page_template(f"{nickname}'s Profile", content, "players")
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def log_message(self, format, *args):
|
||
"""Override to reduce logging noise"""
|
||
pass
|
||
|
||
def serve_teambuilder(self, nickname):
|
||
"""Serve the team builder interface"""
|
||
from urllib.parse import unquote
|
||
nickname = unquote(nickname)
|
||
|
||
try:
|
||
from src.database import Database
|
||
database = Database()
|
||
|
||
# Get event loop
|
||
try:
|
||
loop = asyncio.get_running_loop()
|
||
except RuntimeError:
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
|
||
|
||
if player_data is None:
|
||
self.serve_player_not_found(nickname)
|
||
return
|
||
|
||
pets = player_data['pets']
|
||
if not pets:
|
||
self.serve_teambuilder_no_pets(nickname)
|
||
return
|
||
|
||
self.serve_teambuilder_interface(nickname, pets)
|
||
|
||
except Exception as e:
|
||
print(f"Error loading team builder for {nickname}: {e}")
|
||
self.serve_player_error(nickname, f"Error loading team builder: {str(e)}")
|
||
|
||
def serve_teambuilder_no_pets(self, nickname):
|
||
"""Show message when player has no pets using unified template"""
|
||
content = f"""
|
||
<div class="header">
|
||
<h1>🐾 Team Builder</h1>
|
||
<p>Build your perfect team for battles and adventures</p>
|
||
</div>
|
||
|
||
<div class="no-pets-message">
|
||
<h2>🐾 No Pets Found</h2>
|
||
<p>{nickname}, you need to catch some pets before using the team builder!</p>
|
||
<p>Head to the IRC channel and use <code>!explore</code> to find wild pets!</p>
|
||
<a href="/player/{nickname}" class="btn btn-primary">← Back to Profile</a>
|
||
</div>
|
||
"""
|
||
|
||
# Add no-pets-specific CSS
|
||
additional_css = """
|
||
.main-container {
|
||
text-align: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.no-pets-message {
|
||
background: var(--bg-secondary);
|
||
padding: 40px;
|
||
border-radius: 15px;
|
||
border: 2px solid var(--warning-color);
|
||
margin-top: 30px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.no-pets-message h2 {
|
||
color: var(--warning-color);
|
||
margin-top: 0;
|
||
}
|
||
"""
|
||
|
||
html_content = self.get_page_template(f"Team Builder - {nickname}", content, "players")
|
||
html_content = html_content.replace("</style>", additional_css + "</style>")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def serve_teambuilder_interface(self, nickname, pets):
|
||
"""Serve the full interactive team builder interface"""
|
||
active_pets = [pet for pet in pets if pet['is_active']]
|
||
inactive_pets = [pet for pet in pets if not pet['is_active']]
|
||
|
||
# Debug logging
|
||
print(f"Team Builder Debug for {nickname}:")
|
||
print(f"Total pets: {len(pets)}")
|
||
active_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in active_pets]
|
||
inactive_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in inactive_pets]
|
||
print(f"Active pets: {len(active_pets)} - {active_names}")
|
||
print(f"Inactive pets: {len(inactive_pets)} - {inactive_names}")
|
||
|
||
# Generate detailed pet cards with debugging
|
||
def make_pet_card(pet, is_active):
|
||
name = pet['nickname'] or pet['species_name']
|
||
status = "Active" if is_active else "Storage"
|
||
status_class = "active" if is_active else "storage"
|
||
type_str = pet['type1']
|
||
if pet['type2']:
|
||
type_str += f"/{pet['type2']}"
|
||
|
||
# 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">{name}</h4>
|
||
<div class="status-badge">{status}</div>
|
||
</div>
|
||
<div class="pet-species">Level {pet['level']} {pet['species_name']}</div>
|
||
<div class="pet-type">{type_str}</div>
|
||
|
||
<div class="hp-section">
|
||
<div class="hp-label">HP: {pet['hp']}/{pet['max_hp']}</div>
|
||
<div class="hp-bar">
|
||
<div class="hp-fill" style="width: {hp_percent}%; background: {hp_color};"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat">
|
||
<span class="stat-label">ATK</span>
|
||
<span class="stat-value">{pet['attack']}</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-label">DEF</span>
|
||
<span class="stat-value">{pet['defense']}</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-label">SPD</span>
|
||
<span class="stat-value">{pet['speed']}</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-label">EXP</span>
|
||
<span class="stat-value">{pet['experience']}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pet-happiness">
|
||
<span class="happiness-emoji">{'😊' if pet['happiness'] > 70 else '😐' if pet['happiness'] > 40 else '😞'}</span>
|
||
<span>Happiness: {pet['happiness']}/100</span>
|
||
</div>
|
||
</div>"""
|
||
|
||
# Create 6 numbered slots and place pets in their positions
|
||
team_slots = [''] * 6 # Initialize 6 empty slots
|
||
|
||
# Place active pets in their team_order positions
|
||
for pet in active_pets:
|
||
team_order = pet.get('team_order')
|
||
if team_order and 1 <= team_order <= 6:
|
||
team_slots[team_order - 1] = make_pet_card(pet, True)
|
||
|
||
storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets)
|
||
|
||
html = f"""<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Team Builder - {nickname}</title>
|
||
<style>
|
||
:root {{
|
||
--bg-primary: #0f0f23;
|
||
--bg-secondary: #1e1e3f;
|
||
--bg-tertiary: #2a2a4a;
|
||
--text-primary: #cccccc;
|
||
--text-secondary: #999999;
|
||
--text-accent: #66ff66;
|
||
--active-color: #4CAF50;
|
||
--storage-color: #FF9800;
|
||
--drag-hover: #3a3a5a;
|
||
--shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||
}}
|
||
|
||
body {{
|
||
font-family: 'Segoe UI', Arial, sans-serif;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
margin: 0;
|
||
padding: 20px;
|
||
min-height: 100vh;
|
||
}}
|
||
|
||
.header {{
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 30px;
|
||
border-radius: 15px;
|
||
margin-bottom: 30px;
|
||
box-shadow: var(--shadow);
|
||
}}
|
||
|
||
.header h1 {{ margin: 0; font-size: 2.5em; }}
|
||
.header p {{ margin: 10px 0 0 0; opacity: 0.9; }}
|
||
|
||
.team-sections {{
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 30px;
|
||
margin-bottom: 30px;
|
||
}}
|
||
|
||
@media (max-width: 768px) {{
|
||
.team-sections {{ grid-template-columns: 1fr; }}
|
||
}}
|
||
|
||
.team-section {{
|
||
background: var(--bg-secondary);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
min-height: 500px;
|
||
box-shadow: var(--shadow);
|
||
}}
|
||
|
||
.section-header {{
|
||
font-size: 1.3em;
|
||
font-weight: bold;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}}
|
||
|
||
.active-header {{ background: var(--active-color); color: white; }}
|
||
.storage-header {{ background: var(--storage-color); color: white; }}
|
||
|
||
.pets-container {{
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 15px;
|
||
min-height: 200px;
|
||
}}
|
||
|
||
.team-slots-container {{
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 15px;
|
||
min-height: 400px;
|
||
}}
|
||
|
||
.team-slot {{
|
||
background: var(--bg-tertiary);
|
||
border: 2px dashed #666;
|
||
border-radius: 12px;
|
||
padding: 10px;
|
||
position: relative;
|
||
min-height: 120px;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}}
|
||
|
||
.team-slot:hover {{
|
||
border-color: var(--text-accent);
|
||
background: var(--drag-hover);
|
||
}}
|
||
|
||
.team-slot.drag-over {{
|
||
border-color: var(--text-accent);
|
||
background: var(--drag-hover);
|
||
border-style: solid;
|
||
transform: scale(1.02);
|
||
}}
|
||
|
||
.slot-number {{
|
||
position: absolute;
|
||
top: 5px;
|
||
left: 5px;
|
||
background: var(--active-color);
|
||
color: white;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.9em;
|
||
font-weight: bold;
|
||
z-index: 10;
|
||
}}
|
||
|
||
.slot-content {{
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}}
|
||
|
||
.slot-content:empty::before {{
|
||
content: "Empty Slot";
|
||
color: var(--text-secondary);
|
||
font-style: italic;
|
||
opacity: 0.7;
|
||
}}
|
||
|
||
.slot-content .pet-card {{
|
||
margin: 0;
|
||
width: 100%;
|
||
max-width: none;
|
||
}}
|
||
|
||
.pet-card {{
|
||
background: var(--bg-tertiary);
|
||
border-radius: 12px;
|
||
padding: 15px;
|
||
cursor: grab;
|
||
transition: all 0.3s ease;
|
||
border: 2px solid transparent;
|
||
position: relative;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
user-select: none;
|
||
}}
|
||
|
||
.pet-card:active {{
|
||
cursor: grabbing;
|
||
}}
|
||
|
||
.pet-card:hover {{
|
||
transform: translateY(-3px);
|
||
box-shadow: var(--shadow);
|
||
}}
|
||
|
||
.pet-card.active {{ border-color: var(--active-color); }}
|
||
.pet-card.storage {{ border-color: var(--storage-color); }}
|
||
|
||
.pet-card.dragging {{
|
||
opacity: 0.6;
|
||
transform: rotate(3deg) scale(0.95);
|
||
z-index: 1000;
|
||
}}
|
||
|
||
.pet-header {{
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 10px;
|
||
}}
|
||
|
||
.pet-name {{
|
||
margin: 0;
|
||
font-size: 1.2em;
|
||
color: var(--text-accent);
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.status-badge {{
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
font-size: 0.8em;
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.active .status-badge {{ background: var(--active-color); color: white; }}
|
||
.storage .status-badge {{ background: var(--storage-color); color: white; }}
|
||
|
||
.pet-species {{
|
||
color: var(--text-secondary);
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
}}
|
||
|
||
.pet-type {{
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 0.85em;
|
||
display: inline-block;
|
||
margin-bottom: 10px;
|
||
}}
|
||
|
||
.hp-section {{
|
||
margin: 10px 0;
|
||
}}
|
||
|
||
.hp-label {{
|
||
font-size: 0.9em;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 5px;
|
||
}}
|
||
|
||
.hp-bar {{
|
||
background: #333;
|
||
border-radius: 10px;
|
||
height: 8px;
|
||
overflow: hidden;
|
||
}}
|
||
|
||
.hp-fill {{
|
||
height: 100%;
|
||
border-radius: 10px;
|
||
transition: width 0.3s ease;
|
||
}}
|
||
|
||
.stats-grid {{
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
margin: 12px 0;
|
||
}}
|
||
|
||
.stat {{
|
||
display: flex;
|
||
justify-content: space-between;
|
||
background: rgba(255,255,255,0.05);
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
}}
|
||
|
||
.stat-label {{
|
||
color: var(--text-secondary);
|
||
font-size: 0.85em;
|
||
}}
|
||
|
||
.stat-value {{
|
||
color: var(--text-accent);
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.pet-happiness {{
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
color: var(--text-secondary);
|
||
}}
|
||
|
||
.happiness-emoji {{
|
||
font-size: 1.2em;
|
||
}}
|
||
|
||
.drop-zone {{
|
||
min-height: 80px;
|
||
border: 2px dashed #666;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-secondary);
|
||
margin-top: 15px;
|
||
transition: all 0.3s ease;
|
||
font-style: italic;
|
||
}}
|
||
|
||
.drop-zone.drag-over {{
|
||
border-color: var(--text-accent);
|
||
background: var(--drag-hover);
|
||
color: var(--text-accent);
|
||
border-style: solid;
|
||
}}
|
||
|
||
.drop-zone.has-pets {{
|
||
display: none;
|
||
}}
|
||
|
||
.controls {{
|
||
background: var(--bg-secondary);
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
text-align: center;
|
||
box-shadow: var(--shadow);
|
||
}}
|
||
|
||
.save-btn {{
|
||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 30px;
|
||
font-size: 1.1em;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
margin: 0 10px;
|
||
transition: all 0.3s ease;
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.save-btn:hover {{
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
|
||
}}
|
||
|
||
.save-btn:disabled {{
|
||
background: #666;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}}
|
||
|
||
.back-btn {{
|
||
background: linear-gradient(135deg, #666, #555);
|
||
color: white;
|
||
text-decoration: none;
|
||
padding: 15px 30px;
|
||
border-radius: 25px;
|
||
display: inline-block;
|
||
margin: 0 10px;
|
||
transition: all 0.3s ease;
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.back-btn:hover {{
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
||
}}
|
||
|
||
.pin-section {{
|
||
background: var(--bg-secondary);
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
margin-top: 20px;
|
||
text-align: center;
|
||
display: none;
|
||
box-shadow: var(--shadow);
|
||
}}
|
||
|
||
.pin-input {{
|
||
background: var(--bg-tertiary);
|
||
border: 2px solid #666;
|
||
color: var(--text-primary);
|
||
padding: 15px;
|
||
font-size: 1.3em;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
width: 200px;
|
||
margin: 0 10px;
|
||
letter-spacing: 2px;
|
||
}}
|
||
|
||
.pin-input:focus {{
|
||
border-color: var(--text-accent);
|
||
outline: none;
|
||
box-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
|
||
}}
|
||
|
||
.verify-btn {{
|
||
background: linear-gradient(135deg, #2196F3, #1976D2);
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 30px;
|
||
font-size: 1.1em;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
margin: 0 10px;
|
||
transition: all 0.3s ease;
|
||
font-weight: bold;
|
||
}}
|
||
|
||
.verify-btn:hover {{
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);
|
||
}}
|
||
|
||
.message {{
|
||
background: var(--bg-tertiary);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin: 15px 0;
|
||
text-align: center;
|
||
}}
|
||
|
||
.success {{ border-left: 4px solid #4CAF50; }}
|
||
.error {{ border-left: 4px solid #f44336; }}
|
||
.info {{ border-left: 4px solid #2196F3; }}
|
||
|
||
.empty-state {{
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: var(--text-secondary);
|
||
font-style: italic;
|
||
}}
|
||
|
||
.back-link {{
|
||
color: var(--text-accent);
|
||
text-decoration: none;
|
||
margin-bottom: 20px;
|
||
display: inline-block;
|
||
font-weight: 500;
|
||
}}
|
||
|
||
.back-link:hover {{
|
||
text-decoration: underline;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<a href="/player/{nickname}" class="back-link">← Back to {nickname}'s Profile</a>
|
||
|
||
<div class="header">
|
||
<h1>🐾 Team Builder</h1>
|
||
<p>Drag pets between Active and Storage to build your perfect team</p>
|
||
<p><strong>{nickname}</strong> | Active: {len(active_pets)} pets | Storage: {len(inactive_pets)} pets</p>
|
||
</div>
|
||
|
||
<div class="team-sections">
|
||
<div class="team-section">
|
||
<div class="section-header active-header">⭐ Active Team</div>
|
||
<div class="team-slots-container" id="team-slots-container">
|
||
<div class="team-slot" id="slot-1" data-position="1">
|
||
<div class="slot-number">1</div>
|
||
<div class="slot-content">{team_slots[0]}</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-2" data-position="2">
|
||
<div class="slot-number">2</div>
|
||
<div class="slot-content">{team_slots[1]}</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-3" data-position="3">
|
||
<div class="slot-number">3</div>
|
||
<div class="slot-content">{team_slots[2]}</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-4" data-position="4">
|
||
<div class="slot-number">4</div>
|
||
<div class="slot-content">{team_slots[3]}</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-5" data-position="5">
|
||
<div class="slot-number">5</div>
|
||
<div class="slot-content">{team_slots[4]}</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-6" data-position="6">
|
||
<div class="slot-number">6</div>
|
||
<div class="slot-content">{team_slots[5]}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="team-section">
|
||
<div class="section-header storage-header">📦 Storage</div>
|
||
<div class="pets-container" id="storage-container">
|
||
{storage_cards}
|
||
</div>
|
||
<div class="drop-zone {('has-pets' if inactive_pets else '')}" id="storage-drop">
|
||
Drop pets here to store them
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="save-btn" id="save-btn" onclick="saveTeam()">🔒 Save Team Changes</button>
|
||
<a href="/player/{nickname}" class="back-btn">← Back to Profile</a>
|
||
<div style="margin-top: 15px; color: var(--text-secondary); font-size: 0.9em;">
|
||
Changes are saved securely with PIN verification via IRC
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pin-section" id="pin-section">
|
||
<h3>🔐 PIN Verification Required</h3>
|
||
<p>A 6-digit PIN has been sent to you via IRC private message.</p>
|
||
<p>Enter the PIN below to confirm your team changes:</p>
|
||
<input type="text" class="pin-input" id="pin-input" placeholder="000000" maxlength="6">
|
||
<button class="verify-btn" onclick="verifyPin()">✅ Verify & Apply Changes</button>
|
||
<div id="message-area"></div>
|
||
</div>
|
||
|
||
<script>
|
||
let originalTeam = {{}};
|
||
let currentTeam = {{}};
|
||
let draggedElement = null;
|
||
|
||
// Test function to verify basic functionality
|
||
function runDragDropTest() {{
|
||
console.log('=== DRAG DROP TEST ===');
|
||
const petCards = document.querySelectorAll('.pet-card');
|
||
console.log('Pet cards found:', petCards.length);
|
||
|
||
petCards.forEach((card, index) => {{
|
||
console.log(`Card ${{index}}: ID=${{card.dataset.petId}}, draggable=${{card.draggable}}, active=${{card.dataset.active}}`);
|
||
}});
|
||
|
||
const containers = ['active-container', 'storage-container', 'active-drop', 'storage-drop'];
|
||
containers.forEach(id => {{
|
||
const element = document.getElementById(id);
|
||
console.log(`Container ${{id}}: exists=${{!!element}}`);
|
||
}});
|
||
|
||
// Test function availability (non-destructive)
|
||
if (petCards.length > 0) {{
|
||
console.log('Testing function availability...');
|
||
const testCard = petCards[0];
|
||
const petId = testCard.dataset.petId;
|
||
const isCurrentlyActive = currentTeam[petId];
|
||
|
||
console.log(`Test pet ${{petId}} is currently: ${{isCurrentlyActive ? 'active' : 'storage'}}`);
|
||
console.log('Move functions available:', {{
|
||
movePetToStorage: typeof movePetToStorage === 'function',
|
||
movePetToActive: typeof movePetToActive === 'function'
|
||
}});
|
||
console.log('✅ Test complete - no pets were moved');
|
||
}}
|
||
}}
|
||
|
||
// Add click-to-move as backup for drag issues
|
||
function addClickToMoveBackup() {{
|
||
document.querySelectorAll('.pet-card').forEach(card => {{
|
||
// Add double-click handler
|
||
card.addEventListener('dblclick', function() {{
|
||
const petId = this.dataset.petId;
|
||
const currentPosition = currentTeam[petId];
|
||
|
||
console.log(`Double-click: Moving pet ${{petId}} from ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`);
|
||
|
||
if (currentPosition) {{
|
||
movePetToStorage(petId);
|
||
}} else {{
|
||
// Find first empty slot
|
||
for (let i = 1; i <= 6; i++) {{
|
||
const slot = document.getElementById(`slot-${{i}}`);
|
||
const slotContent = slot.querySelector('.slot-content');
|
||
if (slotContent.children.length === 0) {{
|
||
movePetToTeamSlot(petId, i);
|
||
return;
|
||
}}
|
||
}}
|
||
console.log('No empty slots available');
|
||
}}
|
||
}});
|
||
|
||
// Add visual hint
|
||
const hint = document.createElement('div');
|
||
hint.textContent = '💡 Double-click to move';
|
||
hint.style.cssText = 'position: absolute; top: 5px; left: 5px; background: rgba(0,0,0,0.7); color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; pointer-events: none; opacity: 0; transition: opacity 0.3s;';
|
||
card.style.position = 'relative';
|
||
card.appendChild(hint);
|
||
|
||
card.addEventListener('mouseenter', () => hint.style.opacity = '1');
|
||
card.addEventListener('mouseleave', () => hint.style.opacity = '0');
|
||
}});
|
||
}}
|
||
|
||
// Declare container variables once at the top level
|
||
const teamSlotsContainer = document.getElementById('team-slots-container');
|
||
const storageContainer = document.getElementById('storage-container');
|
||
const storageDrop = document.getElementById('storage-drop');
|
||
const teamSlots = Array.from({{length: 6}}, (_, i) => document.getElementById(`slot-${{i + 1}}`));
|
||
|
||
// Initialize team state with detailed debugging
|
||
console.log('=== TEAM STATE INITIALIZATION ===');
|
||
const allCards = document.querySelectorAll('.pet-card');
|
||
console.log(`Found ${{allCards.length}} pet cards total`);
|
||
console.log(`Team slots container:`, teamSlotsContainer);
|
||
console.log(`Storage container has ${{storageContainer.children.length}} pets initially`);
|
||
|
||
let teamPositions = {{}}; // Track pet positions in team slots
|
||
|
||
allCards.forEach((card, index) => {{
|
||
const petId = card.dataset.petId;
|
||
const isActive = card.dataset.active === 'true';
|
||
const teamOrder = card.dataset.teamOrder;
|
||
|
||
console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, team_order=${{teamOrder}}`);
|
||
|
||
if (isActive && teamOrder) {{
|
||
teamPositions[petId] = parseInt(teamOrder);
|
||
originalTeam[petId] = parseInt(teamOrder);
|
||
currentTeam[petId] = parseInt(teamOrder);
|
||
}} else {{
|
||
originalTeam[petId] = false;
|
||
currentTeam[petId] = false;
|
||
}}
|
||
}});
|
||
|
||
console.log('Team positions:', teamPositions);
|
||
console.log('Original team state:', originalTeam);
|
||
console.log('Current team state:', currentTeam);
|
||
|
||
// Completely rewritten drag and drop for slot system
|
||
function initializeDragAndDrop() {{
|
||
console.log('Initializing drag and drop...');
|
||
|
||
// Make all pet cards draggable
|
||
document.querySelectorAll('.pet-card').forEach(card => {{
|
||
card.draggable = true;
|
||
card.style.cursor = 'grab';
|
||
|
||
card.addEventListener('dragstart', function(e) {{
|
||
console.log('DRAGSTART: Pet ID', this.dataset.petId);
|
||
draggedElement = this;
|
||
this.style.opacity = '0.5';
|
||
this.style.cursor = 'grabbing';
|
||
|
||
// Check if dataTransfer exists (it won't in synthetic events)
|
||
if (e.dataTransfer) {{
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
e.dataTransfer.setData('text/plain', this.dataset.petId);
|
||
}} else {{
|
||
console.log('Warning: dataTransfer is null (synthetic event)');
|
||
}}
|
||
}});
|
||
|
||
card.addEventListener('dragend', function(e) {{
|
||
console.log('DRAGEND');
|
||
this.style.opacity = '';
|
||
this.style.cursor = 'grab';
|
||
draggedElement = null;
|
||
// Clear all highlights
|
||
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||
}});
|
||
}});
|
||
|
||
// Set up team slot drop zones
|
||
teamSlots.forEach((slot, index) => {{
|
||
const position = index + 1;
|
||
|
||
slot.addEventListener('dragover', function(e) {{
|
||
e.preventDefault();
|
||
if (e.dataTransfer) {{
|
||
e.dataTransfer.dropEffect = 'move';
|
||
}}
|
||
}});
|
||
|
||
slot.addEventListener('dragenter', function(e) {{
|
||
e.preventDefault();
|
||
this.classList.add('drag-over');
|
||
console.log(`DRAGENTER: Team slot ${{position}}`);
|
||
}});
|
||
|
||
slot.addEventListener('dragleave', function(e) {{
|
||
if (!this.contains(e.relatedTarget)) {{
|
||
this.classList.remove('drag-over');
|
||
}}
|
||
}});
|
||
|
||
slot.addEventListener('drop', function(e) {{
|
||
e.preventDefault();
|
||
console.log(`DROP: Team slot ${{position}}`);
|
||
this.classList.remove('drag-over');
|
||
|
||
if (draggedElement) {{
|
||
const petId = draggedElement.dataset.petId;
|
||
console.log(`Moving pet ${{petId}} to team slot ${{position}}`);
|
||
movePetToTeamSlot(petId, position);
|
||
}}
|
||
}});
|
||
}});
|
||
|
||
// Set up storage drop zones
|
||
[storageContainer, storageDrop].forEach(zone => {{
|
||
if (zone) {{
|
||
zone.addEventListener('dragover', function(e) {{
|
||
e.preventDefault();
|
||
if (e.dataTransfer) {{
|
||
e.dataTransfer.dropEffect = 'move';
|
||
}}
|
||
}});
|
||
|
||
zone.addEventListener('dragenter', function(e) {{
|
||
e.preventDefault();
|
||
this.classList.add('drag-over');
|
||
console.log('DRAGENTER: Storage zone');
|
||
}});
|
||
|
||
zone.addEventListener('dragleave', function(e) {{
|
||
if (!this.contains(e.relatedTarget)) {{
|
||
this.classList.remove('drag-over');
|
||
}}
|
||
}});
|
||
|
||
zone.addEventListener('drop', function(e) {{
|
||
e.preventDefault();
|
||
console.log('DROP: Storage zone');
|
||
this.classList.remove('drag-over');
|
||
|
||
if (draggedElement) {{
|
||
const petId = draggedElement.dataset.petId;
|
||
console.log('Moving pet', petId, 'to storage');
|
||
movePetToStorage(petId);
|
||
}}
|
||
}});
|
||
}}
|
||
}});
|
||
|
||
console.log('Drag and drop initialization complete');
|
||
}}
|
||
|
||
function movePetToTeamSlot(petId, position) {{
|
||
console.log(`movePetToTeamSlot called for pet ${{petId}}, position ${{position}}`);
|
||
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
|
||
if (!card) {{
|
||
console.log(`No card found for pet ${{petId}}`);
|
||
return;
|
||
}}
|
||
|
||
const slot = document.getElementById(`slot-${{position}}`);
|
||
if (!slot) {{
|
||
console.log(`No slot found for position ${{position}}`);
|
||
return;
|
||
}}
|
||
|
||
const slotContent = slot.querySelector('.slot-content');
|
||
|
||
// Check if slot is already occupied
|
||
if (slotContent.children.length > 0) {{
|
||
console.log(`Slot ${{position}} is already occupied, swapping pets`);
|
||
const existingCard = slotContent.querySelector('.pet-card');
|
||
if (existingCard) {{
|
||
const existingPetId = existingCard.dataset.petId;
|
||
// Move existing pet to storage
|
||
movePetToStorage(existingPetId);
|
||
}}
|
||
}}
|
||
|
||
// Update state
|
||
currentTeam[petId] = position;
|
||
|
||
// Move DOM element
|
||
card.classList.remove('storage');
|
||
card.classList.add('active');
|
||
card.dataset.active = 'true';
|
||
card.dataset.teamOrder = position;
|
||
card.querySelector('.status-badge').textContent = 'Active';
|
||
slotContent.appendChild(card);
|
||
|
||
// Update interface
|
||
updateSaveButton();
|
||
|
||
console.log(`Pet moved to team slot ${{position}} successfully`);
|
||
}}
|
||
|
||
function movePetToStorage(petId) {{
|
||
console.log(`movePetToStorage called for pet ${{petId}}`);
|
||
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
|
||
if (!card) {{
|
||
console.log(`No card found for pet ${{petId}}`);
|
||
return;
|
||
}}
|
||
|
||
const currentPosition = currentTeam[petId];
|
||
|
||
console.log(`Pet ${{petId}} current state: ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`);
|
||
|
||
if (currentPosition) {{
|
||
console.log(`Moving pet ${{petId}} to storage...`);
|
||
|
||
// Update state
|
||
currentTeam[petId] = false;
|
||
|
||
// Move DOM element
|
||
card.classList.remove('active');
|
||
card.classList.add('storage');
|
||
card.dataset.active = 'false';
|
||
card.dataset.teamOrder = '';
|
||
card.querySelector('.status-badge').textContent = 'Storage';
|
||
storageContainer.appendChild(card);
|
||
|
||
// Update interface
|
||
updateSaveButton();
|
||
updateDropZoneVisibility();
|
||
|
||
console.log('Pet moved to storage successfully');
|
||
}} else {{
|
||
console.log(`Pet ${{petId}} is already in storage, no move needed`);
|
||
}}
|
||
}}
|
||
|
||
|
||
function updateDropZoneVisibility() {{
|
||
// Update storage drop zone visibility
|
||
if (storageContainer && storageContainer.children.length > 0) {{
|
||
if (storageDrop) storageDrop.classList.add('has-pets');
|
||
}} else {{
|
||
if (storageDrop) storageDrop.classList.remove('has-pets');
|
||
}}
|
||
|
||
console.log('Drop zone visibility updated:', {{
|
||
storageContainerPets: storageContainer ? storageContainer.children.length : 0
|
||
}});
|
||
}}
|
||
|
||
function updateSaveButton() {{
|
||
const hasChanges = JSON.stringify(originalTeam) !== JSON.stringify(currentTeam);
|
||
const saveBtn = document.getElementById('save-btn');
|
||
saveBtn.disabled = !hasChanges;
|
||
|
||
if (hasChanges) {{
|
||
saveBtn.textContent = '🔒 Save Team Changes';
|
||
}} else {{
|
||
saveBtn.textContent = '✅ No Changes';
|
||
}}
|
||
}}
|
||
|
||
async function saveTeam() {{
|
||
const changes = {{}};
|
||
for (const petId in currentTeam) {{
|
||
if (currentTeam[petId] !== originalTeam[petId]) {{
|
||
changes[petId] = currentTeam[petId];
|
||
}}
|
||
}}
|
||
|
||
if (Object.keys(changes).length === 0) {{
|
||
showMessage('No changes to save!', 'info');
|
||
return;
|
||
}}
|
||
|
||
try {{
|
||
const response = await fetch('/teambuilder/{nickname}/save', {{
|
||
method: 'POST',
|
||
headers: {{ 'Content-Type': 'application/json' }},
|
||
body: JSON.stringify(changes)
|
||
}});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {{
|
||
showMessage('PIN sent to IRC! Check your private messages.', 'success');
|
||
document.getElementById('pin-section').style.display = 'block';
|
||
document.getElementById('pin-section').scrollIntoView({{ behavior: 'smooth' }});
|
||
}} else {{
|
||
showMessage('Error: ' + result.error, 'error');
|
||
}}
|
||
}} catch (error) {{
|
||
showMessage('Network error: ' + error.message, 'error');
|
||
}}
|
||
}}
|
||
|
||
async function verifyPin() {{
|
||
const pin = document.getElementById('pin-input').value.trim();
|
||
if (pin.length !== 6 || !/^\\d{{6}}$/.test(pin)) {{
|
||
showMessage('Please enter a valid 6-digit PIN', 'error');
|
||
return;
|
||
}}
|
||
|
||
try {{
|
||
const response = await fetch('/teambuilder/{nickname}/verify', {{
|
||
method: 'POST',
|
||
headers: {{ 'Content-Type': 'application/json' }},
|
||
body: JSON.stringify({{ pin: pin }})
|
||
}});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {{
|
||
showMessage('Team changes applied successfully! 🎉', 'success');
|
||
// Update original team state
|
||
originalTeam = {{...currentTeam}};
|
||
updateSaveButton();
|
||
document.getElementById('pin-section').style.display = 'none';
|
||
document.getElementById('pin-input').value = '';
|
||
|
||
// Visual celebration
|
||
document.querySelectorAll('.pet-card').forEach(card => {{
|
||
card.style.animation = 'bounce 0.6s ease-in-out';
|
||
}});
|
||
setTimeout(() => {{
|
||
document.querySelectorAll('.pet-card').forEach(card => {{
|
||
card.style.animation = '';
|
||
}});
|
||
}}, 600);
|
||
}} else {{
|
||
showMessage('Verification failed: ' + result.error, 'error');
|
||
}}
|
||
}} catch (error) {{
|
||
showMessage('Network error: ' + error.message, 'error');
|
||
}}
|
||
}}
|
||
|
||
function showMessage(text, type) {{
|
||
const messageArea = document.getElementById('message-area');
|
||
messageArea.innerHTML = `<div class="message ${{type}}">${{text}}</div>`;
|
||
|
||
// Auto-hide success messages
|
||
if (type === 'success') {{
|
||
setTimeout(() => {{
|
||
messageArea.innerHTML = '';
|
||
}}, 5000);
|
||
}}
|
||
}}
|
||
|
||
// Add keyboard support for PIN input
|
||
document.getElementById('pin-input').addEventListener('keypress', function(e) {{
|
||
if (e.key === 'Enter') {{
|
||
verifyPin();
|
||
}}
|
||
}});
|
||
|
||
// Initialize interface with debugging
|
||
console.log('Starting initialization...');
|
||
|
||
// Debug initial state (using previously declared variables)
|
||
console.log('Initial state:', {{
|
||
activePets: activeContainer.children.length,
|
||
storagePets: storageContainer.children.length
|
||
}});
|
||
|
||
initializeDragAndDrop();
|
||
addClickToMoveBackup(); // Add double-click as backup
|
||
updateSaveButton();
|
||
|
||
// Delay updateDropZoneVisibility to ensure DOM is fully settled
|
||
console.log('Before updateDropZoneVisibility...');
|
||
setTimeout(() => {{
|
||
console.log('Running delayed updateDropZoneVisibility...');
|
||
updateDropZoneVisibility();
|
||
}}, 100);
|
||
|
||
console.log('Initialization complete.');
|
||
|
||
// Test available via manual button only - no automatic execution
|
||
|
||
// Add test button for manual debugging
|
||
const testButton = document.createElement('button');
|
||
testButton.textContent = '🧪 Test Functions';
|
||
testButton.style.cssText = 'position: fixed; top: 10px; right: 10px; z-index: 9999; padding: 10px; background: #ff6b6b; color: white; border: none; border-radius: 5px; cursor: pointer;';
|
||
testButton.onclick = runDragDropTest;
|
||
document.body.appendChild(testButton);
|
||
|
||
// Add instruction for backup method
|
||
const instruction = document.createElement('div');
|
||
instruction.innerHTML = '💡 <strong>Backup:</strong> Double-click any pet to move it between Active/Storage';
|
||
instruction.style.cssText = 'position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.8); color: white; padding: 10px; border-radius: 5px; font-size: 12px; z-index: 9999; max-width: 250px;';
|
||
document.body.appendChild(instruction);
|
||
|
||
// Add bounce animation
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes bounce {{
|
||
0%, 20%, 60%, 100% {{ transform: translateY(0); }}
|
||
40% {{ transform: translateY(-10px); }}
|
||
80% {{ transform: translateY(-5px); }}
|
||
}}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
console.log('🐾 Team Builder initialized successfully!');
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
# Generate storage pets HTML first
|
||
storage_pets_html = ""
|
||
for pet in inactive_pets:
|
||
storage_pets_html += make_pet_card(pet, False)
|
||
|
||
# Generate active pets HTML for team slots
|
||
active_pets_html = ""
|
||
for pet in active_pets:
|
||
if pet.get('team_order'):
|
||
active_pets_html += make_pet_card(pet, True)
|
||
|
||
# Create content using string concatenation instead of f-strings to avoid CSS brace issues
|
||
team_builder_content = """
|
||
<style>
|
||
/* Team Builder Specific CSS */
|
||
.team-builder-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.header h1 {
|
||
color: var(--text-accent);
|
||
font-size: 2.5em;
|
||
margin: 0;
|
||
}
|
||
|
||
.team-sections {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.section h2 {
|
||
color: var(--text-accent);
|
||
border-bottom: 2px solid var(--accent-blue);
|
||
padding-bottom: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.team-slots-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
grid-template-rows: repeat(2, 1fr);
|
||
gap: 15px;
|
||
margin-bottom: 30px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.team-slot {
|
||
background: var(--bg-secondary);
|
||
border: 2px dashed var(--border-color);
|
||
border-radius: 12px;
|
||
min-height: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
text-align: center;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.team-slot:hover {
|
||
border-color: var(--accent-blue);
|
||
background: var(--hover-color);
|
||
}
|
||
|
||
.team-slot.drag-over {
|
||
border-color: var(--text-accent);
|
||
background: var(--hover-color);
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.slot-header {
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-weight: bold;
|
||
color: var(--text-accent);
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.slot-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.empty-slot {
|
||
color: var(--text-secondary);
|
||
font-style: italic;
|
||
text-align: center;
|
||
}
|
||
|
||
.storage-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pet-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 12px;
|
||
padding: 15px;
|
||
cursor: move;
|
||
user-select: none;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.pet-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
border-color: var(--accent-blue);
|
||
}
|
||
|
||
.pet-card.active {
|
||
border-color: var(--text-accent);
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.pet-card.storage {
|
||
border-color: var(--border-color);
|
||
}
|
||
|
||
.pet-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.pet-name {
|
||
color: var(--text-primary);
|
||
margin: 0;
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.status-badge {
|
||
background: var(--accent-blue);
|
||
color: white;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 0.8em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.pet-card.active .status-badge {
|
||
background: var(--text-accent);
|
||
}
|
||
|
||
.pet-species, .pet-type {
|
||
color: var(--text-secondary);
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.hp-section {
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.hp-label {
|
||
font-size: 0.9em;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.hp-bar {
|
||
background: var(--bg-primary);
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.hp-fill {
|
||
height: 100%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.7em;
|
||
color: var(--text-secondary);
|
||
display: block;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: bold;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.controls {
|
||
text-align: center;
|
||
margin: 30px 0;
|
||
}
|
||
|
||
.save-btn {
|
||
background: var(--text-accent);
|
||
color: var(--bg-primary);
|
||
border: none;
|
||
padding: 12px 30px;
|
||
border-radius: 8px;
|
||
font-size: 1.1em;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.save-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(102, 255, 102, 0.3);
|
||
}
|
||
|
||
.save-btn:disabled {
|
||
background: var(--text-secondary);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.back-btn {
|
||
background: var(--accent-blue);
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(74, 171, 247, 0.3);
|
||
}
|
||
|
||
.pin-section {
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
margin: 20px 0;
|
||
text-align: center;
|
||
display: none;
|
||
}
|
||
|
||
.pin-input {
|
||
background: var(--bg-tertiary);
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
font-size: 1.5em;
|
||
color: var(--text-primary);
|
||
text-align: center;
|
||
width: 200px;
|
||
margin: 15px;
|
||
}
|
||
|
||
.verify-btn {
|
||
background: var(--success-color);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 25px;
|
||
border-radius: 8px;
|
||
font-size: 1.1em;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.verify-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(81, 207, 102, 0.3);
|
||
}
|
||
|
||
.message {
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.message.success {
|
||
background: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.message.error {
|
||
background: var(--error-color);
|
||
color: white;
|
||
}
|
||
|
||
.drag-hint {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: rgba(0,0,0,0.8);
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
z-index: 9999;
|
||
max-width: 250px;
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0%, 20%, 60%, 100% { transform: translateY(0); }
|
||
40% { transform: translateY(-10px); }
|
||
80% { transform: translateY(-5px); }
|
||
}
|
||
</style>
|
||
|
||
<div class="team-builder-container">
|
||
<div class="header">
|
||
<h1>🐾 Team Builder</h1>
|
||
<p>Drag pets between Active Team and Storage. Double-click as backup.</p>
|
||
</div>
|
||
|
||
<div class="team-sections">
|
||
<div class="section">
|
||
<h2>⚔️ Active Team (1-6 pets)</h2>
|
||
<div class="team-slots-container" id="team-slots-container">
|
||
<div class="team-slot" id="slot-1">
|
||
<div class="slot-header">Slot 1 (Leader)</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-2">
|
||
<div class="slot-header">Slot 2</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-3">
|
||
<div class="slot-header">Slot 3</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-4">
|
||
<div class="slot-header">Slot 4</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-5">
|
||
<div class="slot-header">Slot 5</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
<div class="team-slot" id="slot-6">
|
||
<div class="slot-header">Slot 6</div>
|
||
<div class="slot-content">
|
||
<div class="empty-slot">Drop pet here</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📦 Storage</h2>
|
||
<div class="storage-container" id="storage-container">
|
||
""" + storage_pets_html + active_pets_html + """
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="save-btn" id="save-btn" onclick="saveTeam()">🔒 Save Team Changes</button>
|
||
<a href="/player/""" + nickname + """" class="back-btn">← Back to Profile</a>
|
||
<div style="margin-top: 15px; color: var(--text-secondary); font-size: 0.9em;">
|
||
Changes are saved securely with PIN verification via IRC
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pin-section" id="pin-section">
|
||
<h3>🔐 PIN Verification Required</h3>
|
||
<p>A 6-digit PIN has been sent to you via IRC private message.</p>
|
||
<p>Enter the PIN below to confirm your team changes:</p>
|
||
<input type="text" class="pin-input" id="pin-input" placeholder="000000" maxlength="6">
|
||
<button class="verify-btn" onclick="verifyPin()">✅ Verify & Apply Changes</button>
|
||
<div id="message-area"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drag-hint">
|
||
💡 <strong>How to use:</strong><br>
|
||
• Drag pets to team slots<br>
|
||
• Double-click to move pets<br>
|
||
• Empty slots show placeholders
|
||
</div>
|
||
|
||
<script>
|
||
let originalTeam = {};
|
||
let currentTeam = {};
|
||
let draggedElement = null;
|
||
|
||
// Initialize when DOM is ready
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('Team Builder: DOM loaded, initializing...');
|
||
initializeTeamBuilder();
|
||
});
|
||
|
||
function initializeTeamBuilder() {
|
||
console.log('Team Builder: Starting initialization...');
|
||
|
||
// Initialize team state
|
||
const allCards = document.querySelectorAll('.pet-card');
|
||
console.log(`Found ${allCards.length} pet cards`);
|
||
|
||
allCards.forEach(card => {
|
||
const petId = card.dataset.petId;
|
||
const isActive = card.dataset.active === 'true';
|
||
const teamOrder = card.dataset.teamOrder;
|
||
|
||
if (isActive && teamOrder) {
|
||
originalTeam[petId] = parseInt(teamOrder);
|
||
currentTeam[petId] = parseInt(teamOrder);
|
||
|
||
// Move to correct slot
|
||
const slot = document.getElementById(`slot-${teamOrder}`);
|
||
if (slot) {
|
||
const slotContent = slot.querySelector('.slot-content');
|
||
slotContent.innerHTML = '';
|
||
slotContent.appendChild(card);
|
||
}
|
||
} else {
|
||
originalTeam[petId] = false;
|
||
currentTeam[petId] = false;
|
||
}
|
||
});
|
||
|
||
console.log('Team state initialized:', originalTeam);
|
||
|
||
// Initialize drag and drop
|
||
initializeDragAndDrop();
|
||
|
||
// Initialize double-click backup
|
||
initializeDoubleClick();
|
||
|
||
// Update save button
|
||
updateSaveButton();
|
||
|
||
console.log('Team Builder: Initialization complete');
|
||
}
|
||
|
||
function initializeDragAndDrop() {
|
||
console.log('Initializing drag and drop...');
|
||
|
||
// Make pet cards draggable
|
||
document.querySelectorAll('.pet-card').forEach(card => {
|
||
card.draggable = true;
|
||
|
||
card.addEventListener('dragstart', function(e) {
|
||
draggedElement = this;
|
||
this.style.opacity = '0.5';
|
||
console.log('Drag started:', this.dataset.petId);
|
||
});
|
||
|
||
card.addEventListener('dragend', function(e) {
|
||
this.style.opacity = '';
|
||
draggedElement = null;
|
||
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||
});
|
||
});
|
||
|
||
// Set up team slot drop zones
|
||
for (let i = 1; i <= 6; i++) {
|
||
const slot = document.getElementById(`slot-${i}`);
|
||
if (slot) {
|
||
slot.addEventListener('dragover', e => e.preventDefault());
|
||
slot.addEventListener('dragenter', function(e) {
|
||
e.preventDefault();
|
||
this.classList.add('drag-over');
|
||
});
|
||
slot.addEventListener('dragleave', function(e) {
|
||
if (!this.contains(e.relatedTarget)) {
|
||
this.classList.remove('drag-over');
|
||
}
|
||
});
|
||
slot.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
this.classList.remove('drag-over');
|
||
if (draggedElement) {
|
||
movePetToTeamSlot(draggedElement.dataset.petId, i);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Set up storage drop zone
|
||
const storageContainer = document.getElementById('storage-container');
|
||
if (storageContainer) {
|
||
storageContainer.addEventListener('dragover', e => e.preventDefault());
|
||
storageContainer.addEventListener('dragenter', function(e) {
|
||
e.preventDefault();
|
||
this.classList.add('drag-over');
|
||
});
|
||
storageContainer.addEventListener('dragleave', function(e) {
|
||
if (!this.contains(e.relatedTarget)) {
|
||
this.classList.remove('drag-over');
|
||
}
|
||
});
|
||
storageContainer.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
this.classList.remove('drag-over');
|
||
if (draggedElement) {
|
||
movePetToStorage(draggedElement.dataset.petId);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function initializeDoubleClick() {
|
||
document.querySelectorAll('.pet-card').forEach(card => {
|
||
card.addEventListener('dblclick', function() {
|
||
const petId = this.dataset.petId;
|
||
const currentPosition = currentTeam[petId];
|
||
|
||
if (currentPosition) {
|
||
// Move to storage
|
||
movePetToStorage(petId);
|
||
} else {
|
||
// Find first empty slot
|
||
for (let i = 1; i <= 6; i++) {
|
||
const slot = document.getElementById(`slot-${i}`);
|
||
const slotContent = slot.querySelector('.slot-content');
|
||
if (slotContent.children.length === 0 || slotContent.querySelector('.empty-slot')) {
|
||
movePetToTeamSlot(petId, i);
|
||
return;
|
||
}
|
||
}
|
||
console.log('No empty slots available');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function movePetToTeamSlot(petId, position) {
|
||
console.log(`Moving pet ${petId} to slot ${position}`);
|
||
const card = document.querySelector(`[data-pet-id="${petId}"]`);
|
||
const slot = document.getElementById(`slot-${position}`);
|
||
|
||
if (!card || !slot) return;
|
||
|
||
const slotContent = slot.querySelector('.slot-content');
|
||
|
||
// If slot is occupied, move existing pet to storage
|
||
const existingCard = slotContent.querySelector('.pet-card');
|
||
if (existingCard) {
|
||
movePetToStorage(existingCard.dataset.petId);
|
||
}
|
||
|
||
// Update state
|
||
currentTeam[petId] = position;
|
||
|
||
// Update card
|
||
card.classList.remove('storage');
|
||
card.classList.add('active');
|
||
card.dataset.active = 'true';
|
||
card.dataset.teamOrder = position;
|
||
card.querySelector('.status-badge').textContent = 'Active';
|
||
|
||
// Move to slot
|
||
slotContent.innerHTML = '';
|
||
slotContent.appendChild(card);
|
||
|
||
updateSaveButton();
|
||
}
|
||
|
||
function movePetToStorage(petId) {
|
||
console.log(`Moving pet ${petId} to storage`);
|
||
const card = document.querySelector(`[data-pet-id="${petId}"]`);
|
||
const storageContainer = document.getElementById('storage-container');
|
||
|
||
if (!card || !storageContainer) return;
|
||
|
||
// Update state
|
||
currentTeam[petId] = false;
|
||
|
||
// Update card
|
||
card.classList.remove('active');
|
||
card.classList.add('storage');
|
||
card.dataset.active = 'false';
|
||
card.dataset.teamOrder = '';
|
||
card.querySelector('.status-badge').textContent = 'Storage';
|
||
|
||
// Move to storage
|
||
storageContainer.appendChild(card);
|
||
|
||
updateSaveButton();
|
||
}
|
||
|
||
function updateSaveButton() {
|
||
const saveBtn = document.getElementById('save-btn');
|
||
const hasChanges = JSON.stringify(originalTeam) !== JSON.stringify(currentTeam);
|
||
saveBtn.disabled = !hasChanges;
|
||
saveBtn.textContent = hasChanges ? '🔒 Save Team Changes' : '✅ No Changes';
|
||
}
|
||
|
||
async function saveTeam() {
|
||
const teamData = {};
|
||
Object.entries(currentTeam).forEach(([petId, position]) => {
|
||
teamData[petId] = position;
|
||
});
|
||
|
||
try {
|
||
const response = await fetch('/teambuilder/""" + nickname + """/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(teamData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
document.getElementById('pin-section').style.display = 'block';
|
||
showMessage('PIN sent to IRC! Check your private messages.', 'success');
|
||
} else {
|
||
showMessage('Error: ' + result.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showMessage('Network error: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function verifyPin() {
|
||
const pin = document.getElementById('pin-input').value;
|
||
if (!pin || pin.length !== 6) {
|
||
showMessage('Please enter a 6-digit PIN', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/teambuilder/""" + nickname + """/verify', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pin: pin })
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showMessage('Team saved successfully!', 'success');
|
||
originalTeam = { ...currentTeam };
|
||
updateSaveButton();
|
||
document.getElementById('pin-section').style.display = 'none';
|
||
document.getElementById('pin-input').value = '';
|
||
|
||
// Celebration animation
|
||
document.querySelectorAll('.pet-card').forEach(card => {
|
||
card.style.animation = 'bounce 0.6s ease-in-out';
|
||
});
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.pet-card').forEach(card => {
|
||
card.style.animation = '';
|
||
});
|
||
}, 600);
|
||
} else {
|
||
showMessage('Verification failed: ' + result.error, 'error');
|
||
}
|
||
} catch (error) {
|
||
showMessage('Network error: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function showMessage(text, type) {
|
||
const messageArea = document.getElementById('message-area');
|
||
messageArea.innerHTML = `<div class="message ${type}">${text}</div>`;
|
||
|
||
if (type === 'success') {
|
||
setTimeout(() => {
|
||
messageArea.innerHTML = '';
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
// PIN input keyboard support
|
||
document.getElementById('pin-input').addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
verifyPin();
|
||
}
|
||
});
|
||
</script>
|
||
"""
|
||
|
||
# Get the unified template
|
||
html_content = self.get_page_template(f"Team Builder - {nickname}", team_builder_content, "")
|
||
|
||
self.send_response(200)
|
||
self.send_header('Content-type', 'text/html')
|
||
self.end_headers()
|
||
self.wfile.write(html_content.encode())
|
||
|
||
def handle_team_save(self, nickname):
|
||
"""Handle team save request and generate PIN"""
|
||
try:
|
||
# Get POST data
|
||
content_length = int(self.headers.get('Content-Length', 0))
|
||
if content_length == 0:
|
||
self.send_json_response({"success": False, "error": "No data provided"}, 400)
|
||
return
|
||
|
||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||
|
||
# Parse JSON data
|
||
import json
|
||
try:
|
||
team_data = json.loads(post_data)
|
||
except json.JSONDecodeError:
|
||
self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400)
|
||
return
|
||
|
||
# Run async operations
|
||
import asyncio
|
||
result = asyncio.run(self._handle_team_save_async(nickname, team_data))
|
||
|
||
if result["success"]:
|
||
self.send_json_response(result, 200)
|
||
else:
|
||
self.send_json_response(result, 400)
|
||
|
||
except Exception as e:
|
||
print(f"Error in handle_team_save: {e}")
|
||
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
|
||
|
||
async def _handle_team_save_async(self, nickname, team_data):
|
||
"""Async handler for team save"""
|
||
try:
|
||
# Get player
|
||
player = await self.database.get_player(nickname)
|
||
if not player:
|
||
return {"success": False, "error": "Player not found"}
|
||
|
||
# Validate team composition
|
||
validation = await self.database.validate_team_composition(player["id"], team_data)
|
||
if not validation["valid"]:
|
||
return {"success": False, "error": validation["error"]}
|
||
|
||
# Create pending team change with PIN
|
||
import json
|
||
result = await self.database.create_pending_team_change(
|
||
player["id"],
|
||
json.dumps(team_data)
|
||
)
|
||
|
||
if result["success"]:
|
||
# Send PIN via IRC
|
||
self.send_pin_via_irc(nickname, result["pin_code"])
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "PIN sent to your IRC private messages",
|
||
"expires_in_minutes": 10
|
||
}
|
||
else:
|
||
return result
|
||
|
||
except Exception as e:
|
||
print(f"Error in _handle_team_save_async: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def handle_team_verify(self, nickname):
|
||
"""Handle PIN verification and apply team changes"""
|
||
try:
|
||
# Get POST data
|
||
content_length = int(self.headers.get('Content-Length', 0))
|
||
if content_length == 0:
|
||
self.send_json_response({"success": False, "error": "No PIN provided"}, 400)
|
||
return
|
||
|
||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||
|
||
# Parse JSON data
|
||
import json
|
||
try:
|
||
data = json.loads(post_data)
|
||
pin_code = data.get("pin", "").strip()
|
||
except (json.JSONDecodeError, AttributeError):
|
||
self.send_json_response({"success": False, "error": "Invalid data format"}, 400)
|
||
return
|
||
|
||
if not pin_code:
|
||
self.send_json_response({"success": False, "error": "PIN code is required"}, 400)
|
||
return
|
||
|
||
# Run async operations
|
||
import asyncio
|
||
result = asyncio.run(self._handle_team_verify_async(nickname, pin_code))
|
||
|
||
if result["success"]:
|
||
self.send_json_response(result, 200)
|
||
else:
|
||
self.send_json_response(result, 400)
|
||
|
||
except Exception as e:
|
||
print(f"Error in handle_team_verify: {e}")
|
||
self.send_json_response({"success": False, "error": "Internal server error"}, 500)
|
||
|
||
async def _handle_team_verify_async(self, nickname, pin_code):
|
||
"""Async handler for PIN verification"""
|
||
try:
|
||
# Get player
|
||
player = await self.database.get_player(nickname)
|
||
if not player:
|
||
return {"success": False, "error": "Player not found"}
|
||
|
||
# Apply team changes with PIN verification
|
||
result = await self.database.apply_team_change(player["id"], pin_code)
|
||
|
||
if result["success"]:
|
||
return {
|
||
"success": True,
|
||
"message": f"Team changes applied successfully! {result['changes_applied']} pets updated.",
|
||
"changes_applied": result["changes_applied"]
|
||
}
|
||
else:
|
||
return result
|
||
|
||
except Exception as e:
|
||
print(f"Error in _handle_team_verify_async: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def send_pin_via_irc(self, nickname, pin_code):
|
||
"""Send PIN to player via IRC private message"""
|
||
print(f"🔐 PIN for {nickname}: {pin_code}")
|
||
|
||
# Try to send via IRC bot if available
|
||
if self.bot and hasattr(self.bot, 'send_message'):
|
||
try:
|
||
# Send PIN via private message
|
||
self.bot.send_message(nickname, f"🔐 Team Builder PIN: {pin_code}")
|
||
self.bot.send_message(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}")
|
||
|
||
|
||
class PetBotWebServer:
|
||
"""Standalone web server for PetBot"""
|
||
|
||
def __init__(self, database=None, port=8080, bot=None):
|
||
self.database = database or Database()
|
||
self.port = port
|
||
self.bot = bot
|
||
self.server = None
|
||
|
||
def run(self):
|
||
"""Start the web server"""
|
||
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
|
||
self.server.database = self.database
|
||
self.server.bot = self.bot
|
||
|
||
print(f'🌐 Starting PetBot web server on http://0.0.0.0:{self.port}')
|
||
print(f'📡 Accessible from WSL at: http://172.27.217.61:{self.port}')
|
||
print(f'📡 Accessible from Windows at: http://localhost:{self.port}')
|
||
print('')
|
||
print('🌐 Public access at: http://petz.rdx4.com/')
|
||
print('')
|
||
|
||
self.server.serve_forever()
|
||
|
||
def start_in_thread(self):
|
||
"""Start the web server in a background thread"""
|
||
import threading
|
||
self.thread = threading.Thread(target=self.run, daemon=True)
|
||
self.thread.start()
|
||
|
||
def stop(self):
|
||
"""Stop the web server"""
|
||
if self.server:
|
||
self.server.shutdown()
|
||
self.server.server_close()
|
||
|
||
def run_standalone():
|
||
"""Run the web server in standalone mode"""
|
||
import sys
|
||
|
||
port = 8080
|
||
if len(sys.argv) > 1:
|
||
try:
|
||
port = int(sys.argv[1])
|
||
except ValueError:
|
||
print('Usage: python webserver.py [port]')
|
||
sys.exit(1)
|
||
|
||
server = PetBotWebServer(port)
|
||
|
||
print('🌐 PetBot Web Server')
|
||
print('=' * 50)
|
||
print(f'Port: {port}')
|
||
print('')
|
||
print('🔗 Local URLs:')
|
||
print(f' http://localhost:{port}/ - Game Hub (local)')
|
||
print(f' http://localhost:{port}/help - Command Help (local)')
|
||
print(f' http://localhost:{port}/players - Player List (local)')
|
||
print(f' http://localhost:{port}/leaderboard - Leaderboard (local)')
|
||
print(f' http://localhost:{port}/locations - Locations (local)')
|
||
print('')
|
||
print('🌐 Public URLs:')
|
||
print(' http://petz.rdx4.com/ - Game Hub')
|
||
print(' http://petz.rdx4.com/help - Command Help')
|
||
print(' http://petz.rdx4.com/players - Player List')
|
||
print(' http://petz.rdx4.com/leaderboard - Leaderboard')
|
||
print(' http://petz.rdx4.com/locations - Locations')
|
||
print('')
|
||
print('Press Ctrl+C to stop')
|
||
|
||
try:
|
||
server.run()
|
||
except KeyboardInterrupt:
|
||
print('\n✅ Web server stopped')
|
||
|
||
if __name__ == '__main__':
|
||
run_standalone()
|
||
|