diff --git a/webserver.py b/webserver.py index 374ef0f..709bd1f 100644 --- a/webserver.py +++ b/webserver.py @@ -25,6 +25,529 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """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) @@ -68,139 +591,501 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): def serve_index(self): """Serve the main index page""" - html = """ - - - - - PetBot Game Hub - - - -
-

🐾 PetBot Game Hub

-

Welcome to the PetBot web interface!

-

Connect to irc.libera.chat #petz to play

-
- - - -
-

šŸ¤– 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""" - try: - with open('help.html', 'r') as f: - content = f.read() - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(content.encode()) - except FileNotFoundError: - self.send_error(404, "Help file not found") + """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""" @@ -268,6 +1153,34 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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 = "" @@ -294,175 +1207,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ - html = f""" - - - - - PetBot - Players - - - - ← Back to Game Hub - -
-

šŸ‘„ Registered Players

-

All trainers on their pet collection journey

-
- -
-
šŸ“Š Server Statistics
-
-
-
-
{len(players_data)}
-
Total Players
-
-
-
{sum(p['pet_count'] for p in players_data)}
-
Total Pets Caught
-
-
-
{sum(p['achievement_count'] for p in players_data)}
-
Total Achievements
-
-
-
{max((p['level'] for p in players_data), default=0)}
-
Highest Level
-
-
-
-
- -
-
šŸ† Player Rankings
-
- + # Build table content + table_content = f""" +
+

šŸ† Player Rankings

+
@@ -480,14 +1229,24 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): {players_html}
Rank
-

šŸ’” 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') @@ -495,76 +1254,49 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.wfile.write(html.encode()) def serve_error_page(self, page_name, error_msg): - """Serve a generic error page""" - html = f""" - - - - - PetBot - Error - - - - ← Back to Game Hub - -
-

āš ļø Error Loading {page_name}

-
- -
-

Unable to load page

-

{error_msg}

-

Please try again later or contact an administrator.

-
- -""" + 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.encode()) + self.wfile.write(html_content.encode()) def serve_leaderboard(self): """Serve the leaderboard page - redirect to players for now""" @@ -635,7 +1367,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return [] def serve_locations_data(self, locations_data): - """Serve locations page with real data""" + """Serve locations page with real data using unified template""" # Build locations HTML locations_html = "" @@ -699,119 +1431,133 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - html = f""" - - - - - PetBot - Locations - - - - ← Back to Game Hub - -
-

šŸ—ŗļø 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 -

-
- - - -""" + # 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.encode()) + self.wfile.write(html_content.encode()) def serve_petdex(self): """Serve the petdex page with all pet species data""" @@ -965,9 +1648,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): try: import aiosqlite async with aiosqlite.connect(database.db_path) as db: - # Get all pet species with evolution information + # Get all pet species with evolution information (no duplicates) cursor = await db.execute(""" - SELECT ps.*, + 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 @@ -1018,6 +1701,40 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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'] @@ -1100,207 +1817,24 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):

The petdex appears to be empty. Contact an administrator.

""" - html = f""" - - - - - PetBot - Petdex - - - - ← Back to Game Hub - -
-

šŸ“– Petdex - Complete Pet Encyclopedia

-

Comprehensive guide to all available pet species

-
- -
-
-
-
{total_species}
-
Total Species
-
-
-
{len([p for p in petdex_data if p['type1'] == 'Fire' or p['type2'] == 'Fire'])}
-
Fire Types
-
-
-
{len([p for p in petdex_data if p['type1'] == 'Water' or p['type2'] == 'Water'])}
-
Water Types
-
-
-
{len([p for p in petdex_data if p['evolution_level']])}
-
Can Evolve
-
+ # Combine all content + content = f""" +
+

šŸ“– Petdex

+

Complete encyclopedia of all available pets

-

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

-
- - {petdex_html} - - -""" + {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() @@ -1387,7 +1921,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): '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 - 'species_name': row[14], 'type1': row[15], 'type2': row[16] + 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17] } pets.append(pet_dict) @@ -1492,148 +2026,94 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return None def serve_player_not_found(self, nickname): - """Serve player not found page""" - html = f""" - - - - - PetBot - Player Not Found - - - - ← Back to Game Hub - -
-

🚫 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!

