Update bot and webserver integration for healing system support

- Enhance bot initialization to support healing system background tasks
- Update webserver to properly handle healing system web interfaces
- Ensure proper integration between IRC bot and web components
- Add support for healing system status monitoring and display
- Maintain unified user experience across IRC and web interfaces

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-16 11:33:37 +00:00
parent 8ae7da8379
commit 530134bd36
2 changed files with 856 additions and 35 deletions

View file

@ -19,7 +19,8 @@ from src.database import Database
from src.game_engine import GameEngine
from src.irc_connection_manager import IRCConnectionManager, ConnectionState
from src.rate_limiter import RateLimiter, get_command_category
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder
from src.npc_events import NPCEventsManager
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder, NPCEventsModule
from webserver import PetBotWebServer
from config import IRC_CONFIG, RATE_LIMIT_CONFIG
@ -42,6 +43,7 @@ class PetBotWithReconnect:
# Core components
self.database = Database()
self.game_engine = GameEngine(self.database)
self.npc_events = None
self.config = IRC_CONFIG
# Connection and state management
@ -82,6 +84,11 @@ class PetBotWithReconnect:
await self.game_engine.load_game_data()
self.logger.info("✅ Game data loaded")
# Initialize NPC events manager
self.logger.info("🔄 Initializing NPC events manager...")
self.npc_events = NPCEventsManager(self.database)
self.logger.info("✅ NPC events manager initialized")
# Load modules
self.logger.info("🔄 Loading command modules...")
await self.load_modules()
@ -118,6 +125,7 @@ class PetBotWithReconnect:
self.logger.info("🔄 Starting background tasks...")
asyncio.create_task(self.background_validation_task())
asyncio.create_task(self.connection_stats_task())
asyncio.create_task(self.npc_events.start_background_task())
self.logger.info("✅ Background tasks started")
self.logger.info("🎉 All components initialized successfully!")
@ -137,7 +145,8 @@ class PetBotWithReconnect:
Admin,
Inventory,
GymBattles,
TeamBuilder
TeamBuilder,
NPCEventsModule
]
self.modules = {}

View file

