Bug Fix: - Fixed 'int' object has no attribute 'split' error when viewing player profiles - Issue was incorrect column mapping in encounter data SQL query - species_id (integer) was being treated as first_encounter_date (string) Technical Changes: - Use existing database.get_player_encounters() method with proper row factory - Use existing database.get_encounter_stats() method for consistency - Added robust error handling for date formatting in both encounters and gym badges - Added try/catch blocks to prevent profile crashes from data issues Data Safety: - Added isinstance() checks before calling .split() on date strings - Graceful fallback to 'Unknown' for malformed dates - Error handling ensures other users won't experience crashes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2011 lines
No EOL
71 KiB
Python
2011 lines
No EOL
71 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PetBot Web Server
|
|
Provides web interface for bot data including help, player stats, and pet collections
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import asyncio
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
from threading import Thread
|
|
import time
|
|
|
|
# Add the project directory to the path
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from src.database import Database
|
|
|
|
class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|
"""HTTP request handler for PetBot web server"""
|
|
|
|
def do_GET(self):
|
|
"""Handle GET requests"""
|
|
parsed_path = urlparse(self.path)
|
|
path = parsed_path.path
|
|
|
|
# Route handling
|
|
if path == '/':
|
|
self.serve_index()
|
|
elif path == '/help':
|
|
self.serve_help()
|
|
elif path == '/players':
|
|
self.serve_players()
|
|
elif path.startswith('/player/'):
|
|
nickname = path[8:] # Remove '/player/' prefix
|
|
self.serve_player_profile(nickname)
|
|
elif path == '/leaderboard':
|
|
self.serve_leaderboard()
|
|
elif path == '/locations':
|
|
self.serve_locations()
|
|
elif path == '/petdex':
|
|
self.serve_petdex()
|
|
else:
|
|
self.send_error(404, "Page not found")
|
|
|
|
def serve_index(self):
|
|
"""Serve the main index page"""
|
|
html = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot Game Hub</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 3em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.nav-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.nav-card {
|
|
background: var(--bg-secondary);
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.nav-card:hover {
|
|
transform: translateY(-5px);
|
|
color: var(--text-accent);
|
|
}
|
|
|
|
.nav-card h3 {
|
|
margin: 0 0 15px 0;
|
|
color: var(--text-accent);
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.status {
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🐾 PetBot Game Hub</h1>
|
|
<p>Welcome to the PetBot web interface!</p>
|
|
<p><em>Connect to irc.libera.chat #petz to play</em></p>
|
|
</div>
|
|
|
|
<div class="nav-grid">
|
|
<a href="/help" class="nav-card">
|
|
<h3>📚 Command Help</h3>
|
|
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
|
|
</a>
|
|
|
|
<a href="/players" class="nav-card">
|
|
<h3>👥 Player List</h3>
|
|
<p>View all registered players and their basic stats</p>
|
|
</a>
|
|
|
|
<a href="/leaderboard" class="nav-card">
|
|
<h3>🏆 Leaderboard</h3>
|
|
<p>Top players by level, pets caught, and achievements earned</p>
|
|
</a>
|
|
|
|
<a href="/locations" class="nav-card">
|
|
<h3>🗺️ Locations</h3>
|
|
<p>Explore all game locations and see what pets can be found where</p>
|
|
</a>
|
|
|
|
<a href="/petdex" class="nav-card">
|
|
<h3>📖 Petdex</h3>
|
|
<p>Complete encyclopedia of all available pets with stats, types, and evolution info</p>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="status">
|
|
<p><strong>🤖 Bot Status:</strong> Online and ready for commands!</p>
|
|
<p>Use <code>!help</code> in #petz for quick command reference</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_help(self):
|
|
"""Serve the help page"""
|
|
try:
|
|
with open('help.html', 'r') as f:
|
|
content = f.read()
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(content.encode())
|
|
except FileNotFoundError:
|
|
self.send_error(404, "Help file not found")
|
|
|
|
def serve_players(self):
|
|
"""Serve the players page with real data"""
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_error_page("Players", "Database not available")
|
|
return
|
|
|
|
# Fetch players data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
players_data = loop.run_until_complete(self.fetch_players_data(database))
|
|
loop.close()
|
|
|
|
self.serve_players_data(players_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching players data: {e}")
|
|
self.serve_error_page("Players", f"Error loading players: {str(e)}")
|
|
|
|
async def fetch_players_data(self, database):
|
|
"""Fetch all players data from database"""
|
|
try:
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get all players with basic stats
|
|
cursor = await db.execute("""
|
|
SELECT p.nickname, p.level, p.experience, p.money, p.created_at,
|
|
l.name as location_name,
|
|
(SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count,
|
|
(SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets,
|
|
(SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count
|
|
FROM players p
|
|
LEFT JOIN locations l ON p.current_location_id = l.id
|
|
ORDER BY p.level DESC, p.experience DESC
|
|
""")
|
|
|
|
rows = await cursor.fetchall()
|
|
# Convert SQLite rows to dictionaries properly
|
|
players = []
|
|
for row in rows:
|
|
player_dict = {
|
|
'nickname': row[0],
|
|
'level': row[1],
|
|
'experience': row[2],
|
|
'money': row[3],
|
|
'created_at': row[4],
|
|
'location_name': row[5],
|
|
'pet_count': row[6],
|
|
'active_pets': row[7],
|
|
'achievement_count': row[8]
|
|
}
|
|
players.append(player_dict)
|
|
return players
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching players: {e}")
|
|
return []
|
|
|
|
def serve_players_data(self, players_data):
|
|
"""Serve players page with real data"""
|
|
|
|
# Build players table HTML
|
|
if players_data:
|
|
players_html = ""
|
|
for i, player in enumerate(players_data, 1):
|
|
rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.")
|
|
|
|
players_html += f"""
|
|
<tr onclick="window.location.href='/player/{player['nickname']}'" style="cursor: pointer;">
|
|
<td><strong>{rank_emoji}</strong></td>
|
|
<td><a href="/player/{player['nickname']}" style="color: var(--text-accent); text-decoration: none;"><strong>{player['nickname']}</strong></a></td>
|
|
<td>{player['level']}</td>
|
|
<td>{player['experience']}</td>
|
|
<td>${player['money']}</td>
|
|
<td>{player['pet_count']}</td>
|
|
<td>{player['active_pets']}</td>
|
|
<td>{player['achievement_count']}</td>
|
|
<td>{player.get('location_name', 'Unknown')}</td>
|
|
</tr>"""
|
|
else:
|
|
players_html = """
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; padding: 40px;">
|
|
No players found. Be the first to use !start in #petz!
|
|
</td>
|
|
</tr>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Players</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.players-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.players-table th, .players-table td {{
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.players-table th {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
font-weight: 600;
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.players-table tr:hover {{
|
|
background: var(--hover-color);
|
|
}}
|
|
|
|
.stats-summary {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-tertiary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-value {{
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.stat-label {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>👥 Registered Players</h1>
|
|
<p>All trainers on their pet collection journey</p>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">📊 Server Statistics</div>
|
|
<div class="section-content">
|
|
<div class="stats-summary">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len(players_data)}</div>
|
|
<div class="stat-label">Total Players</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['pet_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Pets Caught</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['achievement_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Achievements</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{max((p['level'] for p in players_data), default=0)}</div>
|
|
<div class="stat-label">Highest Level</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🏆 Player Rankings</div>
|
|
<div class="section-content">
|
|
<table class="players-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Rank</th>
|
|
<th>Player</th>
|
|
<th>Level</th>
|
|
<th>Experience</th>
|
|
<th>Money</th>
|
|
<th>Pets</th>
|
|
<th>Active</th>
|
|
<th>Achievements</th>
|
|
<th>Location</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{players_html}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary);">
|
|
<p>💡 Click on any player name to view their detailed profile</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_error_page(self, page_name, error_msg):
|
|
"""Serve a generic error page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>⚠️ Error Loading {page_name}</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Unable to load page</h2>
|
|
<p>{error_msg}</p>
|
|
<p>Please try again later or contact an administrator.</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_leaderboard(self):
|
|
"""Serve the leaderboard page - redirect to players for now"""
|
|
# For now, leaderboard is the same as players page since they're ranked
|
|
# In the future, this could have different categories
|
|
self.send_response(302) # Temporary redirect
|
|
self.send_header('Location', '/players')
|
|
self.end_headers()
|
|
|
|
def serve_locations(self):
|
|
"""Serve the locations page with real data"""
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_error_page("Locations", "Database not available")
|
|
return
|
|
|
|
# Fetch locations data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
|
|
loop.close()
|
|
|
|
self.serve_locations_data(locations_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching locations data: {e}")
|
|
self.serve_error_page("Locations", f"Error loading locations: {str(e)}")
|
|
|
|
async def fetch_locations_data(self, database):
|
|
"""Fetch all locations and their spawn data from database"""
|
|
try:
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get all locations
|
|
cursor = await db.execute("""
|
|
SELECT l.*,
|
|
GROUP_CONCAT(ps.name || ' (' || ps.type1 ||
|
|
CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns
|
|
FROM locations l
|
|
LEFT JOIN location_spawns ls ON l.id = ls.location_id
|
|
LEFT JOIN pet_species ps ON ls.species_id = ps.id
|
|
GROUP BY l.id
|
|
ORDER BY l.id
|
|
""")
|
|
|
|
rows = await cursor.fetchall()
|
|
# Convert SQLite rows to dictionaries properly
|
|
locations = []
|
|
for row in rows:
|
|
location_dict = {
|
|
'id': row[0],
|
|
'name': row[1],
|
|
'description': row[2],
|
|
'level_min': row[3],
|
|
'level_max': row[4],
|
|
'spawns': row[5] if len(row) > 5 else None
|
|
}
|
|
locations.append(location_dict)
|
|
return locations
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching locations: {e}")
|
|
return []
|
|
|
|
def serve_locations_data(self, locations_data):
|
|
"""Serve locations page with real data"""
|
|
|
|
# Build locations HTML
|
|
locations_html = ""
|
|
if locations_data:
|
|
for location in locations_data:
|
|
spawns = location.get('spawns', 'No pets found')
|
|
if not spawns or spawns == 'None':
|
|
spawns = "No pets spawn here yet"
|
|
|
|
# Split spawns into a readable list
|
|
spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else []
|
|
spawn_badges = ""
|
|
for spawn in spawn_list[:6]: # Limit to first 6 for display
|
|
spawn_badges += f'<span class="spawn-badge">{spawn.strip()}</span>'
|
|
if len(spawn_list) > 6:
|
|
spawn_badges += f'<span class="spawn-badge">+{len(spawn_list) - 6} more</span>'
|
|
|
|
if not spawn_badges:
|
|
spawn_badges = '<em style="color: var(--text-secondary);">No pets spawn here yet</em>'
|
|
|
|
locations_html += f"""
|
|
<div class="location-card">
|
|
<div class="location-header">
|
|
<h3>🗺️ {location['name']}</h3>
|
|
<div class="location-id">ID: {location['id']}</div>
|
|
</div>
|
|
<div class="location-description">
|
|
{location['description']}
|
|
</div>
|
|
<div class="location-levels">
|
|
<strong>Level Range:</strong> {location['level_min']}-{location['level_max']}
|
|
</div>
|
|
<div class="location-spawns">
|
|
<strong>Wild Pets:</strong><br>
|
|
{spawn_badges}
|
|
</div>
|
|
</div>"""
|
|
else:
|
|
locations_html = """
|
|
<div class="location-card">
|
|
<div class="location-header">
|
|
<h3>No Locations Found</h3>
|
|
</div>
|
|
<div class="location-description">
|
|
No game locations are configured yet.
|
|
</div>
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Locations</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.locations-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 25px;
|
|
margin-top: 30px;
|
|
}}
|
|
|
|
.location-card {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
transition: transform 0.3s ease;
|
|
}}
|
|
|
|
.location-card:hover {{
|
|
transform: translateY(-5px);
|
|
}}
|
|
|
|
.location-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
|
|
.location-header h3 {{
|
|
margin: 0;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.location-id {{
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 5px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.8em;
|
|
}}
|
|
|
|
.location-description {{
|
|
padding: 20px;
|
|
color: var(--text-primary);
|
|
font-style: italic;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.location-spawns {{
|
|
padding: 20px;
|
|
}}
|
|
|
|
.spawn-badge {{
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-accent);
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 0.85em;
|
|
margin: 3px;
|
|
display: inline-block;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.info-section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.info-section h2 {{
|
|
color: var(--text-accent);
|
|
margin-top: 0;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🗺️ Game Locations</h1>
|
|
<p>Explore all areas and discover what pets await you!</p>
|
|
</div>
|
|
|
|
<div class="info-section">
|
|
<h2>🎯 How Locations Work</h2>
|
|
<p><strong>Travel:</strong> Use <code>!travel <location></code> to move between areas</p>
|
|
<p><strong>Explore:</strong> Use <code>!explore</code> to find wild pets in your current location</p>
|
|
<p><strong>Unlock:</strong> Some locations require achievements - catch specific pet types to unlock new areas!</p>
|
|
<p><strong>Weather:</strong> Check <code>!weather</code> for conditions that boost certain pet spawn rates</p>
|
|
</div>
|
|
|
|
<div class="locations-grid">
|
|
{locations_html}
|
|
</div>
|
|
|
|
<div class="info-section">
|
|
<p style="text-align: center; color: var(--text-secondary); margin: 0;">
|
|
💡 Use <code>!wild <location></code> in #petz to see what pets spawn in a specific area
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_petdex(self):
|
|
"""Serve the petdex page with all pet species data"""
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_error_page("Petdex", "Database not available")
|
|
return
|
|
|
|
# Fetch petdex data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
petdex_data = loop.run_until_complete(self.fetch_petdex_data(database))
|
|
loop.close()
|
|
|
|
self.serve_petdex_data(petdex_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching petdex data: {e}")
|
|
self.serve_error_page("Petdex", f"Error loading petdex: {str(e)}")
|
|
|
|
async def fetch_petdex_data(self, database):
|
|
"""Fetch all pet species data from database"""
|
|
try:
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get all pet species with evolution information
|
|
cursor = await db.execute("""
|
|
SELECT ps.*,
|
|
evolve_to.name as evolves_to_name,
|
|
(SELECT COUNT(*) FROM location_spawns ls WHERE ls.species_id = ps.id) as location_count
|
|
FROM pet_species ps
|
|
LEFT JOIN pet_species evolve_to ON ps.evolution_species_id = evolve_to.id
|
|
ORDER BY ps.rarity ASC, ps.name ASC
|
|
""")
|
|
|
|
rows = await cursor.fetchall()
|
|
pets = []
|
|
for row in rows:
|
|
pet_dict = {
|
|
'id': row[0], 'name': row[1], 'type1': row[2], 'type2': row[3],
|
|
'base_hp': row[4], 'base_attack': row[5], 'base_defense': row[6],
|
|
'base_speed': row[7], 'evolution_level': row[8],
|
|
'evolution_species_id': row[9], 'rarity': row[10],
|
|
'evolves_to_name': row[11], 'location_count': row[12]
|
|
}
|
|
pets.append(pet_dict)
|
|
|
|
# Get spawn locations for each pet
|
|
for pet in pets:
|
|
cursor = await db.execute("""
|
|
SELECT l.name, ls.min_level, ls.max_level, ls.spawn_rate
|
|
FROM location_spawns ls
|
|
JOIN locations l ON ls.location_id = l.id
|
|
WHERE ls.species_id = ?
|
|
ORDER BY l.name ASC
|
|
""", (pet['id'],))
|
|
spawn_rows = await cursor.fetchall()
|
|
pet['spawn_locations'] = []
|
|
for spawn_row in spawn_rows:
|
|
spawn_dict = {
|
|
'location_name': spawn_row[0], 'min_level': spawn_row[1],
|
|
'max_level': spawn_row[2], 'spawn_rate': spawn_row[3]
|
|
}
|
|
pet['spawn_locations'].append(spawn_dict)
|
|
|
|
return pets
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching petdex: {e}")
|
|
return []
|
|
|
|
def serve_petdex_data(self, petdex_data):
|
|
"""Serve petdex page with all pet species data"""
|
|
|
|
# Build pet cards HTML grouped by rarity
|
|
rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"}
|
|
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
|
|
|
|
pets_by_rarity = {}
|
|
for pet in petdex_data:
|
|
rarity = pet['rarity']
|
|
if rarity not in pets_by_rarity:
|
|
pets_by_rarity[rarity] = []
|
|
pets_by_rarity[rarity].append(pet)
|
|
|
|
petdex_html = ""
|
|
total_species = len(petdex_data)
|
|
|
|
for rarity in sorted(pets_by_rarity.keys()):
|
|
pets_in_rarity = pets_by_rarity[rarity]
|
|
rarity_name = rarity_names.get(rarity, f"Rarity {rarity}")
|
|
rarity_color = rarity_colors.get(rarity, "#ffffff")
|
|
|
|
petdex_html += f"""
|
|
<div class="rarity-section">
|
|
<h2 style="color: {rarity_color}; border-bottom: 2px solid {rarity_color}; padding-bottom: 10px;">
|
|
{rarity_name} ({len(pets_in_rarity)} species)
|
|
</h2>
|
|
<div class="pets-grid">"""
|
|
|
|
for pet in pets_in_rarity:
|
|
# Build type display
|
|
type_str = pet['type1']
|
|
if pet['type2']:
|
|
type_str += f"/{pet['type2']}"
|
|
|
|
# Build evolution info
|
|
evolution_info = ""
|
|
if pet['evolution_level'] and pet['evolves_to_name']:
|
|
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']} → {pet['evolves_to_name']}"
|
|
elif pet['evolution_level']:
|
|
evolution_info = f"<br><strong>Evolves:</strong> Level {pet['evolution_level']}"
|
|
|
|
# Build spawn locations
|
|
spawn_info = ""
|
|
if pet['spawn_locations']:
|
|
locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})"
|
|
for loc in pet['spawn_locations'][:3]]
|
|
if len(pet['spawn_locations']) > 3:
|
|
locations.append(f"+{len(pet['spawn_locations']) - 3} more")
|
|
spawn_info = f"<br><strong>Found in:</strong> {', '.join(locations)}"
|
|
else:
|
|
spawn_info = "<br><strong>Found in:</strong> Not yet available"
|
|
|
|
# Calculate total base stats
|
|
total_stats = pet['base_hp'] + pet['base_attack'] + pet['base_defense'] + pet['base_speed']
|
|
|
|
petdex_html += f"""
|
|
<div class="pet-card" style="border-left: 4px solid {rarity_color};">
|
|
<div class="pet-header">
|
|
<h3 style="color: {rarity_color};">{pet['name']}</h3>
|
|
<span class="type-badge">{type_str}</span>
|
|
</div>
|
|
<div class="pet-stats">
|
|
<div class="stat-row">
|
|
<span>HP: {pet['base_hp']}</span>
|
|
<span>ATK: {pet['base_attack']}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>DEF: {pet['base_defense']}</span>
|
|
<span>SPD: {pet['base_speed']}</span>
|
|
</div>
|
|
<div class="total-stats">Total: {total_stats}</div>
|
|
</div>
|
|
<div class="pet-info">
|
|
<strong>Rarity:</strong> {rarity_name}{evolution_info}{spawn_info}
|
|
</div>
|
|
</div>"""
|
|
|
|
petdex_html += """
|
|
</div>
|
|
</div>"""
|
|
|
|
if not petdex_data:
|
|
petdex_html = """
|
|
<div style="text-align: center; padding: 60px; color: var(--text-secondary);">
|
|
<h2>No pet species found!</h2>
|
|
<p>The petdex appears to be empty. Contact an administrator.</p>
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Petdex</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.stats-summary {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-value {{
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.stat-label {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.rarity-section {{
|
|
margin-bottom: 40px;
|
|
}}
|
|
|
|
.pets-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}}
|
|
|
|
.pet-card {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
transition: transform 0.3s ease;
|
|
}}
|
|
|
|
.pet-card:hover {{
|
|
transform: translateY(-3px);
|
|
}}
|
|
|
|
.pet-header {{
|
|
background: var(--bg-tertiary);
|
|
padding: 15px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
|
|
.pet-header h3 {{
|
|
margin: 0;
|
|
font-size: 1.2em;
|
|
}}
|
|
|
|
.type-badge {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
padding: 4px 12px;
|
|
border-radius: 15px;
|
|
font-size: 0.8em;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.pet-stats {{
|
|
padding: 15px;
|
|
background: var(--bg-tertiary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-row {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.total-stats {{
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
}}
|
|
|
|
.pet-info {{
|
|
padding: 15px;
|
|
font-size: 0.9em;
|
|
line-height: 1.4;
|
|
}}
|
|
|
|
.search-filter {{
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>📖 Petdex - Complete Pet Encyclopedia</h1>
|
|
<p>Comprehensive guide to all available pet species</p>
|
|
</div>
|
|
|
|
<div class="search-filter">
|
|
<div class="stats-summary">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{total_species}</div>
|
|
<div class="stat-label">Total Species</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['type1'] == 'Fire' or p['type2'] == 'Fire'])}</div>
|
|
<div class="stat-label">Fire Types</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['type1'] == 'Water' or p['type2'] == 'Water'])}</div>
|
|
<div class="stat-label">Water Types</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len([p for p in petdex_data if p['evolution_level']])}</div>
|
|
<div class="stat-label">Can Evolve</div>
|
|
</div>
|
|
</div>
|
|
<p style="color: var(--text-secondary); margin: 0;">🎯 Pets are organized by rarity. Use <code>!wild <location></code> in #petz to see what spawns where!</p>
|
|
</div>
|
|
|
|
{petdex_html}
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_profile(self, nickname):
|
|
"""Serve individual player profile page"""
|
|
# URL decode the nickname in case it has special characters
|
|
from urllib.parse import unquote
|
|
nickname = unquote(nickname)
|
|
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_player_error(nickname, "Database not available")
|
|
return
|
|
|
|
# Fetch player data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
|
|
loop.close()
|
|
|
|
if player_data is None:
|
|
self.serve_player_not_found(nickname)
|
|
return
|
|
|
|
self.serve_player_data(nickname, player_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching player data for {nickname}: {e}")
|
|
self.serve_player_error(nickname, f"Error loading player data: {str(e)}")
|
|
return
|
|
|
|
async def fetch_player_data(self, database, nickname):
|
|
"""Fetch all player data from database"""
|
|
try:
|
|
# Get player info
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get player basic info
|
|
cursor = await db.execute("""
|
|
SELECT p.*, l.name as location_name, l.description as location_desc
|
|
FROM players p
|
|
LEFT JOIN locations l ON p.current_location_id = l.id
|
|
WHERE p.nickname = ?
|
|
""", (nickname,))
|
|
player = await cursor.fetchone()
|
|
if not player:
|
|
return None
|
|
|
|
# Convert to dict manually
|
|
player_dict = {
|
|
'id': player[0],
|
|
'nickname': player[1],
|
|
'created_at': player[2],
|
|
'last_active': player[3],
|
|
'level': player[4],
|
|
'experience': player[5],
|
|
'money': player[6],
|
|
'current_location_id': player[7],
|
|
'location_name': player[8],
|
|
'location_desc': player[9]
|
|
}
|
|
|
|
# Get player pets
|
|
cursor = await db.execute("""
|
|
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
|
|
FROM pets p
|
|
JOIN pet_species ps ON p.species_id = ps.id
|
|
WHERE p.player_id = ?
|
|
ORDER BY p.is_active DESC, p.level DESC, p.id ASC
|
|
""", (player_dict['id'],))
|
|
pets_rows = await cursor.fetchall()
|
|
pets = []
|
|
for row in pets_rows:
|
|
pet_dict = {
|
|
'id': row[0], 'player_id': row[1], 'species_id': row[2],
|
|
'nickname': row[3], 'level': row[4], 'experience': row[5],
|
|
'hp': row[6], 'max_hp': row[7], 'attack': row[8],
|
|
'defense': row[9], 'speed': row[10], 'happiness': row[11],
|
|
'caught_at': row[12], 'is_active': row[13],
|
|
'species_name': row[14], 'type1': row[15], 'type2': row[16]
|
|
}
|
|
pets.append(pet_dict)
|
|
|
|
# Get player achievements
|
|
cursor = await db.execute("""
|
|
SELECT pa.*, a.name as achievement_name, a.description as achievement_desc
|
|
FROM player_achievements pa
|
|
JOIN achievements a ON pa.achievement_id = a.id
|
|
WHERE pa.player_id = ?
|
|
ORDER BY pa.completed_at DESC
|
|
""", (player_dict['id'],))
|
|
achievements_rows = await cursor.fetchall()
|
|
achievements = []
|
|
for row in achievements_rows:
|
|
achievement_dict = {
|
|
'id': row[0], 'player_id': row[1], 'achievement_id': row[2],
|
|
'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5]
|
|
}
|
|
achievements.append(achievement_dict)
|
|
|
|
# Get player inventory
|
|
cursor = await db.execute("""
|
|
SELECT i.name, i.description, i.category, i.rarity, pi.quantity
|
|
FROM player_inventory pi
|
|
JOIN items i ON pi.item_id = i.id
|
|
WHERE pi.player_id = ?
|
|
ORDER BY i.rarity DESC, i.name ASC
|
|
""", (player_dict['id'],))
|
|
inventory_rows = await cursor.fetchall()
|
|
inventory = []
|
|
for row in inventory_rows:
|
|
item_dict = {
|
|
'name': row[0], 'description': row[1], 'category': row[2],
|
|
'rarity': row[3], 'quantity': row[4]
|
|
}
|
|
inventory.append(item_dict)
|
|
|
|
# Get player gym badges
|
|
cursor = await db.execute("""
|
|
SELECT g.name, g.badge_name, g.badge_icon, l.name as location_name,
|
|
pgb.victories, pgb.first_victory_date, pgb.highest_difficulty
|
|
FROM player_gym_battles pgb
|
|
JOIN gyms g ON pgb.gym_id = g.id
|
|
JOIN locations l ON g.location_id = l.id
|
|
WHERE pgb.player_id = ? AND pgb.victories > 0
|
|
ORDER BY pgb.first_victory_date ASC
|
|
""", (player_dict['id'],))
|
|
gym_badges_rows = await cursor.fetchall()
|
|
gym_badges = []
|
|
for row in gym_badges_rows:
|
|
badge_dict = {
|
|
'gym_name': row[0], 'badge_name': row[1], 'badge_icon': row[2],
|
|
'location_name': row[3], 'victories': row[4],
|
|
'first_victory_date': row[5], 'highest_difficulty': row[6]
|
|
}
|
|
gym_badges.append(badge_dict)
|
|
|
|
# Get player encounters using database method
|
|
encounters = []
|
|
try:
|
|
# Use the existing database method which handles row factory properly
|
|
temp_encounters = await database.get_player_encounters(player_dict['id'])
|
|
for enc in temp_encounters:
|
|
encounter_dict = {
|
|
'species_name': enc['species_name'],
|
|
'type1': enc['type1'],
|
|
'type2': enc['type2'],
|
|
'rarity': enc['rarity'],
|
|
'total_encounters': enc['total_encounters'],
|
|
'caught_count': enc['caught_count'],
|
|
'first_encounter_date': enc['first_encounter_date']
|
|
}
|
|
encounters.append(encounter_dict)
|
|
except Exception as e:
|
|
print(f"Error fetching encounters: {e}")
|
|
encounters = []
|
|
|
|
# Get encounter stats
|
|
try:
|
|
encounter_stats = await database.get_encounter_stats(player_dict['id'])
|
|
except Exception as e:
|
|
print(f"Error fetching encounter stats: {e}")
|
|
encounter_stats = {
|
|
'species_encountered': 0,
|
|
'total_encounters': 0,
|
|
'total_species': 0,
|
|
'completion_percentage': 0.0
|
|
}
|
|
|
|
return {
|
|
'player': player_dict,
|
|
'pets': pets,
|
|
'achievements': achievements,
|
|
'inventory': inventory,
|
|
'gym_badges': gym_badges,
|
|
'encounters': encounters,
|
|
'encounter_stats': encounter_stats
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching player {nickname}: {e}")
|
|
return None
|
|
|
|
def serve_player_not_found(self, nickname):
|
|
"""Serve player not found page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Player Not Found</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🚫 Player Not Found</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Player "{nickname}" not found</h2>
|
|
<p>This player hasn't started their journey yet or doesn't exist.</p>
|
|
<p>Players can use <code>!start</code> in #petz to begin their adventure!</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(404)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_error(self, nickname, error_msg):
|
|
"""Serve player error page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>⚠️ Error</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Unable to load player data</h2>
|
|
<p>{error_msg}</p>
|
|
<p>Please try again later or contact an administrator.</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_data(self, nickname, player_data):
|
|
"""Serve player profile page with real data"""
|
|
player = player_data['player']
|
|
pets = player_data['pets']
|
|
achievements = player_data['achievements']
|
|
inventory = player_data.get('inventory', [])
|
|
gym_badges = player_data.get('gym_badges', [])
|
|
encounters = player_data.get('encounters', [])
|
|
encounter_stats = player_data.get('encounter_stats', {})
|
|
|
|
# Calculate stats
|
|
active_pets = [pet for pet in pets if pet['is_active']]
|
|
total_pets = len(pets)
|
|
active_count = len(active_pets)
|
|
|
|
# Build pets table HTML
|
|
pets_html = ""
|
|
if pets:
|
|
for pet in pets:
|
|
status = "⭐ Active" if pet['is_active'] else "📦 Storage"
|
|
status_class = "pet-active" if pet['is_active'] else "pet-stored"
|
|
name = pet['nickname'] or pet['species_name']
|
|
|
|
type_str = pet['type1']
|
|
if pet['type2']:
|
|
type_str += f"/{pet['type2']}"
|
|
|
|
pets_html += f"""
|
|
<tr>
|
|
<td class="{status_class}">{status}</td>
|
|
<td><strong>{name}</strong></td>
|
|
<td>{pet['species_name']}</td>
|
|
<td><span class="type-badge">{type_str}</span></td>
|
|
<td>{pet['level']}</td>
|
|
<td>{pet['hp']}/{pet['max_hp']}</td>
|
|
<td>ATK: {pet['attack']} | DEF: {pet['defense']} | SPD: {pet['speed']}</td>
|
|
</tr>"""
|
|
else:
|
|
pets_html = """
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 40px;">
|
|
No pets found. Use !explore and !catch to start your collection!
|
|
</td>
|
|
</tr>"""
|
|
|
|
# Build achievements HTML
|
|
achievements_html = ""
|
|
if achievements:
|
|
for achievement in achievements:
|
|
achievements_html += f"""
|
|
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid var(--text-accent);">
|
|
<strong>🏆 {achievement['achievement_name']}</strong><br>
|
|
<small>{achievement['achievement_desc']}</small><br>
|
|
<em style="color: var(--text-secondary);">Earned: {achievement['completed_at']}</em>
|
|
</div>"""
|
|
else:
|
|
achievements_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No achievements yet. Keep exploring and catching pets to earn achievements!
|
|
</div>"""
|
|
|
|
# Build inventory HTML
|
|
inventory_html = ""
|
|
if inventory:
|
|
rarity_symbols = {
|
|
"common": "○",
|
|
"uncommon": "◇",
|
|
"rare": "◆",
|
|
"epic": "★",
|
|
"legendary": "✦"
|
|
}
|
|
rarity_colors = {
|
|
"common": "#ffffff",
|
|
"uncommon": "#1eff00",
|
|
"rare": "#0070dd",
|
|
"epic": "#a335ee",
|
|
"legendary": "#ff8000"
|
|
}
|
|
|
|
for item in inventory:
|
|
symbol = rarity_symbols.get(item['rarity'], "○")
|
|
color = rarity_colors.get(item['rarity'], "#ffffff")
|
|
quantity_str = f" x{item['quantity']}" if item['quantity'] > 1 else ""
|
|
|
|
inventory_html += f"""
|
|
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid {color};">
|
|
<strong style="color: {color};">{symbol} {item['name']}{quantity_str}</strong><br>
|
|
<small>{item['description']}</small><br>
|
|
<em style="color: var(--text-secondary);">Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}</em>
|
|
</div>"""
|
|
else:
|
|
inventory_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No items yet. Try exploring to find useful items!
|
|
</div>"""
|
|
|
|
# Build gym badges HTML
|
|
badges_html = ""
|
|
if gym_badges:
|
|
for badge in gym_badges:
|
|
# Safely handle date formatting
|
|
try:
|
|
if badge['first_victory_date'] and isinstance(badge['first_victory_date'], str):
|
|
badge_date = badge['first_victory_date'].split()[0]
|
|
else:
|
|
badge_date = 'Unknown'
|
|
except (AttributeError, IndexError):
|
|
badge_date = 'Unknown'
|
|
badges_html += f"""
|
|
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid gold;">
|
|
<strong>{badge['badge_icon']} {badge['badge_name']}</strong><br>
|
|
<small>Earned from {badge['gym_name']} ({badge['location_name']})</small><br>
|
|
<em style="color: var(--text-secondary);">First victory: {badge_date} | Total victories: {badge['victories']} | Highest difficulty: Level {badge['highest_difficulty']}</em>
|
|
</div>"""
|
|
else:
|
|
badges_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No gym badges yet. Challenge gyms to earn badges and prove your training skills!
|
|
</div>"""
|
|
|
|
# Build encounters HTML
|
|
encounters_html = ""
|
|
if encounters:
|
|
rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"}
|
|
for encounter in encounters:
|
|
rarity_color = rarity_colors.get(encounter['rarity'], "#ffffff")
|
|
type_str = encounter['type1']
|
|
if encounter['type2']:
|
|
type_str += f"/{encounter['type2']}"
|
|
|
|
# Safely handle date formatting
|
|
try:
|
|
if encounter['first_encounter_date'] and isinstance(encounter['first_encounter_date'], str):
|
|
encounter_date = encounter['first_encounter_date'].split()[0]
|
|
else:
|
|
encounter_date = 'Unknown'
|
|
except (AttributeError, IndexError):
|
|
encounter_date = 'Unknown'
|
|
|
|
encounters_html += f"""
|
|
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid {rarity_color};">
|
|
<strong style="color: {rarity_color};">{encounter['species_name']}</strong> <span class="type-badge">{type_str}</span><br>
|
|
<small>Encountered {encounter['total_encounters']} times | Caught {encounter['caught_count']} times</small><br>
|
|
<em style="color: var(--text-secondary);">First seen: {encounter_date}</em>
|
|
</div>"""
|
|
else:
|
|
encounters_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No pets encountered yet. Use !explore to discover wild pets!
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - {nickname}'s Profile</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
margin-bottom: 30px;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.stats-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-tertiary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-value {{
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.stat-label {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.pets-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.pets-table th, .pets-table td {{
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.pets-table th {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.pets-table tr:hover {{
|
|
background: var(--hover-color);
|
|
}}
|
|
|
|
.pet-active {{
|
|
color: var(--text-accent);
|
|
font-weight: bold;
|
|
}}
|
|
|
|
.pet-stored {{
|
|
color: var(--text-secondary);
|
|
}}
|
|
|
|
.type-badge {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.8em;
|
|
margin-right: 5px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🐾 {nickname}'s Profile</h1>
|
|
<p>Level {player['level']} Trainer</p>
|
|
<p><em>Currently in {player.get('location_name', 'Unknown Location')}</em></p>
|
|
</div>
|
|
|
|
<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
|
|
|
|
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() |