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:
parent
8ae7da8379
commit
530134bd36
2 changed files with 856 additions and 35 deletions
|
|
@ -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 = {}
|
||||
|
|
|
|||
878
webserver.py
878
webserver.py
|
|
@ -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 <location></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 <location></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 <location></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 <location></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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue