Add comprehensive web interface enhancements and encounter tracking

Major Features Added:
- Complete petdex page showing all available pets with stats, types, evolution info
- Encounter tracking system recording pet discoveries and catch statistics
- Gym badges display on player profiles with victory counts and dates
- Enhanced player profiles with discovery progress and completion percentages

Technical Implementation:
- New /petdex route with rarity-organized pet encyclopedia
- Database encounter tracking with automatic integration into exploration/catch
- Updated webserver.py with encounter data fetching and display
- Fixed battle_system.py syntax error in gym battle completion logic
- Organized project by moving unused bot files to backup_bots/ folder

Database Changes:
- Added player_encounters table for tracking discoveries
- Added methods: record_encounter, get_player_encounters, get_encounter_stats
- Enhanced player profile queries to include gym badges and encounters

Web Interface Updates:
- Petdex page with search stats, rarity grouping, and spawn location info
- Player profiles now show species seen, completion %, gym badges earned
- Encounter section displaying discovered pets with catch statistics
- Updated navigation to include petdex link on main game hub

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-14 16:32:25 +01:00
parent bd455f1be5
commit 1ce7158200
7 changed files with 684 additions and 63 deletions

2
.gitignore vendored
View file

@ -75,4 +75,4 @@ Thumbs.db
# IRC bot specific
*.pid
*.lock
*.lockbackup_bots/

View file