-
- -""" + 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.encode()) + self.wfile.write(html_content.encode()) def serve_player_error(self, nickname, error_msg): - """Serve player error page""" - html = f""" - - - - - PetBot - Error - - - - ← Back to Game Hub - -
-

āš ļø Error

-
- -
-

Unable to load player data

-

{error_msg}

-

Please try again later or contact an administrator.

-
- -""" + 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.encode()) + self.wfile.write(html_content.encode()) def serve_player_data(self, nickname, player_data): """Serve player profile page with real data""" @@ -1685,15 +2165,20 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if achievements: for achievement in achievements: achievements_html += f""" -
- šŸ† {achievement['achievement_name']}
- {achievement['achievement_desc']}
- Earned: {achievement['completed_at']} +
+
šŸ†
+
+

{achievement['achievement_name']}

+

{achievement['achievement_desc']}

+ Earned: {achievement['completed_at']} +
""" else: achievements_html = """ -
- No achievements yet. Keep exploring and catching pets to earn achievements! +
+
šŸ†
+

No achievements yet

+

Keep exploring and catching pets to earn achievements!

""" # Build inventory HTML @@ -1720,15 +2205,19 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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()} +
+
+ {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! +
+
šŸŽ’
+

No items yet

+

Try exploring to find useful items!

""" # Build gym badges HTML @@ -1744,15 +2233,24 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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']} +
+
{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! +
+
šŸ†
+

No gym badges yet

+

Challenge gyms to earn badges and prove your training skills!

""" # Build encounters HTML @@ -1775,283 +2273,495 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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} +
+
+ {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! +
+
šŸ‘ļø
+

No pets encountered yet

+

Use !explore to discover wild pets!

""" - html = f""" - - - - - PetBot - {nickname}'s Profile - - - - ← Back to Game Hub - -
-

🐾 {nickname}'s Profile

-

Level {player['level']} Trainer

-

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

- -
- -
-
šŸ“Š 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} - -
StatusNameSpeciesTypeLevelHPStats
-
-
- -
-
šŸ† 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} -
-
- -""" + } + + .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.encode()) + self.wfile.write(html_content.encode()) def log_message(self, format, *args): """Override to reduce logging noise""" @@ -2091,31 +2801,51 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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""" - html = f""" - - - - - Team Builder - {nickname} - - - -
-

🐾 No Pets Found

-

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

-

← Back to Profile