@ -11,6 +11,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from threading import Thread
import time
import math
# Add the project directory to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@ -177,6 +178,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
padding: 15px 20px;
box-shadow: 0 2px 10px var(--shadow-color);
margin-bottom: 0;
border-radius: 0 0 15px 15px;
}
.nav-content {
@ -576,15 +578,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
("locations", "🏛️ Gyms")
]),
("petdex", "📚 Petdex", [
("petdex", "🔷 by Type"),
("petdex", "⭐ by Rarity"),
("petdex", "🔍 Search")
("petdex?sort=type", "🔷 by Type"),
("petdex?sort=rarity", "⭐ by Rarity"),
("petdex?sort=name", "🔤 by Name"),
("petdex?sort=location", "🗺️ by Location"),
("petdex?sort=all", "📋 Show All"),
("petdex#search", "🔍 Search")
]),
("help", "📖 Help", [
("help", "⚡ Commands"),
("help", "📖 Web Guide"),
("help", "❓ FAQ")
])
("help", "📖 Help", [])
]
nav_links = ""
@ -1256,15 +1257,61 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
}
"""
# Get the unified template with additional CSS
html_content = self.get_page_template("Command Help", content, "help")
# Insert additional CSS before closing </style> tag
html_content = html_content.replace("</style>", additional_css + "</style>")
# Load help.html content and extract both CSS and body content
try:
with open('help.html', 'r', encoding='utf-8') as f:
help_content = f.read()
import re
# Extract CSS from help.html
css_match = re.search(r'<style[^>]*>(.*?)</style>', help_content, re.DOTALL)
help_css = css_match.group(1) if css_match else ""
# Extract body content (everything between <body> tags)
body_match = re.search(r'<body[^>]*>(.*?)</body>', help_content, re.DOTALL)
if body_match:
body_content = body_match.group(1)
# Remove the back link since we'll have the navigation bar
body_content = re.sub(r'<a href="/" class="back-link">.*?</a>', '', body_content, flags=re.DOTALL)
else:
# Fallback: use original content if we can't parse it
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(help_content.encode())
return
# Create template with merged CSS
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot - Help & Commands</title>
<style>
{self.get_unified_css()}
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
/* Help page specific styles */
{help_css}
</style>
</head>
<body>
{self.get_navigation_bar("help")}
<div class="main-container">
{body_content}
</div>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
except FileNotFoundError:
self.serve_error_page("Help", "Help file not found")
except Exception as e:
self.serve_error_page("Help", f"Error loading help file: {str(e)}")
def serve_players(self):
"""Serve the players page with real data"""
@ -1894,9 +1941,10 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
asyncio.set_event_loop(loop)
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
player_locations = loop.run_until_complete(self.fetch_player_locations(database))
loop.close()
self.serve_locations_data(locations_data)
self.serve_locations_data(locations_data, player_locations)
except Exception as e:
print(f"Error fetching locations data: {e}")
@ -1938,7 +1986,243 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
print(f"Database error fetching locations: {e}")
return []
def serve_locations_data(self, locations_data):
async def fetch_player_locations(self, database):
"""Fetch player locations for the interactive map"""
try:
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT p.nickname, p.current_location_id, l.name as location_name
FROM players p
JOIN locations l ON p.current_location_id = l.id
ORDER BY p.nickname
""")
rows = await cursor.fetchall()
players = []
for row in rows:
player_dict = {
'nickname': row['nickname'],
'location_id': row['current_location_id'],
'location_name': row['location_name']
}
players.append(player_dict)
return players
except Exception as e:
print(f"Database error fetching player locations: {e}")
return []
def create_interactive_map(self, locations_data, player_locations):
"""Create an interactive SVG map showing player locations"""
if not locations_data:
return ""
# Define map layout - create a unique visual design
map_positions = {
1: {"x": 200, "y": 400, "shape": "circle", "color": "#4CAF50"}, # Starter Town - central
2: {"x": 100, "y": 200, "shape": "hexagon", "color": "#2E7D32"}, # Whispering Woods - forest
3: {"x": 400, "y": 150, "shape": "diamond", "color": "#FF9800"}, # Thunder Peaks - mountain
4: {"x": 550, "y": 300, "shape": "octagon", "color": "#795548"}, # Stone Caverns - cave
5: {"x": 300, "y": 500, "shape": "star", "color": "#2196F3"}, # Frozen Lake - ice
6: {"x": 500, "y": 450, "shape": "triangle", "color": "#F44336"} # Volcanic Crater - fire
}
# Create player location groups
location_players = {}
for player in player_locations or []:
loc_id = player['location_id']
if loc_id not in location_players:
location_players[loc_id] = []
location_players[loc_id].append(player['nickname'])
# SVG map content
svg_content = ""
# Add connecting paths between locations
paths = [
(1, 2), (1, 3), (1, 5), # Starter Town connections
(2, 5), (3, 4), (4, 6), (5, 6) # Other connections
]
for start, end in paths:
if start in map_positions and end in map_positions:
start_pos = map_positions[start]
end_pos = map_positions[end]
svg_content += f"""
<line x1="{start_pos['x']}" y1="{start_pos['y']}"
x2="{end_pos['x']}" y2="{end_pos['y']}"
stroke="#444" stroke-width="2" stroke-dasharray="5,5" opacity="0.6"/>
"""
# Add location shapes
for location in locations_data:
loc_id = location['id']
if loc_id not in map_positions:
continue
pos = map_positions[loc_id]
players_here = location_players.get(loc_id, [])
player_count = len(players_here)
# Create shape based on type
shape_svg = self.create_location_shape(pos, location, player_count)
svg_content += shape_svg
# Add location label
svg_content += f"""
<text x="{pos['x']}" y="{pos['y'] - 45}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="14"
font-weight="bold"
fill="white">
{location['name']}
</text>
"""
# Add player names if any
if players_here:
player_text = ", ".join(players_here)
svg_content += f"""
<text x="{pos['x']}" y="{pos['y'] + 50}"
text-anchor="middle"
font-family="Arial, sans-serif"
font-size="12"
fill="#FFD700">
{player_text}
</text>
"""
return f"""
<div class="map-section">
<h2>🗺 Interactive World Map</h2>
<p style="text-align: center; color: var(--text-secondary); margin-bottom: 20px;">
Current player locations - shapes represent different terrain types
</p>
<div class="map-container">
<svg width="700" height="600" viewBox="0 0 700 600" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);">
{svg_content}
</svg>
</div>
<div class="map-legend">
<div class="legend-item">
<div class="legend-shape circle" style="background: #4CAF50;"></div>
<span>Towns</span>
</div>
<div class="legend-item">
<div class="legend-shape hexagon" style="background: #2E7D32;"></div>
<span>Forests</span>
</div>
<div class="legend-item">
<div class="legend-shape diamond" style="background: #FF9800;"></div>
<span>Mountains</span>
</div>
<div class="legend-item">
<div class="legend-shape octagon" style="background: #795548;"></div>
<span>Caves</span>
</div>
<div class="legend-item">
<div class="legend-shape star" style="background: #2196F3;"></div>
<span>Ice Areas</span>
</div>
<div class="legend-item">
<div class="legend-shape triangle" style="background: #F44336;"></div>
<span>Volcanic</span>
</div>
</div>
</div>
"""
def create_location_shape(self, pos, location, player_count):
"""Create SVG shape for a location based on its type"""
x, y = pos['x'], pos['y']
color = pos['color']
shape = pos['shape']
# Add glow effect if players are present
glow = 'filter="url(#glow)"' if player_count > 0 else ''
# Base size with scaling for player count
base_size = 25 + (player_count * 3)
if shape == "circle":
return f"""
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<circle cx="{x}" cy="{y}" r="{base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "hexagon":
points = []
for i in range(6):
angle = i * 60 * math.pi / 180
px = x + base_size * math.cos(angle)
py = y + base_size * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "diamond":
return f"""
<polygon points="{x},{y-base_size} {x+base_size},{y} {x},{y+base_size} {x-base_size},{y}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "triangle":
return f"""
<polygon points="{x},{y-base_size} {x+base_size},{y+base_size} {x-base_size},{y+base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "star":
# Create 5-pointed star
points = []
for i in range(10):
angle = i * 36 * math.pi / 180
radius = base_size if i % 2 == 0 else base_size * 0.5
px = x + radius * math.cos(angle)
py = y + radius * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
elif shape == "octagon":
points = []
for i in range(8):
angle = i * 45 * math.pi / 180
px = x + base_size * math.cos(angle)
py = y + base_size * math.sin(angle)
points.append(f"{px},{py}")
return f"""
<polygon points="{' '.join(points)}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
else:
# Default to circle
return f"""
<circle cx="{x}" cy="{y}" r="{base_size}"
fill="{color}" stroke="white" stroke-width="3"
{glow} opacity="0.9"/>
"""
def serve_locations_data(self, locations_data, player_locations=None):
"""Serve locations page with real data using unified template"""
# Build locations HTML
@ -2003,12 +2287,17 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
</div>
</div>"""
# Create interactive map HTML
map_html = self.create_interactive_map(locations_data, player_locations)
content = f"""
<div class="header">
<h1>🗺 Game Locations</h1>
<p>Explore all areas and discover what pets await you!</p>
</div>
{map_html}
<div class="info-section">
<h2>🎯 How Locations Work</h2>
<p><strong>Travel:</strong> Use <code>!travel &lt;location&gt;</code> to move between areas</p>
@ -2070,6 +2359,82 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
# Add locations-specific CSS
additional_css = """
.map-section {
background: var(--bg-secondary);
border-radius: 15px;
padding: 30px;
margin: 30px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.map-section h2 {
color: var(--text-accent);
text-align: center;
margin-bottom: 10px;
}
.map-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.map-container svg {
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
max-width: 100%;
height: auto;
}
.map-legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-size: 0.9em;
}
.legend-shape {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid white;
}
.legend-shape.circle {
border-radius: 50%;
}
.legend-shape.hexagon {
border-radius: 3px;
transform: rotate(45deg);
}
.legend-shape.diamond {
transform: rotate(45deg);
}
.legend-shape.triangle {
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
.legend-shape.star {
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.legend-shape.octagon {
clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);
}
.locations-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
@ -2200,6 +2565,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
self.serve_error_page("Petdex", "Database not available")
return
# Parse URL parameters for sorting
parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
sort_mode = query_params.get('sort', ['rarity'])[0] # Default to rarity
search_query = query_params.get('search', [''])[0] # Default to empty search
# Fetch petdex data
try:
import asyncio
@ -2209,7 +2580,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
petdex_data = loop.run_until_complete(self.fetch_petdex_data(database))
loop.close()
self.serve_petdex_data(petdex_data)
self.serve_petdex_data(petdex_data, sort_mode, search_query)
except Exception as e:
print(f"Error fetching petdex data: {e}")
@ -2268,9 +2639,34 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
print(f"Database error fetching petdex: {e}")
return []
def serve_petdex_data(self, petdex_data):
def remove_pet_duplicates(self, pets_list):
"""Remove duplicate pets based on ID and sort by name"""
seen_ids = set()
unique_pets = []
for pet in pets_list:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
return sorted(unique_pets, key=lambda x: x['name'])
def serve_petdex_data(self, petdex_data, sort_mode='rarity', search_query=''):
"""Serve petdex page with all pet species data"""
# Remove duplicates from input data first
petdex_data = self.remove_pet_duplicates(petdex_data)
# Apply search filter if provided
if search_query:
search_query = search_query.lower()
filtered_data = []
for pet in petdex_data:
# Search in name, type1, type2
if (search_query in pet['name'].lower() or
search_query in pet['type1'].lower() or
(pet['type2'] and search_query in pet['type2'].lower())):
filtered_data.append(pet)
petdex_data = filtered_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"}
@ -2309,20 +2705,395 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
</div>
"""
pets_by_rarity = {}
for pet in petdex_data:
rarity = pet['rarity']
if rarity not in pets_by_rarity:
pets_by_rarity[rarity] = []
pets_by_rarity[rarity].append(pet)
# Sort and group pets based on sort_mode
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")
if sort_mode == 'type':
# Group by type1
pets_by_type = {}
for pet in petdex_data:
pet_type = pet['type1']
if pet_type not in pets_by_type:
pets_by_type[pet_type] = []
# Check for duplicates within this type
if pet not in pets_by_type[pet_type]:
pets_by_type[pet_type].append(pet)
# Sort each type group by name and remove any remaining duplicates
for type_name in pets_by_type:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_type[type_name]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_type[type_name] = sorted(unique_pets, key=lambda x: x['name'])
type_colors = {
'Fire': '#F08030', 'Water': '#6890F0', 'Grass': '#78C850', 'Electric': '#F8D030',
'Psychic': '#F85888', 'Ice': '#98D8D8', 'Dragon': '#7038F8', 'Dark': '#705848',
'Fighting': '#C03028', 'Poison': '#A040A0', 'Ground': '#E0C068', 'Flying': '#A890F0',
'Bug': '#A8B820', 'Rock': '#B8A038', 'Ghost': '#705898', 'Steel': '#B8B8D0',
'Normal': '#A8A878', 'Fairy': '#EE99AC'
}
for type_name in sorted(pets_by_type.keys()):
pets_in_type = pets_by_type[type_name]
type_color = type_colors.get(type_name, '#A8A878')
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: {type_color}; border-bottom: 2px solid {type_color}; padding-bottom: 10px;">
{type_name} Type ({len(pets_in_type)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_in_type:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {type_color};">
<div class="pet-header">
<h3 style="color: {type_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-rarity">
<span style="color: {rarity_colors.get(pet['rarity'], '#ffffff')};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'name':
# Sort alphabetically by name (duplicates already removed)
sorted_pets = sorted(petdex_data, key=lambda x: x['name'])
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: var(--text-accent); border-bottom: 2px solid var(--text-accent); padding-bottom: 10px;">
All Species (A-Z) ({len(sorted_pets)} total)
</h2>
<div class="pets-grid">"""
for pet in sorted_pets:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {rarity_color};">
<div class="pet-header">
<h3 style="color: {rarity_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-rarity">
<span style="color: {rarity_color};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'location':
# Group by spawn locations
pets_by_location = {}
pets_no_location = []
for pet in petdex_data:
if pet['spawn_locations']:
for location in pet['spawn_locations']:
loc_name = location['location_name']
if loc_name not in pets_by_location:
pets_by_location[loc_name] = []
# Check for duplicates within this location
if pet not in pets_by_location[loc_name]:
pets_by_location[loc_name].append(pet)
else:
pets_no_location.append(pet)
# Sort each location group by name and remove any remaining duplicates
for location_name in pets_by_location:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_location[location_name]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_location[location_name] = sorted(unique_pets, key=lambda x: x['name'])
location_colors = {
'Starter Town': '#4CAF50',
'Whispering Woods': '#2E7D32',
'Thunder Peaks': '#FF9800',
'Stone Caverns': '#795548',
'Frozen Lake': '#2196F3',
'Volcanic Crater': '#F44336'
}
for location_name in sorted(pets_by_location.keys()):
pets_in_location = pets_by_location[location_name]
location_color = location_colors.get(location_name, '#A8A878')
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: {location_color}; border-bottom: 2px solid {location_color}; padding-bottom: 10px;">
🗺 {location_name} ({len(pets_in_location)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_in_location:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
# Get level range for this location
level_range = ""
for location in pet['spawn_locations']:
if location['location_name'] == location_name:
level_range = f"Lv.{location['min_level']}-{location['max_level']}"
break
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {location_color};">
<div class="pet-header">
<h3 style="color: {rarity_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: {location_color};">
📍 {level_range} | {'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
# Add pets with no location at the end (remove duplicates)
if pets_no_location:
seen_ids = set()
unique_no_location = []
for pet in pets_no_location:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_no_location.append(pet)
pets_no_location = sorted(unique_no_location, key=lambda x: x['name'])
if pets_no_location:
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: #888; border-bottom: 2px solid #888; padding-bottom: 10px;">
Unknown Locations ({len(pets_no_location)} species)
</h2>
<div class="pets-grid">"""
for pet in pets_no_location:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid #888;">
<div class="pet-header">
<h3 style="color: {rarity_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: #888;">
Location Unknown | {'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
elif sort_mode == 'all':
# Show all pets in a grid format without grouping (duplicates already removed)
sorted_pets = sorted(petdex_data, key=lambda x: (x['rarity'], x['name']))
petdex_html += f"""
<div class="rarity-section">
<h2 style="color: var(--text-accent); border-bottom: 2px solid var(--text-accent); padding-bottom: 10px;">
📋 All Pet Species ({len(sorted_pets)} total)
</h2>
<div class="pets-grid">"""
for pet in sorted_pets:
type_str = pet['type1']
if pet['type2']:
type_str += f" / {pet['type2']}"
rarity_color = rarity_colors.get(pet['rarity'], '#ffffff')
# Get all spawn locations
location_text = ""
if pet['spawn_locations']:
locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})"
for loc in pet['spawn_locations'][:2]]
if len(pet['spawn_locations']) > 2:
locations.append(f"+{len(pet['spawn_locations']) - 2} more")
location_text = f"📍 {', '.join(locations)}"
else:
location_text = "📍 Location Unknown"
petdex_html += f"""
<div class="pet-card" style="border-left: 4px solid {rarity_color};">
<div class="pet-header">
<h3 style="color: {rarity_color};">{pet.get('emoji', '🐾')} {pet['name']}</h3>
<span class="type-badge">{type_str}</span>
</div>
<div class="pet-stats">
<div class="stat-item">
<span class="stat-label">HP:</span>
<span class="stat-value">{pet['base_hp']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Attack:</span>
<span class="stat-value">{pet['base_attack']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Defense:</span>
<span class="stat-value">{pet['base_defense']}</span>
</div>
<div class="stat-item">
<span class="stat-label">Speed:</span>
<span class="stat-value">{pet['base_speed']}</span>
</div>
</div>
<div class="pet-location">
<span style="color: var(--text-secondary); font-size: 0.9em;">
{location_text}
</span>
</div>
<div class="pet-rarity">
<span style="color: {rarity_color};">
{'' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")}
</span>
</div>
</div>"""
petdex_html += """
</div>
</div>"""
else: # Default to rarity sorting
pets_by_rarity = {}
for pet in petdex_data:
rarity = pet['rarity']
if rarity not in pets_by_rarity:
pets_by_rarity[rarity] = []
# Check for duplicates within this rarity
if pet not in pets_by_rarity[rarity]:
pets_by_rarity[rarity].append(pet)
# Sort each rarity group by name and remove any remaining duplicates
for rarity in pets_by_rarity:
# Remove duplicates based on pet ID and sort by name
seen_ids = set()
unique_pets = []
for pet in pets_by_rarity[rarity]:
if pet['id'] not in seen_ids:
seen_ids.add(pet['id'])
unique_pets.append(pet)
pets_by_rarity[rarity] = sorted(unique_pets, key=lambda x: x['name'])
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">
@ -2391,6 +3162,45 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
<p>The petdex appears to be empty. Contact an administrator.</p>
</div>"""
# Create search interface
search_interface = f"""
<div class="card" id="search">
<h2>🔍 Search & Filter</h2>
<form method="GET" action="/petdex" style="margin-bottom: 20px;">
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
<input type="text" name="search" placeholder="Search pets by name or type..."
value="{search_query}" style="flex: 1; min-width: 200px; padding: 8px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary);">
<select name="sort" style="padding: 8px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary);">
<option value="rarity"{'selected' if sort_mode == 'rarity' else ''}>🌟 By Rarity</option>
<option value="type"{'selected' if sort_mode == 'type' else ''}>🔷 By Type</option>
<option value="name"{'selected' if sort_mode == 'name' else ''}>🔤 By Name</option>
<option value="location"{'selected' if sort_mode == 'location' else ''}>🗺 By Location</option>
<option value="all"{'selected' if sort_mode == 'all' else ''}>📋 Show All</option>
</select>
<button type="submit" style="padding: 8px 16px; background: var(--accent-blue); color: white; border: none; border-radius: 5px; cursor: pointer;">Search</button>
</div>
</form>
{f'<p style="color: var(--text-accent);">🔍 Found {len(petdex_data)} pets matching "{search_query}"</p>' if search_query else ''}
</div>
"""
# Determine header text based on sort mode
if sort_mode == 'type':
header_text = "📊 Pet Collection by Type"
description = "🔷 Pets are organized by their primary type. Each type has different strengths and weaknesses!"
elif sort_mode == 'name':
header_text = "📊 Pet Collection (A-Z)"
description = "🔤 All pets sorted alphabetically by name. Perfect for finding specific species!"
elif sort_mode == 'location':
header_text = "📊 Pet Collection by Location"
description = "🗺️ Pets are organized by where they can be found. Use <code>!travel &lt;location&gt;</code> to visit these areas!"
elif sort_mode == 'all':
header_text = "📊 Complete Pet Collection"
description = "📋 All pets displayed in a comprehensive grid view with locations and stats!"
else:
header_text = "📊 Pet Collection by Rarity"
description = "🌟 Pets are organized by rarity. Use <code>!wild &lt;location&gt;</code> in #petz to see what spawns where!"
# Combine all content
content = f"""
<div class="header">
@ -2400,9 +3210,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
{stats_content}
{search_interface}
<div class="card">
<h2>📊 Pet Collection by Rarity</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">🎯 Pets are organized by rarity. Use <code>!wild &lt;location&gt;</code> in #petz to see what spawns where!</p>
<h2>{header_text}</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">{description}</p>
{petdex_html}
</div>