Enhance web interface and item system with user experience improvements

- Fix \!team command to redirect to team builder web page instead of showing IRC list
- Add usage commands to inventory items showing "\!use <item name>" 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 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-15 20:47:37 +00:00
parent 915aa00bea
commit 88e352ee79
4 changed files with 450 additions and 51 deletions

View file

@ -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",

View file

@ -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.")

View file

@ -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"""

View file

@ -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')
"""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 = """
<div class="header">
<h1>🏆 PetBot Leaderboards</h1>
<p>Compete with trainers across all categories!</p>
</div>
<div class="leaderboard-nav">
<button class="category-btn active" onclick="showCategory('levels')">🎯 Levels</button>
<button class="category-btn" onclick="showCategory('experience')"> Experience</button>
<button class="category-btn" onclick="showCategory('money')">💰 Wealth</button>
<button class="category-btn" onclick="showCategory('pet_count')">🐾 Pet Count</button>
<button class="category-btn" onclick="showCategory('achievements')">🏅 Achievements</button>
<button class="category-btn" onclick="showCategory('gym_badges')">🏛 Gym Badges</button>
<button class="category-btn" onclick="showCategory('highest_pet')">🌟 Highest Pet</button>
<button class="category-btn" onclick="showCategory('rare_pets')">💎 Rare Pets</button>
</div>
"""
# 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 += """
<script>
function showCategory(category) {
// Hide all categories
const categories = document.querySelectorAll('.leaderboard-category');
categories.forEach(cat => cat.style.display = 'none');
// Show selected category
document.getElementById(category).style.display = 'block';
// Update button states
const buttons = document.querySelectorAll('.category-btn');
buttons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
// Show first category by default
document.addEventListener('DOMContentLoaded', function() {
showCategory('levels');
});
</script>
<style>
.leaderboard-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0;
justify-content: center;
}
.category-btn {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
}
.category-btn:hover {
background: var(--hover-color);
border-color: var(--text-accent);
}
.category-btn.active {
background: var(--text-accent);
color: var(--bg-primary);
border-color: var(--text-accent);
}
.leaderboard-category {
margin: 20px 0;
display: none;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-secondary);
border-radius: 10px;
overflow: hidden;
box-shadow: var(--shadow-dark);
}
.leaderboard-table th {
background: var(--gradient-primary);
color: white;
padding: 15px;
text-align: left;
font-weight: bold;
}
.leaderboard-table td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.leaderboard-table tr:nth-child(even) {
background: var(--bg-tertiary);
}
.leaderboard-table tr:hover {
background: var(--hover-color);
}
.rank-1 { color: #FFD700; font-weight: bold; }
.rank-2 { color: #C0C0C0; font-weight: bold; }
.rank-3 { color: #CD7F32; font-weight: bold; }
.category-title {
color: var(--text-accent);
margin: 30px 0 15px 0;
font-size: 1.5em;
text-align: center;
}
.no-data {
text-align: center;
color: var(--text-secondary);
font-style: italic;
padding: 20px;
}
</style>
"""
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"""
<div id="{category_id}" class="leaderboard-category" style="display: {display_style};">
<h2 class="category-title">{title}</h2>
"""
if not data or len(data) == 0:
content += '<div class="no-data">No data available for this category yet.</div>'
else:
content += '<table class="leaderboard-table">'
# Headers
content += '<thead><tr>'
for header in headers:
content += f'<th>{header}</th>'
content += '</tr></thead>'
# Data rows
content += '<tbody>'
for i, player in enumerate(data):
row_data = row_formatter(player, i)
rank_class = f"rank-{i+1}" if i < 3 else ""
content += f'<tr class="{rank_class}">'
for cell in row_data:
content += f'<td>{cell}</td>'
content += '</tr>'
content += '</tbody>'
content += '</table>'
content += '</div>'
return content
def serve_locations(self):
"""Serve the locations page with real data"""
@ -2314,6 +2707,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
</div>
<div class="item-description">{item['description']}</div>
<div class="item-meta">Category: {item['category'].replace('_', ' ').title()} | Rarity: {item['rarity'].title()}</div>
<div class="item-command">💬 Use with: <code>!use {item['name']}</code></div>
</div>"""
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}")