#!/usr/bin/env python3 """ PetBot Web Server Provides web interface for bot data including help, player stats, and pet collections """ import os import sys import asyncio from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs from threading import Thread import time # Add the project directory to the path sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.database import Database class PetBotRequestHandler(BaseHTTPRequestHandler): """HTTP request handler for PetBot web server""" @property def database(self): """Get database instance from server""" return self.server.database @property def bot(self): """Get bot instance from server""" return getattr(self.server, 'bot', None) def send_json_response(self, data, status_code=200): """Send a JSON response""" import json self.send_response(status_code) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(data).encode()) def get_unified_css(self): """Return unified CSS theme for all pages""" return """ :root { --bg-primary: #0f0f23; --bg-secondary: #1e1e3f; --bg-tertiary: #2a2a4a; --text-primary: #cccccc; --text-secondary: #aaaaaa; --text-accent: #66ff66; --accent-blue: #4dabf7; --accent-purple: #845ec2; --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --gradient-secondary: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); --border-color: #444466; --hover-color: #3a3a5a; --shadow-color: rgba(0, 0, 0, 0.3); --success-color: #51cf66; --warning-color: #ffd43b; --error-color: #ff6b6b; } * { box-sizing: border-box; } body { font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif; max-width: 1200px; margin: 0 auto; padding: 0; background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; min-height: 100vh; } .main-container { padding: 20px; min-height: calc(100vh - 80px); } /* Navigation Bar */ .navbar { background: var(--gradient-primary); padding: 15px 20px; box-shadow: 0 2px 10px var(--shadow-color); margin-bottom: 0; } .nav-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; } .nav-brand { font-size: 1.5em; font-weight: bold; color: white; text-decoration: none; display: flex; align-items: center; gap: 10px; } .nav-links { display: flex; gap: 20px; align-items: center; } .nav-link { color: white; text-decoration: none; padding: 8px 16px; border-radius: 20px; transition: all 0.3s ease; font-weight: 500; } .nav-link:hover { background: rgba(255, 255, 255, 0.2); transform: translateY(-1px); } .nav-link.active { background: rgba(255, 255, 255, 0.3); } /* Dropdown Navigation */ .nav-dropdown { position: relative; display: inline-block; } .dropdown-arrow { font-size: 0.8em; margin-left: 5px; transition: transform 0.3s ease; } .nav-dropdown:hover .dropdown-arrow { transform: rotate(180deg); } .dropdown-content { display: none; position: absolute; top: 100%; left: 0; background: var(--bg-secondary); min-width: 180px; box-shadow: 0 8px 16px var(--shadow-color); border-radius: 8px; z-index: 1000; border: 1px solid var(--border-color); overflow: hidden; margin-top: 5px; } .nav-dropdown:hover .dropdown-content { display: block; animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .dropdown-item { color: var(--text-primary); padding: 12px 16px; text-decoration: none; display: block; transition: background-color 0.3s ease; border-bottom: 1px solid var(--border-color); } .dropdown-item:last-child { border-bottom: none; } .dropdown-item:hover { background: var(--bg-tertiary); color: var(--text-accent); } @media (max-width: 768px) { .nav-content { flex-direction: column; gap: 15px; } .nav-links { flex-wrap: wrap; justify-content: center; gap: 10px; } .dropdown-content { position: static; display: none; width: 100%; box-shadow: none; border: none; border-radius: 0; background: var(--bg-tertiary); margin-top: 0; } .nav-dropdown:hover .dropdown-content { display: block; } } /* Header styling */ .header { text-align: center; background: var(--gradient-primary); color: white; padding: 40px 20px; border-radius: 15px; margin-bottom: 30px; box-shadow: 0 5px 20px var(--shadow-color); } .header h1 { margin: 0 0 10px 0; font-size: 2.5em; font-weight: bold; } .header p { margin: 0; font-size: 1.1em; opacity: 0.9; } /* Card styling */ .card { background: var(--bg-secondary); border-radius: 15px; padding: 20px; margin-bottom: 20px; box-shadow: 0 4px 15px var(--shadow-color); border: 1px solid var(--border-color); transition: transform 0.3s ease, box-shadow 0.3s ease; } .card:hover { transform: translateY(-2px); box-shadow: 0 6px 25px var(--shadow-color); } .card h2 { margin-top: 0; color: var(--text-accent); border-bottom: 2px solid var(--text-accent); padding-bottom: 10px; } .card h3 { color: var(--accent-blue); margin-top: 25px; } /* Grid layouts */ .grid { display: grid; gap: 20px; margin-bottom: 30px; } .grid-2 { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); } .grid-3 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); } .grid-4 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); } /* Buttons */ .btn { display: inline-block; padding: 10px 20px; border: none; border-radius: 25px; text-decoration: none; font-weight: 500; text-align: center; cursor: pointer; transition: all 0.3s ease; margin: 5px; } .btn-primary { background: var(--gradient-primary); color: white; } .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); } .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px var(--shadow-color); } /* Badges and tags */ .badge { display: inline-block; padding: 4px 12px; border-radius: 15px; font-size: 0.85em; font-weight: 500; margin: 2px; } .badge-primary { background: var(--accent-blue); color: white; } .badge-secondary { background: var(--bg-tertiary); color: var(--text-primary); } .badge-success { background: var(--success-color); color: white; } .badge-warning { background: var(--warning-color); color: #333; } .badge-error { background: var(--error-color); color: white; } /* Tables */ .table { width: 100%; border-collapse: collapse; margin: 20px 0; background: var(--bg-secondary); border-radius: 10px; overflow: hidden; } .table th, .table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border-color); } .table th { background: var(--bg-tertiary); font-weight: bold; color: var(--text-accent); } .table tr:hover { background: var(--bg-tertiary); } /* Loading and status messages */ .loading { text-align: center; padding: 40px; color: var(--text-secondary); } .error-message { background: var(--error-color); color: white; padding: 15px; border-radius: 10px; margin: 20px 0; } .success-message { background: var(--success-color); color: white; padding: 15px; border-radius: 10px; margin: 20px 0; } /* Pet-specific styles for petdex */ .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: 0 4px 15px var(--shadow-color); 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 20px; display: flex; justify-content: space-between; align-items: center; } .pet-header h3 { margin: 0; font-size: 1.3em; } .type-badge { background: var(--gradient-primary); color: white; padding: 4px 12px; border-radius: 15px; font-size: 0.85em; font-weight: 500; } .pet-stats { padding: 15px 20px; background: var(--bg-secondary); } .stat-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 0.9em; } .total-stats { text-align: center; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-color); font-weight: 600; color: var(--text-accent); } .pet-info { padding: 15px 20px; background: var(--bg-tertiary); font-size: 0.9em; line-height: 1.5; } .rarity-section { margin-bottom: 40px; } /* Responsive design */ @media (max-width: 768px) { .main-container { padding: 10px; } .header h1 { font-size: 2em; } .card { padding: 15px; } .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } } """ def get_navigation_bar(self, current_page=""): """Return unified navigation bar HTML with dropdown menus""" # Define navigation structure with dropdowns nav_structure = [ ("", "๐Ÿ  Home", []), ("players", "๐Ÿ‘ฅ Players", [ ("leaderboard", "๐Ÿ† Leaderboard"), ("players", "๐Ÿ“Š Statistics") ]), ("locations", "๐Ÿ—บ๏ธ Locations", [ ("locations", "๐ŸŒค๏ธ Weather"), ("locations", "๐ŸŽฏ Spawns"), ("locations", "๐Ÿ›๏ธ Gyms") ]), ("petdex", "๐Ÿ“š Petdex", [ ("petdex", "๐Ÿ”ท by Type"), ("petdex", "โญ by Rarity"), ("petdex", "๐Ÿ” Search") ]), ("help", "๐Ÿ“– Help", [ ("help", "โšก Commands"), ("help", "๐Ÿ“– Web Guide"), ("help", "โ“ FAQ") ]) ] nav_links = "" for page_path, page_name, subpages in nav_structure: active_class = " active" if current_page == page_path else "" href = f"/{page_path}" if page_path else "/" if subpages: # Create dropdown menu dropdown_items = "" for sub_path, sub_name in subpages: sub_href = f"/{sub_path}" if sub_path else "/" dropdown_items += f'{sub_name}' nav_links += f''' ''' else: # Regular nav link nav_links += f'{page_name}' return f""" """ def get_page_template(self, title, content, current_page=""): """Return complete page HTML with unified theme""" return f""" {title} - PetBot {self.get_navigation_bar(current_page)}
{content}
""" def do_GET(self): """Handle GET requests""" parsed_path = urlparse(self.path) path = parsed_path.path # Route handling if path == '/': self.serve_index() elif path == '/help': self.serve_help() elif path == '/players': self.serve_players() elif path.startswith('/player/'): nickname = path[8:] # Remove '/player/' prefix self.serve_player_profile(nickname) elif path == '/leaderboard': self.serve_leaderboard() elif path == '/locations': self.serve_locations() elif path == '/petdex': self.serve_petdex() elif path.startswith('/teambuilder/'): nickname = path[13:] # Remove '/teambuilder/' prefix self.serve_teambuilder(nickname) else: self.send_error(404, "Page not found") def do_POST(self): """Handle POST requests""" parsed_path = urlparse(self.path) path = parsed_path.path if path.startswith('/teambuilder/') and path.endswith('/save'): nickname = path[13:-5] # Remove '/teambuilder/' prefix and '/save' suffix self.handle_team_save(nickname) elif path.startswith('/teambuilder/') and path.endswith('/verify'): nickname = path[13:-7] # Remove '/teambuilder/' prefix and '/verify' suffix self.handle_team_verify(nickname) else: self.send_error(404, "Page not found") def serve_index(self): """Serve the main index page""" content = """

