Implement comprehensive team builder configuration system

### Major Features Added:

**Team Configuration Management:**
- Add support for 3 different team configurations per player
- Implement save/load/rename functionality for team setups
- Add prominent team configuration UI with dropdown selector
- Create quick action buttons for instant configuration loading
- Add status tracking for current editing state

**Database Enhancements:**
- Add team_configurations table with player_id, slot_number, config_name
- Implement rename_team_configuration() method for configuration management
- Add proper indexing and foreign key constraints

**Web Interface Improvements:**
- Fix team builder template visibility issue (was using wrong template)
- Add comprehensive CSS styling for configuration elements
- Implement responsive design with proper hover effects and transitions
- Add visual feedback with status indicators and progress tracking

**API Endpoints:**
- Add /teambuilder/{nickname}/config/rename/{slot} for renaming configs
- Implement proper validation and error handling for all endpoints
- Add JSON response formatting with success/error states

**JavaScript Functionality:**
- Add switchActiveConfig() for configuration switching
- Implement quickSaveConfig() for instant team saving
- Add renameConfig() with user-friendly prompts
- Create loadConfigToEdit() for seamless configuration editing

**Admin & System Improvements:**
- Enhance weather command system with simplified single-word names
- Fix \!reload command to properly handle all 12 modules
- Improve IRC connection health monitoring with better PONG detection
- Add comprehensive TODO list tracking and progress management

**UI/UX Enhancements:**
- Position team configuration section prominently for maximum visibility
- Add clear instructions: "Save up to 3 different team setups for quick switching"
- Implement intuitive workflow: save → load → edit → rename → resave
- Add visual hierarchy with proper spacing and typography

### Technical Details:

**Problem Solved:**
- Team configuration functionality existed but was hidden in unused template
- Users reported "no indication i can save multiple team configs"
- Configuration management was not visible or accessible

**Solution:**
- Identified dual template system in webserver.py (line 4160 vs 5080)
- Added complete configuration section to actively used template
- Enhanced both CSS styling and JavaScript functionality
- Implemented full backend API support with database persistence

**Files Modified:**
- webserver.py: +600 lines (template fixes, API endpoints, CSS, JavaScript)
- src/database.py: +20 lines (rename_team_configuration method)
- modules/admin.py: +150 lines (weather improvements, enhanced commands)
- TODO.md: Updated progress tracking and completed items

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-15 22:40:23 +00:00
parent 6cd25ab9b1
commit add7731d80
7 changed files with 1434 additions and 51 deletions

22
TODO.md
View file

@ -3,10 +3,10 @@
This file tracks completed work, pending bugs, enhancements, and feature ideas for the PetBot project.
## 📊 Summary
- **✅ Completed**: 17 items
- **✅ Completed**: 18 items
- **🐛 Bugs**: 0 items
- **🔧 Enhancements**: 3 items
- **💡 Ideas**: 10 items
- **💡 Ideas**: 9 items
- **📋 Total**: 30 items tracked
---
@ -163,6 +163,13 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f
- Optimize web interface assets and loading times
- Implement caching where appropriate
- [ ] **Improve admin weather control system**
- Enhanced argument parsing for more intuitive command usage
- Better error messages and validation feedback
- Add weather presets and quick-change options
- Implement weather history and logging
- Add bulk weather operations for multiple locations
---
## 💡 FEATURE IDEAS
@ -173,10 +180,13 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f
- Touch-friendly drag-and-drop alternatives
- Mobile-optimized navigation and layouts
- [ ] **Enhance leaderboard with more categories (gym badges, rare pets, achievements)**
- Multiple leaderboard categories
- Filtering and sorting options
- Achievement-based rankings
- [x] **Enhance leaderboard with more categories (gym badges, rare pets, achievements)**
- ✅ Multiple leaderboard categories with 8 different rankings
- ✅ Interactive category switching with responsive navigation
- ✅ Achievement-based rankings and specialized stats
- ✅ Comprehensive player statistics (Level, Experience, Money, Pet Count, Achievements, Gym Badges, Highest Pet, Rare Pets)
- ✅ Responsive design with gold/silver/bronze highlighting for top 3
- ✅ Real-time data from database with proper SQL optimization
- [ ] **Add auto-save draft functionality to team builder to prevent data loss**
- Local storage for unsaved team changes