-
- -""" + """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.encode()) + self.wfile.write(html_content.encode()) def serve_teambuilder_interface(self, nickname, pets): """Serve the full interactive team builder interface""" @@ -2140,14 +2870,14 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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}") + 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}
@@ -2187,7 +2917,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - active_cards = ''.join(make_pet_card(pet, True) for pet in active_pets) + # 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""" @@ -2269,6 +3007,75 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): min-height: 200px; }} + .team-slots-container {{ + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + min-height: 400px; + }} + + .team-slot {{ + background: var(--bg-tertiary); + border: 2px dashed #666; + border-radius: 12px; + padding: 10px; + position: relative; + min-height: 120px; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + }} + + .team-slot:hover {{ + border-color: var(--text-accent); + background: var(--drag-hover); + }} + + .team-slot.drag-over {{ + border-color: var(--text-accent); + background: var(--drag-hover); + border-style: solid; + transform: scale(1.02); + }} + + .slot-number {{ + position: absolute; + top: 5px; + left: 5px; + background: var(--active-color); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9em; + font-weight: bold; + z-index: 10; + }} + + .slot-content {{ + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + }} + + .slot-content:empty::before {{ + content: "Empty Slot"; + color: var(--text-secondary); + font-style: italic; + opacity: 0.7; + }} + + .slot-content .pet-card {{ + margin: 0; + width: 100%; + max-width: none; + }} + .pet-card {{ background: var(--bg-tertiary); border-radius: 12px; @@ -2565,11 +3372,31 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
⭐ Active Team
-
- {active_cards} -
-
- Drop pets here to add to your 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]}
+
@@ -2644,14 +3471,23 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): // Add double-click handler card.addEventListener('dblclick', function() {{ const petId = this.dataset.petId; - const isActive = currentTeam[petId]; + const currentPosition = currentTeam[petId]; - console.log(`Double-click: Moving pet ${{petId}} from ${{isActive ? 'active' : 'storage'}} to ${{isActive ? 'storage' : 'active'}}`); + console.log(`Double-click: Moving pet ${{petId}} from ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); - if (isActive) {{ + if (currentPosition) {{ movePetToStorage(petId); }} else {{ - movePetToActive(petId); + // Find first empty slot + for (let i = 1; i <= 6; i++) {{ + const slot = document.getElementById(`slot-${{i}}`); + const slotContent = slot.querySelector('.slot-content'); + if (slotContent.children.length === 0) {{ + movePetToTeamSlot(petId, i); + return; + }} + }} + console.log('No empty slots available'); }} }}); @@ -2668,49 +3504,42 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} // Declare container variables once at the top level - const activeContainer = document.getElementById('active-container'); + const teamSlotsContainer = document.getElementById('team-slots-container'); const storageContainer = document.getElementById('storage-container'); - const activeDrop = document.getElementById('active-drop'); const storageDrop = document.getElementById('storage-drop'); + const teamSlots = Array.from({{length: 6}}, (_, i) => document.getElementById(`slot-${{i + 1}}`)); // Initialize team state with detailed debugging console.log('=== TEAM STATE INITIALIZATION ==='); const allCards = document.querySelectorAll('.pet-card'); console.log(`Found ${{allCards.length}} pet cards total`); - console.log(`Active container has ${{activeContainer.children.length}} pets initially`); + console.log(`Team slots container:`, teamSlotsContainer); console.log(`Storage container has ${{storageContainer.children.length}} pets initially`); + let teamPositions = {{}}; // Track pet positions in team slots + allCards.forEach((card, index) => {{ const petId = card.dataset.petId; const isActive = card.dataset.active === 'true'; - const currentContainer = card.parentElement.id; + const teamOrder = card.dataset.teamOrder; - console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, currentContainer=${{currentContainer}}`); + console.log(`Pet ${{index}}: ID=${{petId}}, data-active=${{card.dataset.active}}, isActive=${{isActive}}, team_order=${{teamOrder}}`); - originalTeam[petId] = isActive; - currentTeam[petId] = isActive; - - // CRITICAL: Verify container placement is correct - DO NOT MOVE unless absolutely necessary - const expectedContainer = isActive ? activeContainer : storageContainer; - const expectedContainerId = isActive ? 'active-container' : 'storage-container'; - - if (currentContainer !== expectedContainerId) {{ - console.error(`MISMATCH! Pet ${{petId}} is in ${{currentContainer}} but should be in ${{expectedContainerId}} based on data-active=${{card.dataset.active}}`); - console.log(`Moving pet ${{petId}} to correct container...`); - expectedContainer.appendChild(card); + if (isActive && teamOrder) {{ + teamPositions[petId] = parseInt(teamOrder); + originalTeam[petId] = parseInt(teamOrder); + currentTeam[petId] = parseInt(teamOrder); }} else {{ - console.log(`Pet ${{petId}} correctly placed in ${{currentContainer}}`); + originalTeam[petId] = false; + currentTeam[petId] = false; }} }}); - console.log('After initialization:'); - console.log(`Active container now has ${{activeContainer.children.length}} pets`); - console.log(`Storage container now has ${{storageContainer.children.length}} pets`); - + console.log('Team positions:', teamPositions); console.log('Original team state:', originalTeam); console.log('Current team state:', currentTeam); - // Completely rewritten drag and drop - simpler approach + // Completely rewritten drag and drop for slot system function initializeDragAndDrop() {{ console.log('Initializing drag and drop...'); @@ -2744,43 +3573,43 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }}); }}); - // Set up drop zones (using previously declared variables) - - [activeContainer, activeDrop].forEach(zone => {{ - if (zone) {{ - zone.addEventListener('dragover', function(e) {{ - e.preventDefault(); - if (e.dataTransfer) {{ - e.dataTransfer.dropEffect = 'move'; - }} - }}); - - zone.addEventListener('dragenter', function(e) {{ - e.preventDefault(); - this.classList.add('drag-over'); - console.log('DRAGENTER: Active zone'); - }}); - - zone.addEventListener('dragleave', function(e) {{ - if (!this.contains(e.relatedTarget)) {{ - this.classList.remove('drag-over'); - }} - }}); - - zone.addEventListener('drop', function(e) {{ - e.preventDefault(); - console.log('DROP: Active zone'); + // Set up team slot drop zones + teamSlots.forEach((slot, index) => {{ + const position = index + 1; + + slot.addEventListener('dragover', function(e) {{ + e.preventDefault(); + if (e.dataTransfer) {{ + e.dataTransfer.dropEffect = 'move'; + }} + }}); + + slot.addEventListener('dragenter', function(e) {{ + e.preventDefault(); + this.classList.add('drag-over'); + console.log(`DRAGENTER: Team slot ${{position}}`); + }}); + + slot.addEventListener('dragleave', function(e) {{ + if (!this.contains(e.relatedTarget)) {{ this.classList.remove('drag-over'); - - if (draggedElement) {{ - const petId = draggedElement.dataset.petId; - console.log('Moving pet', petId, 'to active'); - movePetToActive(petId); - }} - }}); - }} + }} + }}); + + slot.addEventListener('drop', function(e) {{ + e.preventDefault(); + console.log(`DROP: Team slot ${{position}}`); + this.classList.remove('drag-over'); + + if (draggedElement) {{ + const petId = draggedElement.dataset.petId; + console.log(`Moving pet ${{petId}} to team slot ${{position}}`); + movePetToTeamSlot(petId, position); + }} + }}); }}); + // Set up storage drop zones [storageContainer, storageDrop].forEach(zone => {{ if (zone) {{ zone.addEventListener('dragover', function(e) {{ @@ -2819,39 +3648,48 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): console.log('Drag and drop initialization complete'); }} - function movePetToActive(petId) {{ - console.log(`movePetToActive called for pet ${{petId}}`); + function movePetToTeamSlot(petId, position) {{ + console.log(`movePetToTeamSlot called for pet ${{petId}}, position ${{position}}`); const card = document.querySelector(`[data-pet-id="${{petId}}"]`); if (!card) {{ console.log(`No card found for pet ${{petId}}`); return; }} - const currentIsActive = currentTeam[petId]; - - console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); - - if (!currentIsActive) {{ - console.log(`Moving pet ${{petId}} to active...`); - - // Update state - currentTeam[petId] = true; - - // Move DOM element - card.classList.remove('storage'); - card.classList.add('active'); - card.dataset.active = 'true'; - card.querySelector('.status-badge').textContent = 'Active'; - activeContainer.appendChild(card); - - // Update interface - updateSaveButton(); - updateDropZoneVisibility(); - - console.log('Pet moved to active successfully'); - }} else {{ - console.log(`Pet ${{petId}} is already active, no move needed`); + const slot = document.getElementById(`slot-${{position}}`); + if (!slot) {{ + console.log(`No slot found for position ${{position}}`); + return; }} + + const slotContent = slot.querySelector('.slot-content'); + + // Check if slot is already occupied + if (slotContent.children.length > 0) {{ + console.log(`Slot ${{position}} is already occupied, swapping pets`); + const existingCard = slotContent.querySelector('.pet-card'); + if (existingCard) {{ + const existingPetId = existingCard.dataset.petId; + // Move existing pet to storage + movePetToStorage(existingPetId); + }} + }} + + // Update state + currentTeam[petId] = position; + + // Move DOM element + card.classList.remove('storage'); + card.classList.add('active'); + card.dataset.active = 'true'; + card.dataset.teamOrder = position; + card.querySelector('.status-badge').textContent = 'Active'; + slotContent.appendChild(card); + + // Update interface + updateSaveButton(); + + console.log(`Pet moved to team slot ${{position}} successfully`); }} function movePetToStorage(petId) {{ @@ -2862,11 +3700,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return; }} - const currentIsActive = currentTeam[petId]; + const currentPosition = currentTeam[petId]; - console.log(`Pet ${{petId}} current state: ${{currentIsActive ? 'active' : 'storage'}}`); + console.log(`Pet ${{petId}} current state: ${{currentPosition ? `team slot ${{currentPosition}}` : 'storage'}}`); - if (currentIsActive) {{ + if (currentPosition) {{ console.log(`Moving pet ${{petId}} to storage...`); // Update state @@ -2876,6 +3714,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): card.classList.remove('active'); card.classList.add('storage'); card.dataset.active = 'false'; + card.dataset.teamOrder = ''; card.querySelector('.status-badge').textContent = 'Storage'; storageContainer.appendChild(card); @@ -2891,16 +3730,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): function updateDropZoneVisibility() {{ - // Using previously declared container variables - - // CRITICAL: Only update visual indicators, never move pets - // Use CSS classes instead of direct style manipulation - if (activeContainer && activeContainer.children.length > 0) {{ - if (activeDrop) activeDrop.classList.add('has-pets'); - }} else {{ - if (activeDrop) activeDrop.classList.remove('has-pets'); - }} - + // Update storage drop zone visibility if (storageContainer && storageContainer.children.length > 0) {{ if (storageDrop) storageDrop.classList.add('has-pets'); }} else {{ @@ -2908,7 +3738,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): }} console.log('Drop zone visibility updated:', {{ - activeContainerPets: activeContainer ? activeContainer.children.length : 0, storageContainerPets: storageContainer ? storageContainer.children.length : 0 }}); }} @@ -3072,10 +3901,716 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ + # 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.encode()) + self.wfile.write(html_content.encode()) def handle_team_save(self, nickname): """Handle team save request and generate PIN""" @@ -3211,90 +4746,94 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"šŸ” PIN for {nickname}: {pin_code}") # Try to send via IRC bot if available - try: - # Check if the bot instance is accessible via global state - import sys - if hasattr(sys.modules.get('__main__'), 'bot_instance'): - bot = sys.modules['__main__'].bot_instance - if hasattr(bot, 'send_message'): - # Send directly via bot's send_message method (non-async) - message = f"šŸ” Team Builder PIN: {pin_code} (expires in 10 minutes)" - bot.send_message(nickname, message) - print(f"āœ… PIN sent to {nickname} via IRC") - return - except Exception as e: - print(f"Could not send PIN via IRC bot: {e}") - - # Fallback: just print to console for now - print(f"āš ļø IRC bot not available - PIN displayed in console only") - - def send_json_response(self, data, status_code=200): - """Send JSON response""" - import json - response = json.dumps(data) - - self.send_response(status_code) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(response.encode()) + 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: - def __init__(self, database, port=8080): - self.database = database + """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 HTTP web server""" - print(f"🌐 Starting PetBot web server on http://0.0.0.0:{self.port}") - print(f"šŸ“” Accessible from WSL at: http://172.27.217.61:{self.port}") - print(f"šŸ“” Accessible from Windows at: http://localhost:{self.port}") + """Start the web server""" self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler) - # Pass database to the server so handlers can access it self.server.database = self.database + self.server.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""" - thread = Thread(target=self.run, daemon=True) - thread.start() - print(f"āœ… Web server started at http://localhost:{self.port}") - print(f"🌐 Public access at: http://petz.rdx4.com/") - return thread + 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 standalone for testing""" - print("🐾 PetBot Web Server (Standalone Mode)") - print("=" * 40) + """Run the web server in standalone mode""" + import sys - # Initialize database - database = Database() - # Note: In standalone mode, we can't easily run async init - # This is mainly for testing the web routes + port = 8080 + if len(sys.argv) > 1: + try: + port = int(sys.argv[1]) + except ValueError: + print('Usage: python webserver.py [port]') + sys.exit(1) - # Start web server - server = PetBotWebServer(database) - print("šŸš€ Starting web server...") - print("šŸ“ Available routes:") - print(" http://localhost:8080/ - Game Hub (local)") - print(" http://localhost:8080/help - Command Help (local)") - print(" http://localhost:8080/players - Player List (local)") - print(" http://localhost:8080/leaderboard - Leaderboard (local)") - print(" http://localhost:8080/locations - Locations (local)") - print("") - print("🌐 Public URLs:") - print(" http://petz.rdx4.com/ - Game Hub") - print(" http://petz.rdx4.com/help - Command Help") - print(" http://petz.rdx4.com/players - Player List") - print(" http://petz.rdx4.com/leaderboard - Leaderboard") - print(" http://petz.rdx4.com/locations - Locations") - print("") - print("Press Ctrl+C to stop") + 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") + print('\nāœ… Web server stopped') + +if __name__ == '__main__': + run_standalone() -if __name__ == "__main__": - run_standalone() \ No newline at end of file