๐Ÿพ PetBot Game Hub

Welcome to the PetBot web interface!

Connect to irc.libera.chat #petz to play

๐Ÿ“š Command Help

Complete reference for all bot commands, battle mechanics, and game features

View Help

๐Ÿ‘ฅ Player List

View all registered players and their basic stats

Browse Players

๐Ÿ† Leaderboard

Top players by level, pets caught, and achievements earned

View Rankings

๐Ÿ—บ๏ธ Locations

Explore all game locations and see what pets can be found where

Explore World

๐Ÿ“– Petdex

Complete encyclopedia of all available pets with stats, types, and evolution info

Browse Petdex

๐Ÿค– Bot Status

Online and ready for commands!

Use !help in #petz for quick command reference

""" html = self.get_page_template("PetBot Game Hub", content, "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def serve_help(self): """Serve the help page using unified template""" content = """

๐Ÿ“š PetBot Commands

Complete guide to Pokemon-style pet collecting in IRC

๐Ÿš€ Getting Started
!start
Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.
Example: !start
!help
Get a link to this comprehensive command reference page.
Example: !help
!stats
View your basic trainer information including level, experience, and money.
Example: !stats
๐ŸŒ Exploration & Travel
!explore
Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!
Example: !explore
!travel <location>
Move to a different location. Each area has unique pets and gyms. Some locations require achievements to unlock.
Example: !travel whispering woods
!weather
Check the current weather effects in your location. Weather affects which pet types spawn more frequently.
Example: !weather
!where / !location
See which location you're currently in and get information about the area.
Example: !where

๐Ÿ—บ๏ธ Available Locations

  • Starter Town - Peaceful starting area (Fire/Water/Grass pets)
  • Whispering Woods - Ancient forest (Grass pets + new species: Vinewrap, Bloomtail)
  • Electric Canyon - Charged valley (Electric/Rock pets)
  • Crystal Caves - Underground caverns (Rock/Crystal pets)
  • Frozen Tundra - Icy wasteland (Ice/Water pets)
  • Dragon's Peak - Ultimate challenge (Fire/Rock/Ice pets)

๐ŸŒค๏ธ Weather Effects

  • Sunny - 1.5x Fire/Grass spawns (1-2 hours)
  • Rainy - 2.0x Water spawns (45-90 minutes)
  • Thunderstorm - 2.0x Electric spawns (30-60 minutes)
  • Blizzard - 1.7x Ice/Water spawns (1-2 hours)
  • Earthquake - 1.8x Rock spawns (30-90 minutes)
  • Calm - Normal spawns (1.5-3 hours)
โš”๏ธ Battle System
!catch / !capture
Attempt to catch a wild pet that appeared during exploration. Success depends on the pet's level and rarity.
Example: !catch
!battle
Start a turn-based battle with a wild pet. Defeat it to gain experience and money for your active pet.
Example: !battle
!attack <move>
Use a specific move during battle. Each move has different power, type, and effects.
Example: !attack flamethrower
!moves
View all available moves for your active pet, including their types and power levels.
Example: !moves
!flee
Attempt to escape from the current battle. Not always successful!
Example: !flee
๐Ÿ›๏ธ Gym Battles NEW!
!gym
List all gyms in your current location with your progress. Shows victories and next difficulty level.
Example: !gym
!gym list
Show all gyms across all locations with your badge collection progress.
Example: !gym list
!gym challenge "<name>"
Challenge a gym leader! You must be in the same location as the gym. Difficulty increases with each victory.
Example: !gym challenge "Forest Guardian"
!gym info "<name>"
Get detailed information about a gym including leader, theme, team, and badge details.
Example: !gym info "Storm Master"
๐Ÿ’ก Gym Strategy: Each gym specializes in a specific type. Bring pets with type advantages! The more you beat a gym, the harder it gets, but the better the rewards!

๐Ÿ† Gym Leaders & Badges

๐Ÿƒ Forest Guardian
Location: Starter Town
Leader: Trainer Verde
Theme: Grass-type
๐ŸŒณ Nature's Haven
Location: Whispering Woods
Leader: Elder Sage
Theme: Grass-type
โšก Storm Master
Location: Electric Canyon
Leader: Captain Volt
Theme: Electric-type
๐Ÿ’Ž Stone Crusher
Location: Crystal Caves
Leader: Miner Magnus
Theme: Rock-type
โ„๏ธ Ice Breaker
Location: Frozen Tundra
Leader: Arctic Queen
Theme: Ice/Water-type
๐Ÿ‰ Dragon Slayer
Location: Dragon's Peak
Leader: Champion Drake
Theme: Fire-type
๐Ÿพ Pet Management
!team
View your active team of pets with their levels, HP, and status.
Example: !team
!pets
View your complete pet collection with detailed stats and information via web interface.
Example: !pets
!activate <pet>
Add a pet to your active battle team. You can have multiple active pets for different situations.
Example: !activate flamey
!deactivate <pet>
Remove a pet from your active team and put it in storage.
Example: !deactivate aqua
๐ŸŽ’ Inventory System NEW!
!inventory / !inv / !items
View all items in your inventory organized by category. Shows quantities and item descriptions.
Example: !inventory
!use <item name>
Use a consumable item from your inventory. Items can heal pets, boost stats, or provide other benefits.
Example: !use Small Potion