View file

@ -1,36 +1,36 @@
{
"weather_types": {
"Sunny": {
"sunny": {
"description": "Bright sunshine increases Fire and Grass-type spawns",
"spawn_modifier": 1.5,
"affected_types": ["Fire", "Grass"],
"duration_minutes": [60, 120]
},
"Rainy": {
"rainy": {
"description": "Heavy rain boosts Water-type spawns significantly",
"spawn_modifier": 2.0,
"affected_types": ["Water"],
"duration_minutes": [45, 90]
},
"Thunderstorm": {
"storm": {
"description": "Electric storms double Electric-type spawn rates",
"spawn_modifier": 2.0,
"affected_types": ["Electric"],
"duration_minutes": [30, 60]
},
"Blizzard": {
"blizzard": {
"description": "Harsh snowstorm increases Ice and Water-type spawns",
"spawn_modifier": 1.7,
"affected_types": ["Ice", "Water"],
"duration_minutes": [60, 120]
},
"Earthquake": {
"earthquake": {
"description": "Ground tremors bring Rock-type pets to the surface",
"spawn_modifier": 1.8,
"affected_types": ["Rock"],
"duration_minutes": [30, 90]
},
"Calm": {
"calm": {
"description": "Perfect weather with normal spawn rates",
"spawn_modifier": 1.0,
"affected_types": [],
@ -38,11 +38,11 @@
}
},
"location_weather_chances": {
"Starter Town": ["Sunny", "Calm", "Rainy"],
"Whispering Woods": ["Sunny", "Rainy", "Calm"],
"Electric Canyon": ["Thunderstorm", "Sunny", "Calm"],
"Crystal Caves": ["Earthquake", "Calm"],
"Frozen Tundra": ["Blizzard", "Calm"],
"Dragon's Peak": ["Thunderstorm", "Sunny", "Calm"]
"Starter Town": ["sunny", "calm", "rainy"],
"Whispering Woods": ["sunny", "rainy", "calm"],
"Electric Canyon": ["storm", "sunny", "calm"],
"Crystal Caves": ["earthquake", "calm"],
"Frozen Tundra": ["blizzard", "calm"],
"Dragon's Peak": ["storm", "sunny", "calm"]
}
}

View file

@ -21,7 +21,7 @@ class Admin(BaseModule):
"""Handles admin-only commands like reload"""
def get_commands(self):
return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset"]
return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather"]
async def handle_command(self, channel, nickname, command, args):
if command == "reload":
@ -34,6 +34,10 @@ class Admin(BaseModule):
await self.cmd_rate_unban(channel, nickname, args)
elif command == "rate_reset":
await self.cmd_rate_reset(channel, nickname, args)
elif command == "weather":
await self.cmd_weather(channel, nickname, args)
elif command == "setweather":
await self.cmd_setweather(channel, nickname, args)
async def cmd_reload(self, channel, nickname):
"""Reload bot modules (admin only)"""
@ -168,4 +172,149 @@ class Admin(BaseModule):
self.send_message(channel, f"{nickname}: No violations found for user {target_user}.")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error resetting violations: {str(e)}")
self.send_message(channel, f"{nickname}: ❌ Error resetting violations: {str(e)}")
async def cmd_weather(self, channel, nickname, args):
"""Check current weather in locations (admin only)"""
if not self.is_admin(nickname):
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
return
try:
if args and args[0].lower() != "all":
# Check weather for specific location
location_name = " ".join(args)
weather = await self.database.get_location_weather_by_name(location_name)
if weather:
self.send_message(channel,
f"🌤️ {nickname}: {location_name} - {weather['weather_type']} "
f"(modifier: {weather['spawn_modifier']}x, "
f"until: {weather['active_until'][:16]})")
else:
self.send_message(channel, f"{nickname}: Location '{location_name}' not found or no weather data.")
else:
# Show weather for all locations
all_weather = await self.database.get_all_location_weather()
if all_weather:
weather_info = []
for w in all_weather:
weather_info.append(f"{w['location_name']}: {w['weather_type']} ({w['spawn_modifier']}x)")
self.send_message(channel, f"🌤️ {nickname}: Current weather - " + " | ".join(weather_info))
else:
self.send_message(channel, f"{nickname}: No weather data available.")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error checking weather: {str(e)}")
async def cmd_setweather(self, channel, nickname, args):
"""Force change weather in a location or all locations (admin only)"""
if not self.is_admin(nickname):
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
return
if not args:
self.send_message(channel,
f"{nickname}: Usage: !setweather <location|all> <weather_type> [duration_minutes]\n"
f"Weather types: sunny, rainy, storm, blizzard, earthquake, calm")
return
try:
import json
import random
import datetime
# Load weather patterns
with open("config/weather_patterns.json", "r") as f:
weather_data = json.load(f)
weather_types = list(weather_data["weather_types"].keys())
# Smart argument parsing - check if any arg is a weather type
location_arg = None
weather_type = None
duration = None
for i, arg in enumerate(args):
if arg.lower() in weather_types:
weather_type = arg.lower()
# Remove weather type from args for location parsing
remaining_args = args[:i] + args[i+1:]
break
if not weather_type:
self.send_message(channel, f"{nickname}: Please specify a valid weather type.")
return
# Parse location from remaining args
if remaining_args:
if remaining_args[0].lower() == "all":
location_arg = "all"
# Check if there's a duration after "all"
if len(remaining_args) > 1:
try:
duration = int(remaining_args[1])
except ValueError:
pass
else:
# Location name (might be multiple words)
location_words = []
for arg in remaining_args:
try:
# If it's a number, it's probably duration
duration = int(arg)
break
except ValueError:
# It's part of location name
location_words.append(arg)
location_arg = " ".join(location_words) if location_words else "all"
else:
location_arg = "all"
weather_config = weather_data["weather_types"][weather_type]
# Calculate duration
if not duration:
duration_range = weather_config.get("duration_minutes", [90, 180])
duration = random.randint(duration_range[0], duration_range[1])
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration)
if location_arg.lower() == "all":
# Set weather for all locations
success = await self.database.set_weather_all_locations(
weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
)
if success:
self.send_message(channel,
f"🌤️ {nickname}: Set {weather_type} weather for ALL locations! "
f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x")
else:
self.send_message(channel, f"{nickname}: Failed to set weather for all locations.")
else:
# Set weather for specific location
location_name = location_arg if len(args) == 2 else " ".join(args[:-1])
success = await self.database.set_weather_for_location(
location_name, weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
)
if success:
self.send_message(channel,
f"🌤️ {nickname}: Set {weather_type} weather for {location_name}! "
f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x")
else:
self.send_message(channel, f"{nickname}: Failed to set weather for '{location_name}'. Location may not exist.")
except FileNotFoundError:
self.send_message(channel, f"{nickname}: ❌ Weather configuration file not found.")
except ValueError as e:
self.send_message(channel, f"{nickname}: ❌ Invalid duration: {str(e)}")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error setting weather: {str(e)}")

View file

@ -161,20 +161,52 @@ class PetBotDebug:
async def reload_modules(self):
"""Reload all modules (for admin use)"""
try:
# Reload module files
import modules
importlib.reload(modules.core_commands)
importlib.reload(modules.exploration)
importlib.reload(modules.battle_system)
importlib.reload(modules.pet_management)
importlib.reload(modules.achievements)
importlib.reload(modules.admin)
importlib.reload(modules)
# Reinitialize modules
print("🔄 Reloading modules...")
# Import all module files
import modules.core_commands
import modules.exploration
import modules.battle_system
import modules.pet_management
import modules.achievements
import modules.admin
import modules.inventory
import modules.gym_battles
import modules.team_builder
import modules.backup_commands
import modules.connection_monitor
import modules.base_module
import modules
# Reload each module individually with error handling
modules_to_reload = [
('base_module', modules.base_module),
('core_commands', modules.core_commands),
('exploration', modules.exploration),
('battle_system', modules.battle_system),
('pet_management', modules.pet_management),
('achievements', modules.achievements),
('admin', modules.admin),
('inventory', modules.inventory),
('gym_battles', modules.gym_battles),
('team_builder', modules.team_builder),
('backup_commands', modules.backup_commands),
('connection_monitor', modules.connection_monitor),
('modules', modules)
]
for module_name, module_obj in modules_to_reload:
try:
importlib.reload(module_obj)
print(f" ✅ Reloaded {module_name}")
except Exception as e:
print(f" ❌ Failed to reload {module_name}: {e}")
# Clear and reinitialize module instances
self.modules = {}
self.load_modules()
print("✅ Modules reloaded successfully")
print("✅ All modules reloaded successfully")
return True
except Exception as e:
print(f"❌ Module reload failed: {e}")

View file

@ -355,6 +355,20 @@ class Database:
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS team_configurations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
config_name TEXT NOT NULL,
slot_number INTEGER NOT NULL,
team_data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players (id),
UNIQUE(player_id, slot_number)
)
""")
# Create species_moves table for move learning system
await db.execute("""
CREATE TABLE IF NOT EXISTS species_moves (
@ -1879,4 +1893,167 @@ class Database:
"total_pets": result[0],
"active_pets": result[1],
"storage_pets": result[2]
}
}
# Weather Management Methods
async def get_location_weather_by_name(self, location_name: str) -> Optional[Dict]:
"""Get current weather for a location by name"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT lw.*, l.name as location_name
FROM location_weather lw
JOIN locations l ON lw.location_id = l.id
WHERE l.name = ? AND lw.active_until > datetime('now')
ORDER BY lw.id DESC LIMIT 1
""", (location_name,))
row = await cursor.fetchone()
return dict(row) if row else None
async def get_all_location_weather(self) -> List[Dict]:
"""Get current weather for all locations"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT lw.*, l.name as location_name
FROM location_weather lw
JOIN locations l ON lw.location_id = l.id
WHERE lw.active_until > datetime('now')
ORDER BY l.name
""")
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def set_weather_all_locations(self, weather_type: str, active_until: str,
spawn_modifier: float, affected_types: str) -> bool:
"""Set weather for all locations"""
try:
async with aiosqlite.connect(self.db_path) as db:
# Clear existing weather
await db.execute("DELETE FROM location_weather")
# Get all location IDs
cursor = await db.execute("SELECT id FROM locations")
location_ids = [row[0] for row in await cursor.fetchall()]
# Set new weather for all locations
for location_id in location_ids:
await db.execute("""
INSERT INTO location_weather
(location_id, weather_type, active_until, spawn_modifier, affected_types)
VALUES (?, ?, ?, ?, ?)
""", (location_id, weather_type, active_until, spawn_modifier, affected_types))
await db.commit()
return True
except Exception as e:
print(f"Error setting weather for all locations: {e}")
return False
async def set_weather_for_location(self, location_name: str, weather_type: str,
active_until: str, spawn_modifier: float,
affected_types: str) -> bool:
"""Set weather for a specific location"""
try:
async with aiosqlite.connect(self.db_path) as db:
# Get location ID
cursor = await db.execute("SELECT id FROM locations WHERE name = ?", (location_name,))
location_row = await cursor.fetchone()
if not location_row:
return False
location_id = location_row[0]
# Clear existing weather for this location
await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location_id,))
# Set new weather
await db.execute("""
INSERT INTO location_weather
(location_id, weather_type, active_until, spawn_modifier, affected_types)
VALUES (?, ?, ?, ?, ?)
""", (location_id, weather_type, active_until, spawn_modifier, affected_types))
await db.commit()
return True
except Exception as e:
print(f"Error setting weather for location {location_name}: {e}")
return False
# Team Configuration Methods
async def save_team_configuration(self, player_id: int, slot_number: int, config_name: str, team_data: str) -> bool:
"""Save a team configuration to a specific slot (1-3)"""
try:
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
INSERT OR REPLACE INTO team_configurations
(player_id, slot_number, config_name, team_data, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (player_id, slot_number, config_name, team_data))
await db.commit()
return True
except Exception as e:
print(f"Error saving team configuration: {e}")
return False
async def get_team_configurations(self, player_id: int) -> List[Dict]:
"""Get all team configurations for a player"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT slot_number, config_name, team_data, created_at, updated_at
FROM team_configurations
WHERE player_id = ?
ORDER BY slot_number
""", (player_id,))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def load_team_configuration(self, player_id: int, slot_number: int) -> Optional[Dict]:
"""Load a specific team configuration"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT config_name, team_data, updated_at
FROM team_configurations
WHERE player_id = ? AND slot_number = ?
""", (player_id, slot_number))
row = await cursor.fetchone()
return dict(row) if row else None
async def delete_team_configuration(self, player_id: int, slot_number: int) -> bool:
"""Delete a team configuration"""
try:
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("""
DELETE FROM team_configurations
WHERE player_id = ? AND slot_number = ?
""", (player_id, slot_number))
await db.commit()
return cursor.rowcount > 0
except Exception as e:
print(f"Error deleting team configuration: {e}")
return False
async def rename_team_configuration(self, player_id: int, slot_number: int, new_name: str) -> bool:
"""Rename a team configuration"""
try:
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("""
UPDATE team_configurations
SET config_name = ?, updated_at = CURRENT_TIMESTAMP
WHERE player_id = ? AND slot_number = ?
""", (new_name, player_id, slot_number))
await db.commit()
return cursor.rowcount > 0
except Exception as e:
print(f"Error renaming team configuration: {e}")
return False

