🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2945 lines
No EOL
105 KiB
Python
2945 lines
No EOL
105 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"""
|
|
|
|
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"""
|
|
html = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot Game Hub</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 3em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.nav-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.nav-card {
|
|
background: var(--bg-secondary);
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.nav-card:hover {
|
|
transform: translateY(-5px);
|
|
color: var(--text-accent);
|
|
}
|
|
|
|
.nav-card h3 {
|
|
margin: 0 0 15px 0;
|
|
color: var(--text-accent);
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.status {
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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="nav-grid">
|
|
<a href="/help" class="nav-card">
|
|
<h3>📚 Command Help</h3>
|
|
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
|
|
</a>
|
|
|
|
<a href="/players" class="nav-card">
|
|
<h3>👥 Player List</h3>
|
|
<p>View all registered players and their basic stats</p>
|
|
</a>
|
|
|
|
<a href="/leaderboard" class="nav-card">
|
|
<h3>🏆 Leaderboard</h3>
|
|
<p>Top players by level, pets caught, and achievements earned</p>
|
|
</a>
|
|
|
|
<a href="/locations" class="nav-card">
|
|
<h3>🗺️ Locations</h3>
|
|
<p>Explore all game locations and see what pets can be found where</p>
|
|
</a>
|
|
|
|
<a href="/petdex" class="nav-card">
|
|
<h3>📖 Petdex</h3>
|
|
<p>Complete encyclopedia of all available pets with stats, types, and evolution info</p>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="status">
|
|
<p><strong>🤖 Bot Status:</strong> Online and ready for commands!</p>
|
|
<p>Use <code>!help</code> in #petz for quick command reference</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
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"""
|
|
try:
|
|
with open('help.html', 'r') as f:
|
|
content = f.read()
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(content.encode())
|
|
except FileNotFoundError:
|
|
self.send_error(404, "Help file not found")
|
|
|
|
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"""
|
|
|
|
# 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>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Players</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.players-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.players-table th, .players-table td {{
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.players-table th {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
font-weight: 600;
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.players-table tr:hover {{
|
|
background: var(--hover-color);
|
|
}}
|
|
|
|
.stats-summary {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-tertiary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>👥 Registered Players</h1>
|
|
<p>All trainers on their pet collection journey</p>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">📊 Server Statistics</div>
|
|
<div class="section-content">
|
|
<div class="stats-summary">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len(players_data)}</div>
|
|
<div class="stat-label">Total Players</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['pet_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Pets Caught</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['achievement_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Achievements</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{max((p['level'] for p in players_data), default=0)}</div>
|
|
<div class="stat-label">Highest Level</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🏆 Player Rankings</div>
|
|
<div class="section-content">
|
|
<table class="players-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>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
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"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<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>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.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(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"""
|
|
|
|
# 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
|
|
spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else []
|
|
spawn_badges = ""
|
|
for spawn in spawn_list[:6]: # Limit to first 6 for display
|
|
spawn_badges += f'<span class="spawn-badge">{spawn.strip()}</span>'
|
|
if len(spawn_list) > 6:
|
|
spawn_badges += f'<span class="spawn-badge">+{len(spawn_list) - 6} 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>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Locations</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.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: var(--shadow-dark);
|
|
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-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);
|
|
}}
|
|
|
|
.info-section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.info-section h2 {{
|
|
color: var(--text-accent);
|
|
margin-top: 0;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<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>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.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
|
|
cursor = await db.execute("""
|
|
SELECT 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"}
|
|
|
|
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>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Petdex</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.stats-summary {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
|
|
.rarity-section {{
|
|
margin-bottom: 40px;
|
|
}}
|
|
|
|
.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: var(--shadow-dark);
|
|
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;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
|
|
.pet-header h3 {{
|
|
margin: 0;
|
|
font-size: 1.2em;
|
|
}}
|
|
|
|
.type-badge {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
padding: 4px 12px;
|
|
border-radius: 15px;
|
|
font-size: 0.8em;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.pet-stats {{
|
|
padding: 15px;
|
|
background: var(--bg-tertiary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-row {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.total-stats {{
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
}}
|
|
|
|
.pet-info {{
|
|
padding: 15px;
|
|
font-size: 0.9em;
|
|
line-height: 1.4;
|
|
}}
|
|
|
|
.search-filter {{
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>📖 Petdex - Complete Pet Encyclopedia</h1>
|
|
<p>Comprehensive guide to all available pet species</p>
|
|
</div>
|
|
|
|
<div class="search-filter">
|
|
<div class="stats-summary">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{total_species}</div>
|
|
<div class="stat-label">Total Species</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['type1'] == 'Fire' or p['type2'] == 'Fire'])}</div>
|
|
<div class="stat-label">Fire Types</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['type1'] == 'Water' or p['type2'] == 'Water'])}</div>
|
|
<div class="stat-label">Water Types</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['evolution_level']])}</div>
|
|
<div class="stat-label">Can Evolve</div>
|
|
</div>
|
|
</div>
|
|
<p style="color: var(--text-secondary); margin: 0;">🎯 Pets are organized by rarity. Use <code>!wild <location></code> in #petz to see what spawns where!</p>
|
|
</div>
|
|
|
|
{petdex_html}
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
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': row[13],
|
|
'species_name': row[14], 'type1': row[15], 'type2': row[16]
|
|
}
|
|
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"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Player Not Found</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<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>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(404)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_error(self, nickname, error_msg):
|
|
"""Serve player error page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<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>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.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 style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid var(--text-accent);">
|
|
<strong>🏆 {achievement['achievement_name']}</strong><br>
|
|
<small>{achievement['achievement_desc']}</small><br>
|
|
<em style="color: var(--text-secondary);">Earned: {achievement['completed_at']}</em>
|
|
</div>"""
|
|
else:
|
|
achievements_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No achievements yet. Keep exploring and catching pets to earn achievements!
|
|
</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 style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid {color};">
|
|
<strong style="color: {color};">{symbol} {item['name']}{quantity_str}</strong><br>
|
|
<small>{item['description']}</small><br>
|
|
<em style="color: var(--text-secondary);">Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}</em>
|
|
</div>"""
|
|
else:
|
|
inventory_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No items yet. Try exploring to find useful items!
|
|
</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 style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid gold;">
|
|
<strong>{badge['badge_icon']} {badge['badge_name']}</strong><br>
|
|
<small>Earned from {badge['gym_name']} ({badge['location_name']})</small><br>
|
|
<em style="color: var(--text-secondary);">First victory: {badge_date} | Total victories: {badge['victories']} | Highest difficulty: Level {badge['highest_difficulty']}</em>
|
|
</div>"""
|
|
else:
|
|
badges_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No gym badges yet. Challenge gyms to earn badges and prove your training skills!
|
|
</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 style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid {rarity_color};">
|
|
<strong style="color: {rarity_color};">{encounter['species_name']}</strong> <span class="type-badge">{type_str}</span><br>
|
|
<small>Encountered {encounter['total_encounters']} times | Caught {encounter['caught_count']} times</small><br>
|
|
<em style="color: var(--text-secondary);">First seen: {encounter_date}</em>
|
|
</div>"""
|
|
else:
|
|
encounters_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No pets encountered yet. Use !explore to discover wild pets!
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - {nickname}'s Profile</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
margin-bottom: 30px;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.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);
|
|
}}
|
|
|
|
.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 {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<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}" style="background: linear-gradient(135deg, #4CAF50, #45a049); color: white; padding: 12px 24px; border-radius: 20px; text-decoration: none; display: inline-block; font-weight: bold; transition: all 0.3s ease;">
|
|
🔧 Team Builder
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<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">
|
|
<div class="section-header">🐾 Pet Collection</div>
|
|
<div class="section-content">
|
|
<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 class="section">
|
|
<div class="section-header">🏆 Achievements</div>
|
|
<div class="section-content">
|
|
{achievements_html}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🎒 Inventory</div>
|
|
<div class="section-content">
|
|
{inventory_html}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🏆 Gym Badges</div>
|
|
<div class="section-content">
|
|
{badges_html}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">👁️ Pet Encounters</div>
|
|
<div class="section-content">
|
|
<div style="margin-bottom: 20px; text-align: center; color: var(--text-secondary);">
|
|
<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>
|
|
{encounters_html}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.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"""
|
|
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>
|
|
body {{ font-family: Arial, sans-serif; background: #0f0f23; color: #cccccc; text-align: center; padding: 50px; }}
|
|
.error {{ background: #2a2a4a; padding: 30px; border-radius: 10px; margin: 20px auto; max-width: 500px; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error">
|
|
<h2>🐾 No Pets Found</h2>
|
|
<p>{nickname}, you need to catch some pets before using the team builder!</p>
|
|
<p><a href="/player/{nickname}" style="color: #66ff66;">← Back to Profile</a></p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.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']]
|
|
|
|
# Generate detailed pet cards
|
|
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']}"
|
|
|
|
# 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()}">
|
|
<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>"""
|
|
|
|
active_cards = ''.join(make_pet_card(pet, True) for pet in active_pets)
|
|
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;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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="pets-container" id="active-container">
|
|
{active_cards}
|
|
</div>
|
|
<div class="drop-zone {('has-pets' if active_pets else '')}" id="active-drop">
|
|
Drop pets here to add to your active team
|
|
</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 if drag events are working
|
|
if (petCards.length > 0) {{
|
|
const testCard = petCards[0];
|
|
console.log('Testing drag events on first card...');
|
|
|
|
// Simulate drag start
|
|
const dragEvent = new DragEvent('dragstart', {{ bubbles: true }});
|
|
testCard.dispatchEvent(dragEvent);
|
|
console.log('Drag event dispatched');
|
|
}}
|
|
}}
|
|
|
|
// Initialize team state
|
|
document.querySelectorAll('.pet-card').forEach(card => {{
|
|
const petId = card.dataset.petId;
|
|
const isActive = card.dataset.active === 'true';
|
|
originalTeam[petId] = isActive;
|
|
currentTeam[petId] = isActive;
|
|
}});
|
|
|
|
// Completely rewritten drag and drop - simpler approach
|
|
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';
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', this.dataset.petId);
|
|
}});
|
|
|
|
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 drop zones
|
|
const activeContainer = document.getElementById('active-container');
|
|
const storageContainer = document.getElementById('storage-container');
|
|
const activeDrop = document.getElementById('active-drop');
|
|
const storageDrop = document.getElementById('storage-drop');
|
|
|
|
[activeContainer, activeDrop].forEach(zone => {{
|
|
if (zone) {{
|
|
zone.addEventListener('dragover', function(e) {{
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
}});
|
|
|
|
zone.addEventListener('dragenter', function(e) {{
|
|
e.preventDefault();
|
|
this.classList.add('drag-over');
|
|
console.log('DRAGENTER: Active 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: Active zone');
|
|
this.classList.remove('drag-over');
|
|
|
|
if (draggedElement) {{
|
|
const petId = draggedElement.dataset.petId;
|
|
console.log('Moving pet', petId, 'to active');
|
|
movePetToActive(petId);
|
|
}}
|
|
}});
|
|
}}
|
|
}});
|
|
|
|
[storageContainer, storageDrop].forEach(zone => {{
|
|
if (zone) {{
|
|
zone.addEventListener('dragover', function(e) {{
|
|
e.preventDefault();
|
|
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 movePetToActive(petId) {{
|
|
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
|
|
if (!card) return;
|
|
|
|
const activeContainer = document.getElementById('active-container');
|
|
const currentIsActive = currentTeam[petId];
|
|
|
|
if (!currentIsActive) {{
|
|
// Update state
|
|
currentTeam[petId] = true;
|
|
|
|
// Move DOM element
|
|
card.classList.remove('storage');
|
|
card.classList.add('active');
|
|
card.dataset.active = 'true';
|
|
card.querySelector('.status-badge').textContent = 'Active';
|
|
activeContainer.appendChild(card);
|
|
|
|
// Update interface
|
|
updateSaveButton();
|
|
updateDropZoneVisibility();
|
|
|
|
console.log('Pet moved to active successfully');
|
|
}}
|
|
}}
|
|
|
|
function movePetToStorage(petId) {{
|
|
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
|
|
if (!card) return;
|
|
|
|
const storageContainer = document.getElementById('storage-container');
|
|
const currentIsActive = currentTeam[petId];
|
|
|
|
if (currentIsActive) {{
|
|
// Update state
|
|
currentTeam[petId] = false;
|
|
|
|
// Move DOM element
|
|
card.classList.remove('active');
|
|
card.classList.add('storage');
|
|
card.dataset.active = 'false';
|
|
card.querySelector('.status-badge').textContent = 'Storage';
|
|
storageContainer.appendChild(card);
|
|
|
|
// Update interface
|
|
updateSaveButton();
|
|
updateDropZoneVisibility();
|
|
|
|
console.log('Pet moved to storage successfully');
|
|
}}
|
|
}}
|
|
|
|
|
|
function updateDropZoneVisibility() {{
|
|
const activeContainer = document.getElementById('active-container');
|
|
const storageContainer = document.getElementById('storage-container');
|
|
const activeDrop = document.getElementById('active-drop');
|
|
const storageDrop = document.getElementById('storage-drop');
|
|
|
|
activeDrop.style.display = activeContainer.children.length > 0 ? 'none' : 'flex';
|
|
storageDrop.style.display = storageContainer.children.length > 0 ? 'none' : 'flex';
|
|
}}
|
|
|
|
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
|
|
initializeDragAndDrop();
|
|
updateSaveButton();
|
|
updateDropZoneVisibility();
|
|
|
|
// Run test to verify everything is working
|
|
setTimeout(() => {{
|
|
runDragDropTest();
|
|
}}, 500);
|
|
|
|
// Add test button for manual debugging
|
|
const testButton = document.createElement('button');
|
|
testButton.textContent = '🧪 Test Drag & Drop';
|
|
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 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>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def handle_team_save(self, nickname):
|
|
"""Handle team save request and generate PIN"""
|
|
self.send_json_response({"success": False, "error": "Team save not fully implemented yet"}, 501)
|
|
|
|
def handle_team_verify(self, nickname):
|
|
"""Handle PIN verification and apply team changes"""
|
|
self.send_json_response({"success": False, "error": "PIN verification not fully implemented yet"}, 501)
|
|
|
|
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
|
|
try:
|
|
# Check if the bot instance is accessible via global state
|
|
import sys
|
|
if hasattr(sys.modules.get('__main__'), 'bot_instance'):
|
|
bot = sys.modules['__main__'].bot_instance
|
|
if hasattr(bot, 'send_team_builder_pin'):
|
|
# Use asyncio to run the async method
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
loop.run_until_complete(bot.send_team_builder_pin(nickname, pin_code))
|
|
loop.close()
|
|
return
|
|
except Exception as e:
|
|
print(f"Could not send PIN via IRC bot: {e}")
|
|
|
|
# Fallback: just print to console for now
|
|
print(f"⚠️ IRC bot not available - PIN displayed in console only")
|
|
|
|
def send_json_response(self, data, status_code=200):
|
|
"""Send JSON response"""
|
|
import json
|
|
response = json.dumps(data)
|
|
|
|
self.send_response(status_code)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(response.encode())
|
|
|
|
class PetBotWebServer:
|
|
def __init__(self, database, port=8080):
|
|
self.database = database
|
|
self.port = port
|
|
self.server = None
|
|
|
|
def run(self):
|
|
"""Start the HTTP web server"""
|
|
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}")
|
|
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
|
|
# Pass database to the server so handlers can access it
|
|
self.server.database = self.database
|
|
self.server.serve_forever()
|
|
|
|
def start_in_thread(self):
|
|
"""Start the web server in a background thread"""
|
|
thread = Thread(target=self.run, daemon=True)
|
|
thread.start()
|
|
print(f"✅ Web server started at http://localhost:{self.port}")
|
|
print(f"🌐 Public access at: http://petz.rdx4.com/")
|
|
return thread
|
|
|
|
def run_standalone():
|
|
"""Run the web server standalone for testing"""
|
|
print("🐾 PetBot Web Server (Standalone Mode)")
|
|
print("=" * 40)
|
|
|
|
# Initialize database
|
|
database = Database()
|
|
# Note: In standalone mode, we can't easily run async init
|
|
# This is mainly for testing the web routes
|
|
|
|
# Start web server
|
|
server = PetBotWebServer(database)
|
|
print("🚀 Starting web server...")
|
|
print("📝 Available routes:")
|
|
print(" http://localhost:8080/ - Game Hub (local)")
|
|
print(" http://localhost:8080/help - Command Help (local)")
|
|
print(" http://localhost:8080/players - Player List (local)")
|
|
print(" http://localhost:8080/leaderboard - Leaderboard (local)")
|
|
print(" http://localhost:8080/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() |