๐ŸŽฏ Item Categories & Rarities

  • โ—‹ Common (15%) - Small Potions, basic healing items
  • โ—‡ Uncommon (8-12%) - Large Potions, battle boosters, special berries
  • โ—† Rare (3-6%) - Super Potions, speed elixirs, location treasures
  • โ˜… Epic (2-3%) - Evolution stones, rare crystals, ancient artifacts
  • โœฆ Legendary (1%) - Lucky charms, ancient fossils, ultimate items
๐Ÿ’ก Item Discovery: Find items while exploring! Each location has unique treasures. Items stack in your inventory and can be used anytime.
๐Ÿ† Achievements & Progress
!achievements
View your achievement progress and see which new locations you've unlocked.
Example: !achievements

๐ŸŽฏ Location Unlock Requirements

  • Pet Collector (5 pets) โ†’ Unlocks Whispering Woods
  • Spark Collector (2 Electric species) โ†’ Unlocks Electric Canyon
  • Rock Hound (3 Rock species) โ†’ Unlocks Crystal Caves
  • Ice Breaker (5 Water/Ice species) โ†’ Unlocks Frozen Tundra
  • Dragon Tamer (15 pets + 3 Fire species) โ†’ Unlocks Dragon's Peak
๐ŸŒ Web Interface
Access detailed information through the web dashboard at http://petz.rdx4.com/
  • Player Profiles - Complete stats, pet collections, and inventories
  • Leaderboard - Top players by level and achievements
  • Locations Guide - All areas with spawn information
  • Gym Badges - Display your earned badges and progress