View file

@ -35,7 +35,7 @@ class IRCConnectionManager:
self.last_ping_time = 0
self.last_pong_time = 0
self.ping_interval = 60 # Send PING every 60 seconds
self.ping_timeout = 120 # Expect PONG within 2 minutes
self.ping_timeout = 180 # Expect PONG within 3 minutes
# Reconnection settings
self.reconnect_attempts = 0
@ -242,10 +242,15 @@ class IRCConnectionManager:
if line.startswith("PING"):
pong_response = line.replace("PING", "PONG")
await self._send_raw(pong_response)
# Server-initiated PING also counts as activity
self.last_pong_time = time.time()
self.logger.debug(f"Server PING received and replied: {line}")
return
if line.startswith("PONG"):
# Improved PONG detection - handle various formats
if line.startswith("PONG") or " PONG " in line:
self.last_pong_time = time.time()
self.logger.debug(f"PONG received: {line}")
return
# Handle connection completion
@ -324,16 +329,29 @@ class IRCConnectionManager:
# Send ping if interval has passed
if current_time - self.last_ping_time > self.ping_interval:
try:
await self._send_raw(f"PING :health_check_{int(current_time)}")
ping_message = f"PING :health_check_{int(current_time)}"
await self._send_raw(ping_message)
self.last_ping_time = current_time
self.logger.debug(f"Sent health check ping: {ping_message}")
except Exception as e:
self.logger.error(f"Failed to send ping: {e}")
raise ConnectionError("Health check ping failed")
# Check if we've received a pong recently
if current_time - self.last_pong_time > self.ping_timeout:
self.logger.warning("No PONG received within timeout period")
raise ConnectionError("Ping timeout - connection appears dead")
# Check if we've received a pong recently (only if we've sent a ping)
time_since_last_ping = current_time - self.last_ping_time
time_since_last_pong = current_time - self.last_pong_time
# Only check for PONG timeout if we've sent a ping and enough time has passed
if time_since_last_ping < self.ping_interval and time_since_last_pong > self.ping_timeout:
# Add more lenient check - only fail if we've had no activity at all
time_since_last_message = current_time - self.last_message_time
if time_since_last_message > self.ping_timeout:
self.logger.warning(f"No PONG received within timeout period. Last ping: {time_since_last_ping:.1f}s ago, Last pong: {time_since_last_pong:.1f}s ago, Last message: {time_since_last_message:.1f}s ago")
raise ConnectionError("Ping timeout - connection appears dead")
else:
# We're getting other messages, so connection is likely fine
self.logger.debug(f"No PONG but other messages received recently ({time_since_last_message:.1f}s ago)")
async def _handle_connection_error(self, error):
"""Handle connection errors and initiate reconnection."""

File diff suppressed because it is too large Load diff