Added normalize_input() function to BaseModule for consistent lowercase conversion of user input. Updated all command modules to use normalization for commands, arguments, pet names, location names, gym names, and item names. Players can now use any capitalization for commands and arguments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
3300 lines
No EOL
121 KiB
Python
3300 lines
No EOL
121 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
|
|
|
|
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(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"""
|
|
|
|
# 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>"""
|
|
|
|
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);
|
|
}}
|
|
|
|
.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: 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>
|
|
|
|
<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>
|
|
</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': bool(row[13]), # Convert to proper boolean
|
|
'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']]
|
|
|
|
# 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}")
|
|
|
|
# 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;
|
|
}}
|
|
|
|
.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="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 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 isActive = currentTeam[petId];
|
|
|
|
console.log(`Double-click: Moving pet ${{petId}} from ${{isActive ? 'active' : 'storage'}} to ${{isActive ? 'storage' : 'active'}}`);
|
|
|
|
if (isActive) {{
|
|
movePetToStorage(petId);
|
|
}} else {{
|
|
movePetToActive(petId);
|
|
}}
|
|
}});
|
|
|
|
// 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 activeContainer = document.getElementById('active-container');
|
|
const storageContainer = document.getElementById('storage-container');
|
|
const activeDrop = document.getElementById('active-drop');
|
|
const storageDrop = document.getElementById('storage-drop');
|
|
|
|
// 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(`Active container has ${{activeContainer.children.length}} pets initially`);
|
|
console.log(`Storage container has ${{storageContainer.children.length}} pets initially`);
|
|
|
|
allCards.forEach((card, index) => {{
|
|
const petId = card.dataset.petId;
|
|
const isActive = card.dataset.active === 'true';
|
|
const currentContainer = card.parentElement.id;
|
|
|
|
console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, currentContainer=${{currentContainer}}`);
|
|
|
|
originalTeam[petId] = isActive;
|
|
currentTeam[petId] = isActive;
|
|
|
|
// CRITICAL: Verify container placement is correct - DO NOT MOVE unless absolutely necessary
|
|
const expectedContainer = isActive ? activeContainer : storageContainer;
|
|
const expectedContainerId = isActive ? 'active-container' : 'storage-container';
|
|
|
|
if (currentContainer !== expectedContainerId) {{
|
|
console.error(`MISMATCH! Pet ${{petId}} is in ${{currentContainer}} but should be in ${{expectedContainerId}} based on data-active=${{card.dataset.active}}`);
|
|
console.log(`Moving pet ${{petId}} to correct container...`);
|
|
expectedContainer.appendChild(card);
|
|
}} else {{
|
|
console.log(`Pet ${{petId}} correctly placed in ${{currentContainer}}`);
|
|
}}
|
|
}});
|
|
|
|
console.log('After initialization:');
|
|
console.log(`Active container now has ${{activeContainer.children.length}} pets`);
|
|
console.log(`Storage container now has ${{storageContainer.children.length}} pets`);
|
|
|
|
console.log('Original team state:', originalTeam);
|
|
console.log('Current team state:', currentTeam);
|
|
|
|
// 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';
|
|
|
|
// 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 drop zones (using previously declared variables)
|
|
|
|
[activeContainer, activeDrop].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: 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();
|
|
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 movePetToActive(petId) {{
|
|
console.log(`movePetToActive called for pet ${{petId}}`);
|
|
const card = document.querySelector(`[data-pet-id="${{petId}}"]`);
|
|
if (!card) {{
|
|
console.log(`No card found for pet ${{petId}}`);
|
|
return;
|
|
}}
|
|
|
|
const currentIsActive = currentTeam[petId];
|
|
|
|
console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`);
|
|
|
|
if (!currentIsActive) {{
|
|
console.log(`Moving pet ${{petId}} to active...`);
|
|
|
|
// 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');
|
|
}} else {{
|
|
console.log(`Pet ${{petId}} is already active, no move needed`);
|
|
}}
|
|
}}
|
|
|
|
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 currentIsActive = currentTeam[petId];
|
|
|
|
console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`);
|
|
|
|
if (currentIsActive) {{
|
|
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.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() {{
|
|
// Using previously declared container variables
|
|
|
|
// CRITICAL: Only update visual indicators, never move pets
|
|
// Use CSS classes instead of direct style manipulation
|
|
if (activeContainer && activeContainer.children.length > 0) {{
|
|
if (activeDrop) activeDrop.classList.add('has-pets');
|
|
}} else {{
|
|
if (activeDrop) activeDrop.classList.remove('has-pets');
|
|
}}
|
|
|
|
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:', {{
|
|
activeContainerPets: activeContainer ? activeContainer.children.length : 0,
|
|
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>"""
|
|
|
|
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"""
|
|
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
|
|
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_message'):
|
|
# Send directly via bot's send_message method (non-async)
|
|
message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)"
|
|
bot.send_message(nickname, message)
|
|
print(f"✅ PIN sent to {nickname} via IRC")
|
|
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() |