""" # Add command-specific CSS to the unified styles additional_css = """ .section { background: var(--bg-secondary); border-radius: 15px; margin-bottom: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); border: 1px solid var(--border-color); overflow: hidden; } .section-header { background: var(--gradient-primary); color: white; padding: 20px 25px; font-size: 1.3em; font-weight: 700; } .section-content { padding: 25px; } .command-grid { display: grid; gap: 20px; } .command { border: 1px solid var(--border-color); border-radius: 12px; overflow: hidden; transition: all 0.3s ease; background: var(--bg-tertiary); } .command:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15); border-color: var(--text-accent); } .command-name { background: var(--bg-primary); padding: 15px 20px; font-family: 'Fira Code', 'Courier New', monospace; font-weight: bold; color: var(--text-accent); border-bottom: 1px solid var(--border-color); font-size: 1.2em; text-shadow: 0 0 10px rgba(102, 255, 102, 0.3); } .command-desc { padding: 20px; line-height: 1.7; color: var(--text-primary); } .command-example { background: var(--bg-primary); padding: 12px 20px; font-family: 'Fira Code', 'Courier New', monospace; color: var(--text-secondary); border-top: 1px solid var(--border-color); font-size: 0.95em; } .info-box { background: var(--bg-tertiary); padding: 20px; border-radius: 12px; margin: 20px 0; border: 1px solid var(--border-color); } .info-box h4 { margin: 0 0 15px 0; color: var(--text-accent); font-size: 1.1em; font-weight: 600; } .info-box ul { margin: 0; padding-left: 25px; } .info-box li { margin: 8px 0; color: var(--text-primary); } .info-box strong { color: var(--text-accent); } .footer { text-align: center; margin-top: 50px; padding: 30px; background: var(--bg-secondary); border-radius: 15px; color: var(--text-secondary); box-shadow: 0 4px 20px rgba(0,0,0,0.3); border: 1px solid var(--border-color); } .tip { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin: 20px 0; font-weight: 500; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .gym-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; margin: 15px 0; } .gym-card { background: var(--bg-primary); padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); } .gym-card strong { color: var(--text-accent); } """ # Get the unified template with additional CSS html_content = self.get_page_template("Command Help", content, "help") # Insert additional CSS before closing tag html_content = html_content.replace("", additional_css + "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def serve_players(self): """Serve the players page with real data""" # Get database instance from the server class database = self.server.database if hasattr(self.server, 'database') else None if not database: self.serve_error_page("Players", "Database not available") return # Fetch players data try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) players_data = loop.run_until_complete(self.fetch_players_data(database)) loop.close() self.serve_players_data(players_data) except Exception as e: print(f"Error fetching players data: {e}") self.serve_error_page("Players", f"Error loading players: {str(e)}") async def fetch_players_data(self, database): """Fetch all players data from database""" try: import aiosqlite async with aiosqlite.connect(database.db_path) as db: # Get all players with basic stats cursor = await db.execute(""" SELECT p.nickname, p.level, p.experience, p.money, p.created_at, l.name as location_name, (SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count, (SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets, (SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count FROM players p LEFT JOIN locations l ON p.current_location_id = l.id ORDER BY p.level DESC, p.experience DESC """) rows = await cursor.fetchall() # Convert SQLite rows to dictionaries properly players = [] for row in rows: player_dict = { 'nickname': row[0], 'level': row[1], 'experience': row[2], 'money': row[3], 'created_at': row[4], 'location_name': row[5], 'pet_count': row[6], 'active_pets': row[7], 'achievement_count': row[8] } players.append(player_dict) return players except Exception as e: print(f"Database error fetching players: {e}") return [] def serve_players_data(self, players_data): """Serve players page with real data""" # Calculate statistics total_players = len(players_data) total_pets = sum(p['pet_count'] for p in players_data) if players_data else 0 total_achievements = sum(p['achievement_count'] for p in players_data) if players_data else 0 highest_level = max((p['level'] for p in players_data), default=0) if players_data else 0 # Build statistics cards stats_content = f"""

๐Ÿ“Š Total Players

{total_players}

๐Ÿพ Total Pets

{total_pets}

๐Ÿ† Achievements

{total_achievements}

โญ Highest Level

{highest_level}
""" # Build players table HTML if players_data: players_html = "" for i, player in enumerate(players_data, 1): rank_emoji = {"1": "๐Ÿฅ‡", "2": "๐Ÿฅˆ", "3": "๐Ÿฅ‰"}.get(str(i), f"{i}.") players_html += f""" {rank_emoji} {player['nickname']} {player['level']} {player['experience']} ${player['money']} {player['pet_count']} {player['active_pets']} {player['achievement_count']} {player.get('location_name', 'Unknown')} """ else: players_html = """ No players found. Be the first to use !start in #petz! """ # Build table content table_content = f"""

๐Ÿ† Player Rankings

{players_html}
Rank Player Level Experience Money Pets Active Achievements Location

๐Ÿ’ก Click on any player name to view their detailed profile

""" # Combine all content content = f"""

๐Ÿ‘ฅ Registered Players

All trainers on their pet collection journey

{stats_content} {table_content} """ html = self.get_page_template("Players", content, "players") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def serve_error_page(self, page_name, error_msg): """Serve a generic error page using unified template""" content = f"""

โš ๏ธ Error Loading {page_name}

Unable to load page

{error_msg}

Please try again later or contact an administrator.

""" # Add error-specific CSS additional_css = """ .main-container { text-align: center; max-width: 800px; margin: 0 auto; } .error-message { background: var(--bg-secondary); padding: 40px; border-radius: 15px; border: 2px solid var(--error-color); margin-top: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); } .error-message h2 { color: var(--error-color); margin-top: 0; } """ html_content = self.get_page_template("Error", content, "") html_content = html_content.replace("", additional_css + "") self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def serve_leaderboard(self): """Serve the leaderboard page - redirect to players for now""" # For now, leaderboard is the same as players page since they're ranked # In the future, this could have different categories self.send_response(302) # Temporary redirect self.send_header('Location', '/players') self.end_headers() def serve_locations(self): """Serve the locations page with real data""" # Get database instance from the server class database = self.server.database if hasattr(self.server, 'database') else None if not database: self.serve_error_page("Locations", "Database not available") return # Fetch locations data try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) locations_data = loop.run_until_complete(self.fetch_locations_data(database)) loop.close() self.serve_locations_data(locations_data) except Exception as e: print(f"Error fetching locations data: {e}") self.serve_error_page("Locations", f"Error loading locations: {str(e)}") async def fetch_locations_data(self, database): """Fetch all locations and their spawn data from database""" try: import aiosqlite async with aiosqlite.connect(database.db_path) as db: # Get all locations cursor = await db.execute(""" SELECT l.*, GROUP_CONCAT(DISTINCT ps.name || ' (' || ps.type1 || CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns FROM locations l LEFT JOIN location_spawns ls ON l.id = ls.location_id LEFT JOIN pet_species ps ON ls.species_id = ps.id GROUP BY l.id ORDER BY l.id """) rows = await cursor.fetchall() # Convert SQLite rows to dictionaries properly locations = [] for row in rows: location_dict = { 'id': row[0], 'name': row[1], 'description': row[2], 'level_min': row[3], 'level_max': row[4], 'spawns': row[5] if len(row) > 5 else None } locations.append(location_dict) return locations except Exception as e: print(f"Database error fetching locations: {e}") return [] def serve_locations_data(self, locations_data): """Serve locations page with real data using unified template""" # Build locations HTML locations_html = "" if locations_data: for location in locations_data: spawns = location.get('spawns', 'No pets found') if not spawns or spawns == 'None': spawns = "No pets spawn here yet" # Split spawns into a readable list and remove duplicates if spawns != "No pets spawn here yet": spawn_list = list(set([spawn.strip() for spawn in spawns.split(',') if spawn.strip()])) spawn_list.sort() # Sort alphabetically for consistency else: spawn_list = [] spawn_badges = "" visible_spawns = spawn_list[:6] # Show first 6 hidden_spawns = spawn_list[6:] # Hide the rest # Add visible spawn badges for spawn in visible_spawns: spawn_badges += f'{spawn}' # Add hidden spawn badges (initially hidden) if hidden_spawns: location_id = location['id'] for spawn in hidden_spawns: spawn_badges += f'{spawn}' # Add functional "show more" button spawn_badges += f'+{len(hidden_spawns)} more' if not spawn_badges: spawn_badges = 'No pets spawn here yet' locations_html += f"""

๐Ÿ—บ๏ธ {location['name']}

ID: {location['id']}
{location['description']}
Level Range: {location['level_min']}-{location['level_max']}
Wild Pets:
{spawn_badges}
""" else: locations_html = """

No Locations Found

No game locations are configured yet.
""" content = f"""

๐Ÿ—บ๏ธ Game Locations

Explore all areas and discover what pets await you!

๐ŸŽฏ How Locations Work

Travel: Use !travel <location> to move between areas

Explore: Use !explore to find wild pets in your current location

Unlock: Some locations require achievements - catch specific pet types to unlock new areas!

Weather: Check !weather for conditions that boost certain pet spawn rates

{locations_html}

๐Ÿ’ก Use !wild <location> in #petz to see what pets spawn in a specific area

""" # Add locations-specific CSS additional_css = """ .locations-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 25px; margin-top: 30px; } .location-card { background: var(--bg-secondary); border-radius: 15px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.3); border: 1px solid var(--border-color); transition: transform 0.3s ease; } .location-card:hover { transform: translateY(-5px); } .location-header { background: var(--gradient-primary); color: white; padding: 20px; display: flex; justify-content: space-between; align-items: center; } .location-header h3 { margin: 0; font-size: 1.3em; font-weight: 700; } .location-id { background: rgba(255,255,255,0.2); padding: 5px 10px; border-radius: 20px; font-size: 0.8em; } .location-description { padding: 20px; color: var(--text-primary); font-style: italic; border-bottom: 1px solid var(--border-color); } .location-levels { padding: 20px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); } .location-spawns { padding: 20px; } .spawn-badge { background: var(--bg-tertiary); color: var(--text-accent); padding: 4px 10px; border-radius: 12px; font-size: 0.85em; margin: 3px; display: inline-block; border: 1px solid var(--border-color); } .hidden-spawn { display: none; } .more-button { background: var(--gradient-primary) !important; color: white !important; cursor: pointer; transition: transform 0.2s ease; } .more-button:hover { transform: scale(1.05); } .less-button { background: #ff6b6b !important; color: white !important; cursor: pointer; transition: transform 0.2s ease; } .less-button:hover { transform: scale(1.05); } .info-section { background: var(--bg-secondary); border-radius: 15px; padding: 25px; margin-bottom: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); border: 1px solid var(--border-color); } .info-section h2 { color: var(--text-accent); margin-top: 0; } """ # Get the unified template with additional CSS html_content = self.get_page_template("Game Locations", content, "locations") # Insert additional CSS before closing tag html_content = html_content.replace("", additional_css + "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.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 (no duplicates) cursor = await db.execute(""" SELECT DISTINCT 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"} # Calculate statistics total_species = len(petdex_data) type_counts = {} for pet in petdex_data: if pet['type1'] not in type_counts: type_counts[pet['type1']] = 0 type_counts[pet['type1']] += 1 if pet['type2'] and pet['type2'] not in type_counts: type_counts[pet['type2']] = 0 if pet['type2']: type_counts[pet['type2']] += 1 # Build statistics section stats_content = f"""

๐Ÿ“Š Total Species

{total_species}

๐ŸŽจ Types

{len(type_counts)}

โญ Rarities

{len(set(pet['rarity'] for pet in petdex_data))}

๐Ÿงฌ Evolutions

{len([p for p in petdex_data if p['evolution_level']])}
""" 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"""

{rarity_name} ({len(pets_in_rarity)} species)

""" 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"
Evolves: Level {pet['evolution_level']} โ†’ {pet['evolves_to_name']}" elif pet['evolution_level']: evolution_info = f"
Evolves: 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"
Found in: {', '.join(locations)}" else: spawn_info = "
Found in: Not yet available" # Calculate total base stats total_stats = pet['base_hp'] + pet['base_attack'] + pet['base_defense'] + pet['base_speed'] petdex_html += f"""

{pet['name']}

{type_str}
HP: {pet['base_hp']} ATK: {pet['base_attack']}
DEF: {pet['base_defense']} SPD: {pet['base_speed']}
Total: {total_stats}
Rarity: {rarity_name}{evolution_info}{spawn_info}
""" petdex_html += """
""" if not petdex_data: petdex_html = """

No pet species found!

The petdex appears to be empty. Contact an administrator.

""" # Combine all content content = f"""

๐Ÿ“– Petdex

Complete encyclopedia of all available pets

{stats_content}

๐Ÿ“Š Pet Collection by Rarity

๐ŸŽฏ Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!

{petdex_html}
""" html = self.get_page_template("Petdex", content, "petdex") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def serve_player_profile(self, nickname): """Serve individual player profile page""" # URL decode the nickname in case it has special characters from urllib.parse import unquote nickname = unquote(nickname) # Get database instance from the server class database = self.server.database if hasattr(self.server, 'database') else None if not database: self.serve_player_error(nickname, "Database not available") return # Fetch player data try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) player_data = loop.run_until_complete(self.fetch_player_data(database, nickname)) loop.close() if player_data is None: self.serve_player_not_found(nickname) return self.serve_player_data(nickname, player_data) except Exception as e: print(f"Error fetching player data for {nickname}: {e}") self.serve_player_error(nickname, f"Error loading player data: {str(e)}") return async def fetch_player_data(self, database, nickname): """Fetch all player data from database""" try: # Get player info import aiosqlite async with aiosqlite.connect(database.db_path) as db: # Get player basic info cursor = await db.execute(""" SELECT p.*, l.name as location_name, l.description as location_desc FROM players p LEFT JOIN locations l ON p.current_location_id = l.id WHERE p.nickname = ? """, (nickname,)) player = await cursor.fetchone() if not player: return None # Convert to dict manually player_dict = { 'id': player[0], 'nickname': player[1], 'created_at': player[2], 'last_active': player[3], 'level': player[4], 'experience': player[5], 'money': player[6], 'current_location_id': player[7], 'location_name': player[8], 'location_desc': player[9] } # Get player pets cursor = await db.execute(""" SELECT p.*, ps.name as species_name, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? ORDER BY p.is_active DESC, p.level DESC, p.id ASC """, (player_dict['id'],)) pets_rows = await cursor.fetchall() pets = [] for row in pets_rows: pet_dict = { 'id': row[0], 'player_id': row[1], 'species_id': row[2], 'nickname': row[3], 'level': row[4], 'experience': row[5], 'hp': row[6], 'max_hp': row[7], 'attack': row[8], 'defense': row[9], 'speed': row[10], 'happiness': row[11], 'caught_at': row[12], 'is_active': bool(row[13]), # Convert to proper boolean 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17] } pets.append(pet_dict) # Get player achievements cursor = await db.execute(""" SELECT pa.*, a.name as achievement_name, a.description as achievement_desc FROM player_achievements pa JOIN achievements a ON pa.achievement_id = a.id WHERE pa.player_id = ? ORDER BY pa.completed_at DESC """, (player_dict['id'],)) achievements_rows = await cursor.fetchall() achievements = [] for row in achievements_rows: achievement_dict = { 'id': row[0], 'player_id': row[1], 'achievement_id': row[2], 'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5] } achievements.append(achievement_dict) # Get player inventory cursor = await db.execute(""" SELECT i.name, i.description, i.category, i.rarity, pi.quantity FROM player_inventory pi JOIN items i ON pi.item_id = i.id WHERE pi.player_id = ? ORDER BY i.rarity DESC, i.name ASC """, (player_dict['id'],)) inventory_rows = await cursor.fetchall() inventory = [] for row in inventory_rows: item_dict = { 'name': row[0], 'description': row[1], 'category': row[2], 'rarity': row[3], 'quantity': row[4] } inventory.append(item_dict) # Get player gym badges cursor = await db.execute(""" SELECT g.name, g.badge_name, g.badge_icon, l.name as location_name, pgb.victories, pgb.first_victory_date, pgb.highest_difficulty FROM player_gym_battles pgb JOIN gyms g ON pgb.gym_id = g.id JOIN locations l ON g.location_id = l.id WHERE pgb.player_id = ? AND pgb.victories > 0 ORDER BY pgb.first_victory_date ASC """, (player_dict['id'],)) gym_badges_rows = await cursor.fetchall() gym_badges = [] for row in gym_badges_rows: badge_dict = { 'gym_name': row[0], 'badge_name': row[1], 'badge_icon': row[2], 'location_name': row[3], 'victories': row[4], 'first_victory_date': row[5], 'highest_difficulty': row[6] } gym_badges.append(badge_dict) # Get player encounters using database method encounters = [] try: # Use the existing database method which handles row factory properly temp_encounters = await database.get_player_encounters(player_dict['id']) for enc in temp_encounters: encounter_dict = { 'species_name': enc['species_name'], 'type1': enc['type1'], 'type2': enc['type2'], 'rarity': enc['rarity'], 'total_encounters': enc['total_encounters'], 'caught_count': enc['caught_count'], 'first_encounter_date': enc['first_encounter_date'] } encounters.append(encounter_dict) except Exception as e: print(f"Error fetching encounters: {e}") encounters = [] # Get encounter stats try: encounter_stats = await database.get_encounter_stats(player_dict['id']) except Exception as e: print(f"Error fetching encounter stats: {e}") encounter_stats = { 'species_encountered': 0, 'total_encounters': 0, 'total_species': 0, 'completion_percentage': 0.0 } return { 'player': player_dict, 'pets': pets, 'achievements': achievements, 'inventory': inventory, 'gym_badges': gym_badges, 'encounters': encounters, 'encounter_stats': encounter_stats } except Exception as e: print(f"Database error fetching player {nickname}: {e}") return None def serve_player_not_found(self, nickname): """Serve player not found page using unified template""" content = f"""

๐Ÿšซ Player Not Found

Player "{nickname}" not found

This player hasn't started their journey yet or doesn't exist.

Players can use !start in #petz to begin their adventure!

""" # Add error-specific CSS additional_css = """ .main-container { text-align: center; max-width: 800px; margin: 0 auto; } .error-message { background: var(--bg-secondary); padding: 40px; border-radius: 15px; border: 2px solid var(--error-color); margin-top: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); } .error-message h2 { color: var(--error-color); margin-top: 0; } """ html_content = self.get_page_template("Player Not Found", content, "players") html_content = html_content.replace("", additional_css + "") self.send_response(404) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def serve_player_error(self, nickname, error_msg): """Serve player error page using unified template""" content = f"""

