- 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()
|
|
|