@ -224,74 +224,74 @@ class BattleSystem(BaseModule):
return
defeated_pet = gym_team[current_pet_index]
self.send_message(channel, f"🎉 {nickname}: You defeated {defeated_pet['species_name']}!")
# Award experience for defeating gym pet
await self.award_battle_experience(channel, nickname, player, defeated_pet, "gym")
# Check if there are more pets
if await self.database.advance_gym_battle(player["id"]):
# More pets to fight
next_index = current_pet_index + 1
next_pet = gym_team[next_index]
self.send_message(channel,
f"🥊 {gym_battle['leader_name']} sends out {next_pet['species_name']} (Lv.{next_pet['level']})!")
self.send_message(channel, f"🎉 {nickname}: You defeated {defeated_pet['species_name']}!")
# Start battle with next gym pet
active_pets = await self.database.get_active_pets(player["id"])
player_pet = active_pets[0] # Use first active pet
# Award experience for defeating gym pet
await self.award_battle_experience(channel, nickname, player, defeated_pet, "gym")
# Create gym pet data for battle engine
next_gym_pet_data = {
"species_name": next_pet["species_name"],
"level": next_pet["level"],
"type1": next_pet["type1"],
"type2": next_pet["type2"],
"stats": {
"hp": next_pet["hp"],
"attack": next_pet["attack"],
"defense": next_pet["defense"],
"speed": next_pet["speed"]
# Check if there are more pets
if await self.database.advance_gym_battle(player["id"]):
# More pets to fight
next_index = current_pet_index + 1
next_pet = gym_team[next_index]
self.send_message(channel,
f"🥊 {gym_battle['leader_name']} sends out {next_pet['species_name']} (Lv.{next_pet['level']})!")
# Start battle with next gym pet
active_pets = await self.database.get_active_pets(player["id"])
player_pet = active_pets[0] # Use first active pet
# Create gym pet data for battle engine
next_gym_pet_data = {
"species_name": next_pet["species_name"],
"level": next_pet["level"],
"type1": next_pet["type1"],
"type2": next_pet["type2"],
"stats": {
"hp": next_pet["hp"],
"attack": next_pet["attack"],
"defense": next_pet["defense"],
"speed": next_pet["speed"]
}
}
}
# Start next battle
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, next_gym_pet_data)
self.send_message(channel,
f"⚔️ Your {player_pet['species_name']} (HP: {battle['player_hp']}/{player_pet['max_hp']}) vs {next_pet['species_name']} (HP: {battle['wild_hp']}/{next_pet['hp']})")
# Show available moves
moves_colored = " | ".join([
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
for move in battle["available_moves"]
])
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>")
# Start next battle
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, next_gym_pet_data)
self.send_message(channel,
f"⚔️ Your {player_pet['species_name']} (HP: {battle['player_hp']}/{player_pet['max_hp']}) vs {next_pet['species_name']} (HP: {battle['wild_hp']}/{next_pet['hp']})")
# Show available moves
moves_colored = " | ".join([
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
for move in battle["available_moves"]
])
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>")
else:
# All gym pets defeated - gym victory!
result = await self.database.end_gym_battle(player["id"], victory=True)
self.send_message(channel, f"🏆 {nickname}: You defeated all of {gym_battle['leader_name']}'s pets!")
self.send_message(channel,
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Impressive! You've earned the {gym_battle['gym_name']} badge!\"")
self.send_message(channel, f"🎉 {nickname} earned the {gym_battle['gym_name']} badge {gym_battle['badge_icon']}!")
# Award rewards based on difficulty
money_reward = 500 + (result["difficulty_level"] * 100)
self.send_message(channel, f"💰 Rewards: ${money_reward} | 🌟 Gym mastery increased!")
else:
# All gym pets defeated - gym victory!
result = await self.database.end_gym_battle(player["id"], victory=True)
# Player lost gym battle
result = await self.database.end_gym_battle(player["id"], victory=False)
self.send_message(channel, f"🏆 {nickname}: You defeated all of {gym_battle['leader_name']}'s pets!")
self.send_message(channel, f"💀 {nickname}: Your pet fainted!")
self.send_message(channel,
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Impressive! You've earned the {gym_battle['gym_name']} badge!\"")
self.send_message(channel, f"🎉 {nickname} earned the {gym_battle['gym_name']} badge {gym_battle['badge_icon']}!")
# Award rewards based on difficulty
money_reward = 500 + (result["difficulty_level"] * 100)
self.send_message(channel, f"💰 Rewards: ${money_reward} | 🌟 Gym mastery increased!")
else:
# Player lost gym battle
result = await self.database.end_gym_battle(player["id"], victory=False)
self.send_message(channel, f"💀 {nickname}: Your pet fainted!")
self.send_message(channel,
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"")
self.send_message(channel,
f"💡 {nickname}: Try leveling up your pets or bringing items to heal during battle!")
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"")
self.send_message(channel,
f"💡 {nickname}: Try leveling up your pets or bringing items to heal during battle!")
except Exception as e:
self.send_message(channel, f"{nickname}: Gym battle error occurred - please !forfeit and try again")

View file

@ -46,6 +46,9 @@ class Exploration(BaseModule):
if pet["type2"]:
type_str += f"/{pet['type2']}"
# Record the encounter
await self.database.record_encounter(player["id"], pet["species_name"])
self.send_message(channel,
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
@ -200,6 +203,9 @@ class Exploration(BaseModule):
# Successful catch during battle
result = await self.game_engine.attempt_catch_current_location(player["id"], wild_pet)
# Record the successful catch
await self.database.record_encounter(player["id"], wild_pet["species_name"], was_caught=True)
# End the battle
await_result = await self.game_engine.battle_engine.end_battle(player["id"], "caught")
@ -236,6 +242,9 @@ class Exploration(BaseModule):
# Check for achievements after successful catch
if "Success!" in result:
# Record the successful catch
await self.database.record_encounter(player["id"], target_pet["species_name"], was_caught=True)
# Award experience for successful catch
await self.award_catch_experience(channel, nickname, player, target_pet)

View file

@ -271,6 +271,21 @@ class Database:
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS player_encounters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
species_id INTEGER NOT NULL,
first_encounter_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_encounters INTEGER DEFAULT 1,
last_encounter_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
caught_count INTEGER DEFAULT 0,
FOREIGN KEY (player_id) REFERENCES players (id),
FOREIGN KEY (species_id) REFERENCES pet_species (id),
UNIQUE(player_id, species_id)
)
""")
await db.commit()
async def get_player(self, nickname: str) -> Optional[Dict]:
@ -1209,4 +1224,96 @@ class Database:
await self.record_gym_victory(player_id, battle_dict["gym_id"])
await db.commit()
return result
return result
async def record_encounter(self, player_id: int, species_name: str, was_caught: bool = False) -> bool:
"""Record a player encountering a pet species"""
try:
async with aiosqlite.connect(self.db_path) as db:
# Get species ID
cursor = await db.execute(
"SELECT id FROM pet_species WHERE name = ?", (species_name,)
)
species_row = await cursor.fetchone()
if not species_row:
return False
species_id = species_row[0]
# Check if encounter already exists
cursor = await db.execute("""
SELECT total_encounters, caught_count FROM player_encounters
WHERE player_id = ? AND species_id = ?
""", (player_id, species_id))
existing = await cursor.fetchone()
if existing:
# Update existing encounter
new_total = existing[0] + 1
new_caught = existing[1] + (1 if was_caught else 0)
await db.execute("""
UPDATE player_encounters
SET total_encounters = ?, last_encounter_date = CURRENT_TIMESTAMP, caught_count = ?
WHERE player_id = ? AND species_id = ?
""", (new_total, new_caught, player_id, species_id))
else:
# Create new encounter record
caught_count = 1 if was_caught else 0
await db.execute("""
INSERT INTO player_encounters (player_id, species_id, caught_count)
VALUES (?, ?, ?)
""", (player_id, species_id, caught_count))
await db.commit()
return True
except Exception as e:
print(f"Error recording encounter: {e}")
return False
async def get_player_encounters(self, player_id: int) -> List[Dict]:
"""Get all encounters for a player"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT pe.*, ps.name as species_name, ps.type1, ps.type2, ps.rarity
FROM player_encounters pe
JOIN pet_species ps ON pe.species_id = ps.id
WHERE pe.player_id = ?
ORDER BY pe.first_encounter_date ASC
""", (player_id,))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def get_encounter_stats(self, player_id: int) -> Dict:
"""Get encounter statistics for a player"""
async with aiosqlite.connect(self.db_path) as db:
# Total species encountered
cursor = await db.execute("""
SELECT COUNT(*) FROM player_encounters WHERE player_id = ?
""", (player_id,))
species_encountered = (await cursor.fetchone())[0]
# Total encounters
cursor = await db.execute("""
SELECT SUM(total_encounters) FROM player_encounters WHERE player_id = ?
""", (player_id,))
total_encounters_result = await cursor.fetchone()
total_encounters = total_encounters_result[0] if total_encounters_result[0] else 0
# Total species available
cursor = await db.execute("""
SELECT COUNT(*) FROM pet_species
""")
total_species = (await cursor.fetchone())[0]
# Calculate completion percentage
completion_percentage = (species_encountered / total_species * 100) if total_species > 0 else 0
return {
"species_encountered": species_encountered,
"total_encounters": total_encounters,
"total_species": total_species,
"completion_percentage": round(completion_percentage, 1)
}

View file

@ -39,6 +39,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
self.serve_leaderboard()
elif path == '/locations':
self.serve_locations()
elif path == '/petdex':
self.serve_petdex()
else:
self.send_error(404, "Page not found")
@ -148,6 +150,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
<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">
@ -819,6 +826,376 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
</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 &lt;location&gt;</code> in #petz to see what spawns where!</p>
</div>
{petdex_html}
</body>
</html>"""
self.send_response(200)
@ -945,11 +1322,68 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
}
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
cursor = await db.execute("""
SELECT pe.*, ps.name as species_name, ps.type1, ps.type2, ps.rarity
FROM player_encounters pe
JOIN pet_species ps ON pe.species_id = ps.id
WHERE pe.player_id = ?
ORDER BY pe.first_encounter_date ASC
""", (player_dict['id'],))
encounters_rows = await cursor.fetchall()
encounters = []
for row in encounters_rows:
encounter_dict = {
'species_name': row[6], 'type1': row[7], 'type2': row[8], 'rarity': row[9],
'total_encounters': row[4], 'caught_count': row[5], 'first_encounter_date': row[2]
}
encounters.append(encounter_dict)
# Get encounter stats
cursor = await db.execute("""
SELECT COUNT(*) as species_encountered,
SUM(total_encounters) as total_encounters,
(SELECT COUNT(*) FROM pet_species) as total_species
FROM player_encounters
WHERE player_id = ?
""", (player_dict['id'],))
stats_row = await cursor.fetchone()
encounter_stats = {
'species_encountered': stats_row[0] if stats_row[0] else 0,
'total_encounters': stats_row[1] if stats_row[1] else 0,
'total_species': stats_row[2] if stats_row[2] else 0
}
completion_percentage = (encounter_stats['species_encountered'] / encounter_stats['total_species'] * 100) if encounter_stats['total_species'] > 0 else 0
encounter_stats['completion_percentage'] = round(completion_percentage, 1)
return {
'player': player_dict,
'pets': pets,
'achievements': achievements,
'inventory': inventory
'inventory': inventory,
'gym_badges': gym_badges,
'encounters': encounters,
'encounter_stats': encounter_stats
}
except Exception as e:
@ -1106,6 +1540,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
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']]
@ -1193,6 +1630,47 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
No items yet. Try exploring to find useful items!
</div>"""
# Build gym badges HTML
badges_html = ""
if gym_badges:
for badge in gym_badges:
badge_date = badge['first_victory_date'].split()[0] if badge['first_victory_date'] else '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']}"
encounter_date = encounter['first_encounter_date'].split()[0] if encounter['first_encounter_date'] else '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>
@ -1381,6 +1859,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
<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>
@ -1420,6 +1906,25 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
{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>"""