โš ๏ธ Error

Unable to load player data

{error_msg}

Please try again later or contact an administrator.

""" # Add error-specific CSS additional_css = """ .main-container { text-align: center; max-width: 800px; margin: 0 auto; } .error-message { background: var(--bg-secondary); padding: 40px; border-radius: 15px; border: 2px solid var(--error-color); margin-top: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); } .error-message h2 { color: var(--error-color); margin-top: 0; } """ html_content = self.get_page_template("Player Error", content, "players") html_content = html_content.replace("", additional_css + "") self.send_response(500) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def serve_player_data(self, nickname, player_data): """Serve player profile page with real data""" player = player_data['player'] pets = player_data['pets'] achievements = player_data['achievements'] inventory = player_data.get('inventory', []) gym_badges = player_data.get('gym_badges', []) encounters = player_data.get('encounters', []) encounter_stats = player_data.get('encounter_stats', {}) # Calculate stats active_pets = [pet for pet in pets if pet['is_active']] total_pets = len(pets) active_count = len(active_pets) # Build pets table HTML pets_html = "" if pets: for pet in pets: status = "โญ Active" if pet['is_active'] else "๐Ÿ“ฆ Storage" status_class = "pet-active" if pet['is_active'] else "pet-stored" name = pet['nickname'] or pet['species_name'] type_str = pet['type1'] if pet['type2']: type_str += f"/{pet['type2']}" pets_html += f""" {status} {name} {pet['species_name']} {type_str} {pet['level']} {pet['hp']}/{pet['max_hp']} ATK: {pet['attack']} | DEF: {pet['defense']} | SPD: {pet['speed']} """ else: pets_html = """ No pets found. Use !explore and !catch to start your collection! """ # Build achievements HTML achievements_html = "" if achievements: for achievement in achievements: achievements_html += f"""
๐Ÿ†

