From 88e352ee79d352226398c828eabace888c32a877 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Tue, 15 Jul 2025 20:47:37 +0000 Subject: [PATCH] Enhance web interface and item system with user experience improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix \!team command to redirect to team builder web page instead of showing IRC list - Add usage commands to inventory items showing "\!use " for each item - Implement Coin Pouch treasure item with 1-3 coin rewards (rare drop rate) - Fix gym badges leaderboard database query with proper COALESCE and CASE syntax - Improve inventory item display with styled command instructions and monospace code blocks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- config/items.json | 13 ++ modules/inventory.py | 16 ++ modules/pet_management.py | 44 +--- webserver.py | 428 +++++++++++++++++++++++++++++++++++++- 4 files changed, 450 insertions(+), 51 deletions(-) diff --git a/config/items.json b/config/items.json index c17f6dc..0912108 100644 --- a/config/items.json +++ b/config/items.json @@ -188,6 +188,19 @@ "spawn_rate": 0.0075 } ], + "treasure_items": [ + { + "id": 17, + "name": "Coin Pouch", + "description": "A small leather pouch containing loose coins", + "rarity": "rare", + "category": "treasure", + "effect": "money", + "effect_value": "1-3", + "locations": ["all"], + "spawn_rate": 0.008 + } + ], "rarity_info": { "common": { "color": "white", diff --git a/modules/inventory.py b/modules/inventory.py index 9994ad6..227e882 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -99,6 +99,22 @@ class Inventory(BaseModule): self.send_message(channel, f"🍀 {nickname}: Used {item['name']}! Rare pet encounter rate increased by {effect_value}% for 1 hour!") + elif effect == "money": + # Handle money items (like Coin Pouch) + import random + if "-" in str(effect_value): + # Parse range like "1-3" + min_coins, max_coins = map(int, str(effect_value).split("-")) + coins_gained = random.randint(min_coins, max_coins) + else: + coins_gained = int(effect_value) + + # Add money to player + await self.database.add_money(player["id"], coins_gained) + + self.send_message(channel, + f"💰 {nickname}: Used {item['name']}! Found {coins_gained} coins inside!") + elif effect == "none": self.send_message(channel, f"📦 {nickname}: Used {item['name']}! This item has no immediate effect but may be useful later.") diff --git a/modules/pet_management.py b/modules/pet_management.py index 911bef5..b75ef2c 100644 --- a/modules/pet_management.py +++ b/modules/pet_management.py @@ -22,51 +22,13 @@ class PetManagement(BaseModule): await self.cmd_nickname(channel, nickname, args) async def cmd_team(self, channel, nickname): - """Show active pets (channel display)""" + """Redirect player to their team builder page""" player = await self.require_player(channel, nickname) if not player: return - pets = await self.database.get_player_pets(player["id"], active_only=False) - if not pets: - self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.") - return - - # Show active pets first, then others - active_pets = [pet for pet in pets if pet.get("is_active")] - inactive_pets = [pet for pet in pets if not pet.get("is_active")] - - team_info = [] - - # Active pets with star and team position - for pet in active_pets: - name = pet["nickname"] or pet["species_name"] - - # Calculate EXP progress - current_exp = pet.get("experience", 0) - next_level_exp = self.database.calculate_exp_for_level(pet["level"] + 1) - current_level_exp = self.database.calculate_exp_for_level(pet["level"]) - exp_needed = next_level_exp - current_exp - - if pet["level"] >= 100: - exp_display = "MAX" - else: - exp_display = f"{exp_needed} to next" - - # Show team position - position = pet.get("team_order", "?") - team_info.append(f"[{position}]⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}") - - # Inactive pets - for pet in inactive_pets[:5]: # Show max 5 inactive - name = pet["nickname"] or pet["species_name"] - team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP") - - if len(inactive_pets) > 5: - team_info.append(f"... and {len(inactive_pets) - 5} more in storage") - - self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info)) - self.send_message(channel, f"🌐 View detailed pet collection at: http://petz.rdx4.com/player/{nickname}#pets") + # Redirect to web interface for team management + self.send_message(channel, f"⚔️ {nickname}: Manage your team at: http://petz.rdx4.com/teambuilder/{nickname}") async def cmd_pets(self, channel, nickname): """Show link to pet collection web page""" diff --git a/webserver.py b/webserver.py index 5b28697..04b9343 100644 --- a/webserver.py +++ b/webserver.py @@ -1402,12 +1402,405 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 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() + """Serve the enhanced leaderboard page with multiple categories""" + import asyncio + + # Check rate limit first + allowed, rate_limit_message = self.check_rate_limit() + if not allowed: + self.send_rate_limit_error(rate_limit_message) + return + + # Get database instance + database = self.server.database if hasattr(self.server, 'database') else None + + if not database: + self.serve_error_page("Leaderboard", "Database not available") + return + + try: + # Run async database operations in event loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Get all leaderboard data + leaderboard_data = loop.run_until_complete(self.get_leaderboard_data(database)) + + # Generate HTML content + content = self.generate_leaderboard_content(leaderboard_data) + + html_content = self.get_page_template("Leaderboard - PetBot", content, "leaderboard") + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_content.encode()) + + except Exception as e: + print(f"Error generating leaderboard: {e}") + self.serve_error_page("Leaderboard", f"Error loading leaderboard data: {str(e)}") + finally: + loop.close() + + async def get_leaderboard_data(self, database): + """Get all leaderboard data for different categories""" + leaderboard_data = {} + + # 1. Top Players by Level + leaderboard_data['levels'] = await self.get_level_leaderboard(database) + + # 2. Top Players by Experience + leaderboard_data['experience'] = await self.get_experience_leaderboard(database) + + # 3. Richest Players + leaderboard_data['money'] = await self.get_money_leaderboard(database) + + # 4. Most Pets Collected + leaderboard_data['pet_count'] = await self.get_pet_count_leaderboard(database) + + # 5. Most Achievements + leaderboard_data['achievements'] = await self.get_achievement_leaderboard(database) + + # 6. Gym Champions (most gym badges) + leaderboard_data['gym_badges'] = await self.get_gym_badge_leaderboard(database) + + # 7. Highest Level Pet + leaderboard_data['highest_pet'] = await self.get_highest_pet_leaderboard(database) + + # 8. Most Rare Pets + leaderboard_data['rare_pets'] = await self.get_rare_pet_leaderboard(database) + + return leaderboard_data + + async def get_level_leaderboard(self, database): + """Get top players by level""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT nickname, level, experience + FROM players + ORDER BY level DESC, experience DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "level": p[1], "experience": p[2]} for p in players] + + async def get_experience_leaderboard(self, database): + """Get top players by total experience""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT nickname, level, experience + FROM players + ORDER BY experience DESC, level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "level": p[1], "experience": p[2]} for p in players] + + async def get_money_leaderboard(self, database): + """Get richest players""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT nickname, money, level + FROM players + ORDER BY money DESC, level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "money": p[1], "level": p[2]} for p in players] + + async def get_pet_count_leaderboard(self, database): + """Get players with most pets""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT p.nickname, COUNT(pets.id) as pet_count, p.level + FROM players p + LEFT JOIN pets ON p.id = pets.player_id + GROUP BY p.id, p.nickname, p.level + ORDER BY pet_count DESC, p.level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "pet_count": p[1], "level": p[2]} for p in players] + + async def get_achievement_leaderboard(self, database): + """Get players with most achievements""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT p.nickname, COUNT(pa.achievement_id) as achievement_count, p.level + FROM players p + LEFT JOIN player_achievements pa ON p.id = pa.player_id + GROUP BY p.id, p.nickname, p.level + ORDER BY achievement_count DESC, p.level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "achievement_count": p[1], "level": p[2]} for p in players] + + async def get_gym_badge_leaderboard(self, database): + """Get players with most gym victories (substitute for badges)""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + # Check if player_gym_battles table exists and has data + cursor = await db.execute(""" + SELECT p.nickname, + COALESCE(COUNT(DISTINCT CASE WHEN pgb.victories > 0 THEN pgb.gym_id END), 0) as gym_victories, + p.level + FROM players p + LEFT JOIN player_gym_battles pgb ON p.id = pgb.player_id + GROUP BY p.id, p.nickname, p.level + ORDER BY gym_victories DESC, p.level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "badge_count": p[1], "level": p[2]} for p in players] + + async def get_highest_pet_leaderboard(self, database): + """Get players with highest level pets""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT p.nickname, MAX(pets.level) as highest_pet_level, + ps.name as pet_species, p.level as player_level + FROM players p + JOIN pets ON p.id = pets.player_id + JOIN pet_species ps ON pets.species_id = ps.id + GROUP BY p.id, p.nickname, p.level + ORDER BY highest_pet_level DESC, p.level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "highest_pet_level": p[1], "pet_species": p[2], "player_level": p[3]} for p in players] + + async def get_rare_pet_leaderboard(self, database): + """Get players with most rare pets (epic/legendary)""" + import aiosqlite + async with aiosqlite.connect(database.db_path) as db: + cursor = await db.execute(""" + SELECT p.nickname, COUNT(pets.id) as rare_pet_count, p.level + FROM players p + JOIN pets ON p.id = pets.player_id + JOIN pet_species ps ON pets.species_id = ps.id + WHERE ps.rarity >= 4 + GROUP BY p.id, p.nickname, p.level + ORDER BY rare_pet_count DESC, p.level DESC + LIMIT 10 + """) + players = await cursor.fetchall() + + return [{"nickname": p[0], "rare_pet_count": p[1], "level": p[2]} for p in players] + + def generate_leaderboard_content(self, leaderboard_data): + """Generate HTML content for the enhanced leaderboard""" + content = """ +
+

🏆 PetBot Leaderboards

+

Compete with trainers across all categories!

+
+ +
+ + + + + + + + +
+ """ + + # Generate each leaderboard category + content += self.generate_leaderboard_category("levels", "🎯 Level Leaders", leaderboard_data['levels'], + ["Rank", "Player", "Level", "Experience"], + lambda p, i: [i+1, p['nickname'], p['level'], f"{p['experience']:,}"], True) + + content += self.generate_leaderboard_category("experience", "⭐ Experience Champions", leaderboard_data['experience'], + ["Rank", "Player", "Experience", "Level"], + lambda p, i: [i+1, p['nickname'], f"{p['experience']:,}", p['level']]) + + content += self.generate_leaderboard_category("money", "💰 Wealthiest Trainers", leaderboard_data['money'], + ["Rank", "Player", "Money", "Level"], + lambda p, i: [i+1, p['nickname'], f"${p['money']:,}", p['level']]) + + content += self.generate_leaderboard_category("pet_count", "🐾 Pet Collectors", leaderboard_data['pet_count'], + ["Rank", "Player", "Pet Count", "Level"], + lambda p, i: [i+1, p['nickname'], p['pet_count'], p['level']]) + + content += self.generate_leaderboard_category("achievements", "🏅 Achievement Hunters", leaderboard_data['achievements'], + ["Rank", "Player", "Achievements", "Level"], + lambda p, i: [i+1, p['nickname'], p['achievement_count'], p['level']]) + + content += self.generate_leaderboard_category("gym_badges", "🏛️ Gym Champions", leaderboard_data['gym_badges'], + ["Rank", "Player", "Gym Badges", "Level"], + lambda p, i: [i+1, p['nickname'], p['badge_count'], p['level']]) + + content += self.generate_leaderboard_category("highest_pet", "🌟 Elite Pet Trainers", leaderboard_data['highest_pet'], + ["Rank", "Player", "Highest Pet", "Species", "Player Level"], + lambda p, i: [i+1, p['nickname'], f"Lvl {p['highest_pet_level']}", p['pet_species'], p['player_level']]) + + content += self.generate_leaderboard_category("rare_pets", "💎 Rare Pet Masters", leaderboard_data['rare_pets'], + ["Rank", "Player", "Rare Pets", "Level"], + lambda p, i: [i+1, p['nickname'], p['rare_pet_count'], p['level']]) + + # Add JavaScript for category switching + content += """ + + + + """ + + return content + + def generate_leaderboard_category(self, category_id, title, data, headers, row_formatter, is_default=False): + """Generate HTML for a single leaderboard category""" + display_style = "block" if is_default else "none" + + content = f""" +
+

{title}

+ """ + + if not data or len(data) == 0: + content += '
No data available for this category yet.
' + else: + content += '' + + # Headers + content += '' + for header in headers: + content += f'' + content += '' + + # Data rows + content += '' + for i, player in enumerate(data): + row_data = row_formatter(player, i) + rank_class = f"rank-{i+1}" if i < 3 else "" + + content += f'' + for cell in row_data: + content += f'' + content += '' + content += '' + + content += '
{header}
{cell}
' + + content += '
' + return content def serve_locations(self): """Serve the locations page with real data""" @@ -2314,6 +2707,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
{item['description']}
Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}
+
💬 Use with: !use {item['name']}
""" else: inventory_html = """ @@ -2765,6 +3159,20 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): font-size: 0.9em; } + .item-command { + margin-top: 8px; + color: var(--text-accent); + font-size: 0.85em; + } + + .item-command code { + background: var(--bg-secondary); + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + color: var(--text-primary); + } + .empty-state { text-align: center; padding: 40px; @@ -4849,11 +5257,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"🔐 PIN for {nickname}: {pin_code}") # Try to send via IRC bot if available - if self.bot and hasattr(self.bot, 'send_message'): + if self.bot and hasattr(self.bot, 'send_message_sync'): 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.") + # Send PIN via private message using sync wrapper + self.bot.send_message_sync(nickname, f"🔐 Team Builder PIN: {pin_code}") + self.bot.send_message_sync(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}")