{achievement['achievement_name']}

{achievement['achievement_desc']}

Earned: {achievement['completed_at']}
""" else: achievements_html = """
๐Ÿ†

No achievements yet

Keep exploring and catching pets to earn achievements!

""" # Build inventory HTML inventory_html = "" if inventory: rarity_symbols = { "common": "โ—‹", "uncommon": "โ—‡", "rare": "โ—†", "epic": "โ˜…", "legendary": "โœฆ" } rarity_colors = { "common": "#ffffff", "uncommon": "#1eff00", "rare": "#0070dd", "epic": "#a335ee", "legendary": "#ff8000" } for item in inventory: symbol = rarity_symbols.get(item['rarity'], "โ—‹") color = rarity_colors.get(item['rarity'], "#ffffff") quantity_str = f" x{item['quantity']}" if item['quantity'] > 1 else "" inventory_html += f"""
{symbol} {item['name']}{quantity_str}
{item['description']}
Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}
""" else: inventory_html = """
๐ŸŽ’

No items yet

Try exploring to find useful items!

""" # Build gym badges HTML badges_html = "" if gym_badges: for badge in gym_badges: # Safely handle date formatting try: if badge['first_victory_date'] and isinstance(badge['first_victory_date'], str): badge_date = badge['first_victory_date'].split()[0] else: badge_date = 'Unknown' except (AttributeError, IndexError): badge_date = 'Unknown' badges_html += f"""
{badge['badge_icon']}

{badge['badge_name']}

Earned from {badge['gym_name']} ({badge['location_name']})

First victory: {badge_date} Total victories: {badge['victories']} Highest difficulty: Level {badge['highest_difficulty']}
""" else: badges_html = """
๐Ÿ†

No gym badges yet

Challenge gyms to earn badges and prove your training skills!

""" # Build encounters HTML encounters_html = "" if encounters: rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"} for encounter in encounters: rarity_color = rarity_colors.get(encounter['rarity'], "#ffffff") type_str = encounter['type1'] if encounter['type2']: type_str += f"/{encounter['type2']}" # Safely handle date formatting try: if encounter['first_encounter_date'] and isinstance(encounter['first_encounter_date'], str): encounter_date = encounter['first_encounter_date'].split()[0] else: encounter_date = 'Unknown' except (AttributeError, IndexError): encounter_date = 'Unknown' encounters_html += f"""
{encounter['species_name']} {type_str}
Encountered {encounter['total_encounters']} times Caught {encounter['caught_count']} times
First seen: {encounter_date}
""" else: encounters_html = """
๐Ÿ‘๏ธ

No pets encountered yet

Use !explore to discover wild pets!

""" # Build content for the unified template content = f"""

๐Ÿพ {nickname}'s Profile

Level {player['level']} Trainer

Currently in {player.get('location_name', 'Unknown Location')}

๐Ÿ”ง Team Builder
๐Ÿ“Š Player Statistics
{player['level']}
Level
{player['experience']}
Experience
${player['money']}
Money
{total_pets}
Pets Caught
{active_count}
Active Pets
{len(achievements)}
Achievements
{encounter_stats.get('species_encountered', 0)}
Species Seen
{encounter_stats.get('completion_percentage', 0)}%
Petdex Complete
๐Ÿพ Pet Collection
{pets_html}
Status Name Species Type Level HP Stats
๐Ÿ† Achievements
{achievements_html}
๐ŸŽ’ Inventory
{inventory_html}
๐Ÿ† Gym Badges
{badges_html}
๐Ÿ‘๏ธ Pet Encounters

Species discovered: {encounter_stats.get('species_encountered', 0)}/{encounter_stats.get('total_species', 0)} ({encounter_stats.get('completion_percentage', 0)}% complete)

Total encounters: {encounter_stats.get('total_encounters', 0)}

{encounters_html}
""" # Add custom CSS for the profile page additional_css = """ /* Profile Page Styles */ html { scroll-behavior: smooth; } .quick-nav { background: var(--bg-secondary); padding: 15px; border-radius: 10px; margin-bottom: 20px; text-align: center; box-shadow: 0 2px 10px var(--shadow-color); } .nav-pill { display: inline-block; background: var(--bg-tertiary); color: var(--text-primary); padding: 8px 16px; border-radius: 20px; text-decoration: none; margin: 5px; font-size: 0.9em; transition: all 0.3s ease; border: 1px solid var(--border-color); } .nav-pill:hover { background: var(--gradient-primary); color: white; transform: translateY(-2px); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; } .stat-card { background: var(--bg-tertiary); padding: 20px; border-radius: 10px; text-align: center; border: 1px solid var(--border-color); transition: transform 0.3s ease; } .stat-card:hover { transform: translateY(-5px); } .stat-value { font-size: 2em; font-weight: bold; color: var(--text-accent); margin-bottom: 5px; } .stat-label { color: var(--text-secondary); font-size: 0.9em; } .pets-table-wrapper { overflow-x: auto; } .pets-table { width: 100%; border-collapse: collapse; margin: 20px 0; background: var(--bg-tertiary); border-radius: 8px; overflow: hidden; min-width: 600px; } .pets-table th, .pets-table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border-color); } .pets-table th { background: var(--bg-primary); color: var(--text-accent); font-weight: 600; position: sticky; top: 0; z-index: 10; } .pets-table tr:hover { background: var(--hover-color); } .pet-active { color: var(--text-accent); font-weight: bold; } .pet-stored { color: var(--text-secondary); } .type-badge { background: var(--bg-primary); color: var(--text-accent); padding: 2px 8px; border-radius: 12px; font-size: 0.8em; margin-right: 5px; } .achievements-grid, .badges-grid, .encounters-grid, .inventory-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; } .achievement-card, .badge-card, .encounter-card, .inventory-item { background: var(--bg-tertiary); padding: 15px; border-radius: 8px; border-left: 4px solid var(--text-accent); transition: transform 0.3s ease; } .achievement-card:hover, .badge-card:hover, .encounter-card:hover, .inventory-item:hover { transform: translateY(-3px); } .achievement-card { display: flex; align-items: flex-start; gap: 15px; } .achievement-icon { font-size: 1.5em; flex-shrink: 0; } .achievement-content h4 { margin: 0 0 8px 0; color: var(--text-accent); } .achievement-content p { margin: 0 0 8px 0; color: var(--text-primary); } .achievement-date { color: var(--text-secondary); font-size: 0.9em; } .badge-card { display: flex; align-items: flex-start; gap: 15px; border-left-color: gold; } .badge-icon { font-size: 1.5em; flex-shrink: 0; } .badge-content h4 { margin: 0 0 8px 0; color: gold; } .badge-content p { margin: 0 0 10px 0; color: var(--text-primary); } .badge-stats { display: flex; flex-direction: column; gap: 4px; } .badge-stats span { color: var(--text-secondary); font-size: 0.9em; } .encounter-card { border-left: 4px solid var(--text-accent); } .encounter-header { margin-bottom: 10px; } .encounter-stats { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; } .encounter-stats span { color: var(--text-primary); font-size: 0.9em; } .encounter-date { color: var(--text-secondary); font-size: 0.9em; } .inventory-item { border-left: 4px solid var(--text-accent); } .item-header { margin-bottom: 8px; } .item-description { color: var(--text-primary); margin-bottom: 8px; } .item-meta { color: var(--text-secondary); font-size: 0.9em; } .empty-state { text-align: center; padding: 40px; color: var(--text-secondary); } .empty-icon { font-size: 3em; margin-bottom: 15px; } .empty-state h3 { margin: 0 0 10px 0; color: var(--text-primary); } .empty-state p { margin: 0; font-size: 1.1em; } .encounters-summary { text-align: center; margin-bottom: 20px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px; } .encounters-summary p { margin: 5px 0; color: var(--text-secondary); } .btn { display: inline-block; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 600; text-align: center; transition: all 0.3s ease; border: none; cursor: pointer; } .btn-primary { background: var(--gradient-primary); color: white; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); } /* Mobile Responsive */ @media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; } .stat-card { padding: 15px; } .stat-value { font-size: 1.5em; } .achievements-grid, .badges-grid, .encounters-grid, .inventory-grid { grid-template-columns: 1fr; } .nav-pill { padding: 6px 12px; font-size: 0.8em; margin: 3px; } .pets-table { min-width: 500px; } .pets-table th, .pets-table td { padding: 8px 10px; font-size: 0.9em; } } """ # Get the unified template with the additional CSS html_content = self.get_page_template(f"{nickname}'s Profile", content, "players") html_content = html_content.replace("", additional_css + "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def log_message(self, format, *args): """Override to reduce logging noise""" pass def serve_teambuilder(self, nickname): """Serve the team builder interface""" from urllib.parse import unquote nickname = unquote(nickname) try: from src.database import Database database = Database() # Get event loop try: loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) player_data = loop.run_until_complete(self.fetch_player_data(database, nickname)) if player_data is None: self.serve_player_not_found(nickname) return pets = player_data['pets'] if not pets: self.serve_teambuilder_no_pets(nickname) return self.serve_teambuilder_interface(nickname, pets) except Exception as e: print(f"Error loading team builder for {nickname}: {e}") self.serve_player_error(nickname, f"Error loading team builder: {str(e)}") def serve_teambuilder_no_pets(self, nickname): """Show message when player has no pets using unified template""" content = f"""

๐Ÿพ Team Builder

Build your perfect team for battles and adventures

๐Ÿพ No Pets Found

{nickname}, you need to catch some pets before using the team builder!

Head to the IRC channel and use !explore to find wild pets!

โ† Back to Profile
""" # Add no-pets-specific CSS additional_css = """ .main-container { text-align: center; max-width: 800px; margin: 0 auto; } .no-pets-message { background: var(--bg-secondary); padding: 40px; border-radius: 15px; border: 2px solid var(--warning-color); margin-top: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); } .no-pets-message h2 { color: var(--warning-color); margin-top: 0; } """ html_content = self.get_page_template(f"Team Builder - {nickname}", content, "players") html_content = html_content.replace("", additional_css + "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def serve_teambuilder_interface(self, nickname, pets): """Serve the full interactive team builder interface""" active_pets = [pet for pet in pets if pet['is_active']] inactive_pets = [pet for pet in pets if not pet['is_active']] # Debug logging print(f"Team Builder Debug for {nickname}:") print(f"Total pets: {len(pets)}") active_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in active_pets] inactive_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in inactive_pets] print(f"Active pets: {len(active_pets)} - {active_names}") print(f"Inactive pets: {len(inactive_pets)} - {inactive_names}") # Generate detailed pet cards with debugging def make_pet_card(pet, is_active): name = pet['nickname'] or pet['species_name'] status = "Active" if is_active else "Storage" status_class = "active" if is_active else "storage" type_str = pet['type1'] if pet['type2']: type_str += f"/{pet['type2']}" # Debug logging print(f"Making pet card for {name} (ID: {pet['id']}): is_active={pet['is_active']}, passed_is_active={is_active}, status_class={status_class}, team_order={pet.get('team_order', 'None')}") # Calculate HP percentage for health bar hp_percent = (pet['hp'] / pet['max_hp']) * 100 if pet['max_hp'] > 0 else 0 hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336" return f"""

{name}

{status}
Level {pet['level']} {pet['species_name']}
{type_str}
HP: {pet['hp']}/{pet['max_hp']}
ATK {pet['attack']}
DEF {pet['defense']}
SPD {pet['speed']}
EXP {pet['experience']}
{'๐Ÿ˜Š' if pet['happiness'] > 70 else '๐Ÿ˜' if pet['happiness'] > 40 else '๐Ÿ˜ž'} Happiness: {pet['happiness']}/100
""" # Create 6 numbered slots and place pets in their positions team_slots = [''] * 6 # Initialize 6 empty slots # Place active pets in their team_order positions for pet in active_pets: team_order = pet.get('team_order') if team_order and 1 <= team_order <= 6: team_slots[team_order - 1] = make_pet_card(pet, True) storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets) html = f""" Team Builder - {nickname} โ† Back to {nickname}'s Profile

๐Ÿพ Team Builder

Drag pets between Active and Storage to build your perfect team

{nickname} | Active: {len(active_pets)} pets | Storage: {len(inactive_pets)} pets

โญ Active Team
1
{team_slots[0]}
2
{team_slots[1]}
3
{team_slots[2]}
4
{team_slots[3]}
5
{team_slots[4]}
6
{team_slots[5]}
๐Ÿ“ฆ Storage
{storage_cards}
Drop pets here to store them
โ† Back to Profile
Changes are saved securely with PIN verification via IRC

๐Ÿ” PIN Verification Required

A 6-digit PIN has been sent to you via IRC private message.

Enter the PIN below to confirm your team changes:

""" # Generate storage pets HTML first storage_pets_html = "" for pet in inactive_pets: storage_pets_html += make_pet_card(pet, False) # Generate active pets HTML for team slots active_pets_html = "" for pet in active_pets: if pet.get('team_order'): active_pets_html += make_pet_card(pet, True) # Create content using string concatenation instead of f-strings to avoid CSS brace issues team_builder_content = """

๐Ÿพ Team Builder

Drag pets between Active Team and Storage. Double-click as backup.

โš”๏ธ Active Team (1-6 pets)

Slot 1 (Leader)
Drop pet here
Slot 2
Drop pet here
Slot 3
Drop pet here
Slot 4
Drop pet here
Slot 5
Drop pet here
Slot 6
Drop pet here

๐Ÿ“ฆ Storage

""" + storage_pets_html + active_pets_html + """
โ† Back to Profile
Changes are saved securely with PIN verification via IRC

๐Ÿ” PIN Verification Required

A 6-digit PIN has been sent to you via IRC private message.

Enter the PIN below to confirm your team changes:

๐Ÿ’ก How to use:
โ€ข Drag pets to team slots
โ€ข Double-click to move pets
โ€ข Empty slots show placeholders
""" # Get the unified template html_content = self.get_page_template(f"Team Builder - {nickname}", team_builder_content, "") self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html_content.encode()) def handle_team_save(self, nickname): """Handle team save request and generate PIN""" try: # Get POST data content_length = int(self.headers.get('Content-Length', 0)) if content_length == 0: self.send_json_response({"success": False, "error": "No data provided"}, 400) return post_data = self.rfile.read(content_length).decode('utf-8') # Parse JSON data import json try: team_data = json.loads(post_data) except json.JSONDecodeError: self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) return # Run async operations import asyncio result = asyncio.run(self._handle_team_save_async(nickname, team_data)) if result["success"]: self.send_json_response(result, 200) else: self.send_json_response(result, 400) except Exception as e: print(f"Error in handle_team_save: {e}") self.send_json_response({"success": False, "error": "Internal server error"}, 500) async def _handle_team_save_async(self, nickname, team_data): """Async handler for team save""" try: # Get player player = await self.database.get_player(nickname) if not player: return {"success": False, "error": "Player not found"} # Validate team composition validation = await self.database.validate_team_composition(player["id"], team_data) if not validation["valid"]: return {"success": False, "error": validation["error"]} # Create pending team change with PIN import json result = await self.database.create_pending_team_change( player["id"], json.dumps(team_data) ) if result["success"]: # Send PIN via IRC self.send_pin_via_irc(nickname, result["pin_code"]) return { "success": True, "message": "PIN sent to your IRC private messages", "expires_in_minutes": 10 } else: return result except Exception as e: print(f"Error in _handle_team_save_async: {e}") return {"success": False, "error": str(e)} def handle_team_verify(self, nickname): """Handle PIN verification and apply team changes""" try: # Get POST data content_length = int(self.headers.get('Content-Length', 0)) if content_length == 0: self.send_json_response({"success": False, "error": "No PIN provided"}, 400) return post_data = self.rfile.read(content_length).decode('utf-8') # Parse JSON data import json try: data = json.loads(post_data) pin_code = data.get("pin", "").strip() except (json.JSONDecodeError, AttributeError): self.send_json_response({"success": False, "error": "Invalid data format"}, 400) return if not pin_code: self.send_json_response({"success": False, "error": "PIN code is required"}, 400) return # Run async operations import asyncio result = asyncio.run(self._handle_team_verify_async(nickname, pin_code)) if result["success"]: self.send_json_response(result, 200) else: self.send_json_response(result, 400) except Exception as e: print(f"Error in handle_team_verify: {e}") self.send_json_response({"success": False, "error": "Internal server error"}, 500) async def _handle_team_verify_async(self, nickname, pin_code): """Async handler for PIN verification""" try: # Get player player = await self.database.get_player(nickname) if not player: return {"success": False, "error": "Player not found"} # Apply team changes with PIN verification result = await self.database.apply_team_change(player["id"], pin_code) if result["success"]: return { "success": True, "message": f"Team changes applied successfully! {result['changes_applied']} pets updated.", "changes_applied": result["changes_applied"] } else: return result except Exception as e: print(f"Error in _handle_team_verify_async: {e}") return {"success": False, "error": str(e)} def send_pin_via_irc(self, nickname, pin_code): """Send PIN to player via IRC private message""" print(f"๐Ÿ” PIN for {nickname}: {pin_code}") # Try to send via IRC bot if available if self.bot and hasattr(self.bot, 'send_message'): try: # Send PIN via private message self.bot.send_message(nickname, f"๐Ÿ” Team Builder PIN: {pin_code}") self.bot.send_message(nickname, f"๐Ÿ’ก Enter this PIN on the web page to confirm your team changes. PIN expires in 10 minutes.") print(f"โœ… PIN sent to {nickname} via IRC") except Exception as e: print(f"โŒ Failed to send PIN via IRC: {e}") else: print(f"โŒ No IRC bot available to send PIN to {nickname}") print(f"๐Ÿ’ก Manual PIN for {nickname}: {pin_code}") class PetBotWebServer: """Standalone web server for PetBot""" def __init__(self, database=None, port=8080, bot=None): self.database = database or Database() self.port = port self.bot = bot self.server = None def run(self): """Start the web server""" self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler) self.server.database = self.database self.server.bot = self.bot print(f'๐ŸŒ Starting PetBot web server on http://0.0.0.0:{self.port}') print(f'๐Ÿ“ก Accessible from WSL at: http://172.27.217.61:{self.port}') print(f'๐Ÿ“ก Accessible from Windows at: http://localhost:{self.port}') print('') print('๐ŸŒ Public access at: http://petz.rdx4.com/') print('') self.server.serve_forever() def start_in_thread(self): """Start the web server in a background thread""" import threading self.thread = threading.Thread(target=self.run, daemon=True) self.thread.start() def stop(self): """Stop the web server""" if self.server: self.server.shutdown() self.server.server_close() def run_standalone(): """Run the web server in standalone mode""" import sys port = 8080 if len(sys.argv) > 1: try: port = int(sys.argv[1]) except ValueError: print('Usage: python webserver.py [port]') sys.exit(1) server = PetBotWebServer(port) print('๐ŸŒ PetBot Web Server') print('=' * 50) print(f'Port: {port}') print('') print('๐Ÿ”— Local URLs:') print(f' http://localhost:{port}/ - Game Hub (local)') print(f' http://localhost:{port}/help - Command Help (local)') print(f' http://localhost:{port}/players - Player List (local)') print(f' http://localhost:{port}/leaderboard - Leaderboard (local)') print(f' http://localhost:{port}/locations - Locations (local)') print('') print('๐ŸŒ Public URLs:') print(' http://petz.rdx4.com/ - Game Hub') print(' http://petz.rdx4.com/help - Command Help') print(' http://petz.rdx4.com/players - Player List') print(' http://petz.rdx4.com/leaderboard - Leaderboard') print(' http://petz.rdx4.com/locations - Locations') print('') print('Press Ctrl+C to stop') try: server.run() except KeyboardInterrupt: print('\nโœ… Web server stopped') if __name__ == '__main__': run_standalone()