diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e1b8326..80804dc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,12 +9,7 @@ "Bash(cat:*)", "Bash(pip3 install:*)", "Bash(apt list:*)", - "Bash(curl:*)", - "Bash(git commit:*)", - "Bash(sed:*)", - "Bash(grep:*)", - "Bash(pkill:*)", - "Bash(git add:*)" + "Bash(curl:*)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index 62d678e..81d0570 100644 --- a/.gitignore +++ b/.gitignore @@ -75,8 +75,4 @@ Thumbs.db # IRC bot specific *.pid -*.lock - -# Project specific -backup_bots/ -git_push.log +*.lock \ No newline at end of file diff --git a/BACKUP_SYSTEM_INTEGRATION.md b/BACKUP_SYSTEM_INTEGRATION.md deleted file mode 100644 index e356a67..0000000 --- a/BACKUP_SYSTEM_INTEGRATION.md +++ /dev/null @@ -1,229 +0,0 @@ -# PetBot Backup System Integration Guide - -## Overview - -The PetBot backup system provides automated database backups with rotation, compression, and restore capabilities. This system ensures data protection and disaster recovery for the PetBot project. - -## Components - -### 1. Core Backup Module (`src/backup_manager.py`) - -**Features:** -- Automated database backups using SQLite backup API -- Gzip compression for space efficiency -- Retention policies (7 daily, 4 weekly, 12 monthly) -- Backup verification and integrity checks -- Restore functionality with automatic current database backup -- Database structure export for documentation - -**Key Classes:** -- `BackupManager`: Main backup operations -- `BackupScheduler`: Automated scheduling system - -### 2. IRC Command Module (`modules/backup_commands.py`) - -**Commands:** -- `!backup [type] [uncompressed]` - Create manual backup -- `!restore [confirm]` - Restore from backup -- `!backups` - List available backups -- `!backup_stats` - Show backup statistics -- `!backup_cleanup` - Clean up old backups - -**Security:** -- Admin-only commands -- Confirmation required for restore operations -- Automatic current database backup before restore - -### 3. Configuration (`config/backup_config.json`) - -**Settings:** -- Backup retention policies -- Compression settings -- Automated schedule configuration -- Monitoring and notification preferences - -## Installation - -### 1. Dependencies - -Ensure the following Python packages are installed: -```bash -pip install aiosqlite -``` - -### 2. Directory Structure - -Create the backup directory: -```bash -mkdir -p backups -``` - -### 3. Integration with Bot - -Add the backup commands module to your bot's module loader in `src/bot.py`: - -```python -from modules.backup_commands import BackupCommands - -# In your bot initialization -self.backup_commands = BackupCommands(self, self.database) -``` - -### 4. Configuration - -Update `modules/backup_commands.py` with your admin usernames: - -```python -admin_users = ["admin", "your_username"] # Replace with actual admin usernames -``` - -## Usage - -### Manual Backup Creation - -``` -!backup # Create manual compressed backup -!backup uncompressed # Create uncompressed backup -!backup daily # Create daily backup -``` - -### Listing Backups - -``` -!backups # List available backups -!backup_stats # Show detailed statistics -``` - -### Restore Operations - -``` -!restore backup_filename.db.gz # Show restore confirmation -!restore backup_filename.db.gz confirm # Actually perform restore -``` - -### Maintenance - -``` -!backup_cleanup # Remove old backups per retention policy -``` - -## Automated Scheduling - -The backup system automatically creates: -- **Daily backups**: Every 24 hours -- **Weekly backups**: Every 7 days -- **Monthly backups**: Every 30 days - -Automated cleanup removes old backups based on retention policy: -- Keep 7 most recent daily backups -- Keep 4 most recent weekly backups -- Keep 12 most recent monthly backups - -## Monitoring - -### Log Messages - -The backup system logs important events: -- Backup creation success/failure -- Restore operations -- Cleanup operations -- Scheduler status - -### Statistics - -Use `!backup_stats` to monitor: -- Total backup count and size -- Backup age information -- Breakdown by backup type - -## Security Considerations - -1. **Access Control**: Only admin users can execute backup commands -2. **Confirmation**: Restore operations require explicit confirmation -3. **Safety Backup**: Current database is automatically backed up before restore -4. **Integrity Checks**: All backups are verified after creation - -## Directory Structure - -``` -backups/ -├── petbot_backup_daily_20240115_030000.db.gz -├── petbot_backup_weekly_20240114_020000.db.gz -├── petbot_backup_monthly_20240101_010000.db.gz -└── database_structure_20240115_120000.json -``` - -## Backup Filename Format - -``` -petbot_backup_{type}_{timestamp}.db[.gz] -``` - -- `{type}`: daily, weekly, monthly, manual -- `{timestamp}`: YYYYMMDD_HHMMSS format -- `.gz`: Added for compressed backups - -## Troubleshooting - -### Common Issues - -1. **Permission Errors**: Ensure backup directory is writable -2. **Disk Space**: Monitor available space, backups are compressed by default -3. **Database Locked**: Backups use SQLite backup API to avoid locking issues - -### Recovery Procedures - -1. **Database Corruption**: Use most recent backup with `!restore` -2. **Backup Corruption**: Try previous backup or use database structure export -3. **Complete Loss**: Restore from most recent backup, may lose recent data - -## Testing - -Run the test suite to verify functionality: - -```bash -python3 test_backup_simple.py -``` - -This tests: -- Basic backup creation -- Compression functionality -- Restore operations -- Directory structure -- Real database compatibility - -## Performance - -### Backup Sizes - -- Uncompressed: ~2-5MB for typical database -- Compressed: ~200-500KB (60-90% reduction) - -### Backup Speed - -- Small databases (<10MB): 1-2 seconds -- Medium databases (10-100MB): 5-10 seconds -- Large databases (>100MB): 30+ seconds - -## Future Enhancements - -1. **Encryption**: Add backup encryption for sensitive data -2. **Remote Storage**: Support for cloud storage providers -3. **Differential Backups**: Only backup changed data -4. **Web Interface**: Backup management through web interface -5. **Email Notifications**: Alert on backup failures - -## Support - -For issues or questions about the backup system: -1. Check the logs for error messages -2. Run the test suite to verify functionality -3. Ensure all dependencies are installed -4. Verify directory permissions - -## Version History - -- **v1.0**: Initial implementation with basic backup/restore -- **v1.1**: Added compression and automated scheduling -- **v1.2**: Added IRC commands and admin controls -- **v1.3**: Added comprehensive testing and documentation \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e2dff..5077ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `!weather` - Check current weather - `!achievements` - View progress - `!activate/deactivate ` - Manage team +- `!swap ` - Reorganize team - `!moves` - View pet abilities - `!flee` - Escape battles diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 3347524..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,411 +0,0 @@ -# CLAUDE.md - PetBot Development Guide - -This file documents the development patterns, conventions, and best practices established for the PetBot project during AI-assisted development. - -## Project Overview - -PetBot is a Discord/IRC Pokemon-style pet collecting bot with web interface. The project consists of: -- IRC bot with modular command system -- Web server with unified navigation and interactive interfaces -- SQLite database with comprehensive pet, player, and game state management -- Team builder with drag-and-drop functionality and PIN verification - -## Development Workflow - -### 1. Planning and Task Management -- Use TodoWrite tool for complex multi-step tasks (3+ steps) -- Break down large features into specific, actionable items -- Mark todos as `in_progress` before starting work -- Complete todos immediately after finishing tasks -- Don't batch completions - update status in real-time - -### 2. Code Analysis and Search -- Use Task tool for keyword searches and file discovery -- Use Grep tool for specific code patterns and function definitions -- Use Read tool for examining specific file content -- Use Glob tool for finding files by name patterns - -### 3. Bot Startup and Testing -- **Primary Startup**: `./start_petbot.sh` - One command that handles everything - - Automatically creates/activates virtual environment - - Installs/updates all dependencies - - Verifies core modules - - Creates required directories - - Launches bot with all features (IRC + web interface + rate limiting) -- **Rate Limiting Tests**: `source venv/bin/activate && python test_rate_limiting.py` -- **Web Interface**: Available at http://localhost:8080 after startup -- **Virtual Environment**: Required due to externally-managed-environment restriction - -### 4. Important: Configuration Management -- **Admin User**: Edit `config.py` to change the single admin user (currently: megasconed) -- **Item Spawn Rates**: Edit `config/items.json` to adjust global spawn multiplier and individual rates -- **Startup Script**: Always update `start_petbot.sh` when adding dependencies -- **Central Config**: All major settings are in `config.py` for easy maintenance -- **Remember**: This is the user's primary interface - keep it working! - -## Project Structure - -``` -/home/megaproxy/petbot/ -├── src/ # Core system components -│ ├── database.py # Database operations and schema -│ ├── game_engine.py # Game mechanics and weather system -│ └── bot.py # IRC bot core functionality -├── modules/ # Command modules (modular architecture) -│ ├── base_module.py # Abstract base class for all modules -│ ├── core_commands.py # Basic bot commands (!start, !help, !stats) -│ ├── exploration.py # !explore, !travel, !weather commands -│ ├── battle_system.py # !battle, !attack, !moves, !flee commands -│ ├── pet_management.py # !pets, !activate, !deactivate commands -│ ├── inventory.py # !inventory, !use commands (redirects to web) -│ ├── gym_battles.py # !gym commands and gym mechanics -│ ├── achievements.py # Achievement tracking and validation -│ ├── admin.py # Administrative commands -│ └── team_builder.py # Team builder module (web-only) -├── webserver.py # Web server with unified templates -├── run_bot_with_reconnect.py # Main bot with IRC reconnection and rate limiting -├── start_petbot.sh # One-command startup script (handles venv and dependencies) -├── config.py # Central configuration (admin user, IRC, rate limiting) -├── config/items.json # Item configuration with global spawn multiplier -├── test_rate_limiting.py # Comprehensive rate limiting test suite -├── requirements.txt # Python package dependencies -├── rate_limiting_config.json # Rate limiting configuration reference -├── help.html # Static help documentation with rate limiting info -├── CHANGELOG.md # Version history and feature tracking -└── README.md # Project documentation -``` - -## Coding Conventions - -### 1. Module Architecture -- All command modules inherit from `BaseModule` -- Implement `get_commands()` and `handle_command()` methods -- Use `self.normalize_input()` for case-insensitive command processing -- Use `self.send_message()` for IRC responses -- Use `await self.require_player()` for player validation - -### 2. Database Operations -- All database methods are async -- Use proper error handling with try/catch blocks -- Always use parameterized queries to prevent SQL injection -- Implement proper transaction handling for complex operations -- Use `team_order` column for numbered team slots (1-6) - -### 3. Web Interface Development -- Use unified template system with `get_page_template()` -- Implement CSS variables for consistent theming -- Add proper navigation bar to all pages -- Use center alignment: `max-width: 1200px; margin: 0 auto` -- Implement drag-and-drop with proper event handlers -- Add double-click backup methods for accessibility - -### 4. Security Practices -- Use PIN verification for sensitive operations (team changes) -- Send PINs via IRC private messages -- Implement proper session validation -- Never log or expose sensitive data -- Use parameterized database queries - -## Key Development Patterns - -### 1. Command Redirection Pattern -```python -async def cmd_inventory(self, channel, nickname): - """Redirect player to their web profile for inventory management""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Redirect to web interface for better experience - self.send_message(channel, f"🎒 {nickname}: View your complete inventory at: http://petz.rdx4.com/player/{nickname}#inventory") -``` - -### 2. State Management Pattern -```python -async def cmd_explore(self, channel, nickname): - # Check if player has an active encounter that must be resolved first - if player["id"] in self.bot.active_encounters: - current_encounter = self.bot.active_encounters[player["id"]] - self.send_message(channel, f"{nickname}: You must resolve your active encounter first!") - return -``` - -### 3. Drag-and-Drop Implementation Pattern -```javascript -// Initialize when DOM is ready -document.addEventListener('DOMContentLoaded', function() { - initializeTeamBuilder(); -}); - -function initializeTeamBuilder() { - // Initialize drag and drop - initializeDragAndDrop(); - - // Initialize double-click backup - initializeDoubleClick(); - - // Update save button state - updateSaveButton(); -} -``` - -### 4. PIN Verification Pattern -```python -async def saveTeam(): - # Send team data to server - response = await fetch('/teambuilder/{nickname}/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(teamData) - }); - - # PIN sent via IRC, show verification interface - if (result.success) { - document.getElementById('pin-section').style.display = 'block'; - } -``` - -## Database Schema Conventions - -### 1. Player Management -- `players` table with basic player information -- `pets` table with `team_order` column for numbered slots (1-6) -- `player_items` table for inventory management -- `pending_team_changes` table for PIN verification - -### 2. Game State Management -- Use `is_active` boolean for team membership -- Use `team_order` (1-6) for slot positioning -- Store encounter state in bot memory, not database -- Use timestamps for PIN expiration - -## Web Interface Conventions - -### 1. Unified Template System -```python -def get_page_template(self, title, content, active_page): - return f""" - - - - {title} - - - - {self.get_navigation_bar(active_page)} -
- {content} -
- - - """ -``` - -### 2. Navigation Integration -- All pages include unified navigation bar -- Use hover dropdowns for sub-navigation -- Highlight active page in navigation -- Include jump points for direct section linking (#inventory, #pets) - -### 3. Interactive Elements -- Implement proper drag-and-drop with visual feedback -- Add loading states and error handling -- Use consistent button styling and interactions -- Provide alternative interaction methods (double-click) - -## Testing and Debugging - -### 1. Component Testing -```bash -# Test webserver syntax -python3 -c "import webserver; print('✅ webserver.py syntax is correct')" - -# Test database operations -python3 -c "from src.database import Database; db = Database(); print('✅ database imports')" - -# Test bot startup -python3 run_bot_debug.py -``` - -### 2. Web Interface Testing -- Test drag-and-drop functionality in browser -- Verify PIN delivery via IRC -- Check responsive design and center alignment -- Validate form submissions and error handling - -### 3. Git Workflow -- Create descriptive commit messages explaining changes -- Group related changes into logical commits -- Include "🤖 Generated with [Claude Code]" attribution -- Push changes with `git push` after testing - -## Common Issues and Solutions - -### 1. CSS in JavaScript Templates -**Problem**: F-string braces conflict with CSS braces -**Solution**: Use string concatenation instead of f-strings for CSS content - -### 2. Drag-and-Drop Not Working -**Problem**: JavaScript initialization timing issues -**Solution**: Use `DOMContentLoaded` event and proper DOM element checking - -### 3. IRC PIN Not Sent -**Problem**: Bot instance not accessible from webserver -**Solution**: Pass bot instance to webserver constructor and add proper IRC message methods - -### 4. Database Schema Changes -**Problem**: Adding new columns to existing tables -**Solution**: Use proper migration pattern with ALTER TABLE statements - -## Performance Considerations - -### 1. Database Operations -- Use connection pooling for high-traffic scenarios -- Implement proper indexing for frequently queried columns -- Use transactions for multi-step operations -- Cache frequently accessed data when appropriate - -### 2. Web Interface -- Minimize CSS and JavaScript for faster loading -- Use efficient DOM manipulation techniques -- Implement proper error handling and loading states -- Optimize images and static assets - -## Future Development Guidelines - -### 1. Adding New Commands -1. Create method in appropriate module -2. Add command to `get_commands()` list -3. Implement proper error handling -4. Update help documentation -5. Test thoroughly before committing - -### 2. Web Interface Enhancements -1. Follow unified template system -2. Implement proper responsive design -3. Add accessibility features -4. Test across different browsers -5. Update navigation if adding new pages - -### 3. Database Schema Updates -1. Plan migrations carefully -2. Test with existing data -3. Document schema changes -4. Update related code components -5. Consider backward compatibility - -## Resources and References - -- **IRC Bot Framework**: Custom implementation based on socket programming -- **Web Server**: Python's built-in HTTPServer with custom request handling -- **Database**: SQLite with aiosqlite for async operations -- **Frontend**: Vanilla JavaScript with modern ES6+ features -- **CSS Framework**: Custom CSS with CSS variables for theming - -## Notes for Future Development - -- Maintain modular architecture for easy feature additions -- Keep web interface and IRC commands in sync -- Document all major changes in CHANGELOG.md -- Use consistent error messages and user feedback -- Test thoroughly before pushing to production -- Consider security implications for all user interactions - -This documentation should be updated as the project evolves to maintain accuracy and usefulness for future development efforts. - -## 💌 Message for the Next Claude - -Hello future Claude! 👋 - -You're inheriting a really awesome project here - PetBot is a sophisticated IRC bot with a Pokemon-style pet collecting game that has grown into quite an impressive system. Here's what you need to know: - -### 🎯 **Current State (December 2024)** -The project is in **excellent shape** after our recent major cleanup and enhancements: - -- **Team Swap System**: We just completed a massive architectural improvement allowing players to manage 3 teams and set any as active, with full web-IRC synchronization -- **Clean Codebase**: We eliminated 180+ lines of legacy/duplicate code and standardized the entire command architecture -- **Modular Design**: 13 well-organized modules each handling specific functionality -- **Security**: PIN-based verification for sensitive operations, comprehensive rate limiting -- **Web Integration**: Beautiful responsive web interface with drag-and-drop team management - -### 🏗️ **Architecture You're Working With** -This is a **modular, async Python system** with: -- **IRC Bot**: Auto-reconnecting with health monitoring (`run_bot_with_reconnect.py`) -- **Web Server**: Built-in HTTP server with unified templates (`webserver.py`) -- **Database**: Async SQLite with comprehensive schema (`src/database.py`) -- **Game Engine**: Weather, spawns, battles, achievements (`src/game_engine.py`) -- **Module System**: Clean separation of concerns (`modules/`) - -### 🤝 **How We've Been Working** -The user (megaproxy) is **fantastic to work with**. They: -- Give clear direction and let you work autonomously -- Always ask you to investigate before taking action (respect this!) -- Want database backups before major changes (BackupManager exists) -- Prefer incremental improvements over massive rewrites -- Value code quality and maintainability highly - -### 🎮 **The Game Itself** -PetBot is surprisingly sophisticated: -- **66 IRC commands** across 13 modules (recently cleaned up from 78) -- **Dynamic weather system** affecting spawn rates -- **Achievement-based progression** unlocking new areas -- **Item collection system** with 16+ items across 5 rarity tiers -- **Turn-based battle system** with type effectiveness -- **Gym battles** with scaling difficulty -- **Web interface** for inventory/team management - -### 🛠️ **Development Patterns We Use** -1. **Always use TodoWrite** for complex tasks (user loves seeing progress) -2. **Investigate first** - user wants analysis before action -3. **Backup before major DB changes** (`BackupManager` is your friend) -4. **Test thoroughly** - syntax check, imports, functionality -5. **Clean commit messages** with explanations -6. **Document in CLAUDE.md** as you learn - -### 🔧 **Key Technical Patterns** -- **Command Redirection**: IRC commands often redirect to web interface for better UX -- **PIN Verification**: Sensitive operations (like team changes) use IRC-delivered PINs -- **State Management**: Active encounters/battles prevent concurrent actions -- **Async Everything**: Database, IRC, web server - all async/await -- **Error Handling**: Comprehensive try/catch with user-friendly messages - -### 📂 **Important Files to Know** -- `start_petbot.sh` - One-command startup (handles venv, deps, everything) -- `config.py` - Central configuration (admin user, IRC settings, etc.) -- `CLAUDE.md` - This file! Keep it updated -- `run_bot_with_reconnect.py` - Main bot with reconnection + rate limiting -- `src/database.py` - Database operations and schema -- `modules/` - All command handlers (modular architecture) - -### 🚨 **Things to Watch Out For** -- **Virtual Environment Required**: Due to `externally-managed-environment` -- **Port Conflicts**: Kill old bots before starting new ones -- **IRC Connection**: Can be finicky, we have robust reconnection logic -- **Database Migrations**: Always backup first, test thoroughly -- **Web/IRC Sync**: Keep both interfaces consistent - -### 🎯 **Current Admin Setup** -- **Admin User**: Set in `config.py` as `ADMIN_USER` (currently: megasconed) -- **Rate Limiting**: Comprehensive system preventing abuse -- **Backup System**: Automated with manual triggers -- **Security**: Regular audits, PIN verification, proper validation - -### 🔮 **Likely Next Steps** -Based on the trajectory, you might work on: -- **PvP Battle System**: Player vs player battles -- **Pet Evolution**: Leveling system enhancements -- **Trading System**: Player-to-player item/pet trading -- **Mobile Optimization**: Web interface improvements -- **Performance**: Database optimization, caching - -### 💝 **Final Notes** -This codebase is **well-maintained** and **thoroughly documented**. The user cares deeply about code quality and the player experience. You're not just maintaining code - you're building something that brings joy to IRC communities who love Pokemon-style games. - -**Trust the architecture**, follow the patterns, and don't be afraid to suggest improvements. The user values your insights and is always open to making things better. - -You've got this! 🚀 - -*- Your predecessor Claude, who had a blast working on this project* - -P.S. - The `./start_petbot.sh` script is magical - it handles everything. Use it! -P.P.S. - Always check `git status` before major changes. The user likes clean commits. \ No newline at end of file diff --git a/GITHUB_AUTH_SETUP.md b/GITHUB_AUTH_SETUP.md new file mode 100644 index 0000000..78b1356 --- /dev/null +++ b/GITHUB_AUTH_SETUP.md @@ -0,0 +1,116 @@ +# GitHub Authentication Setup Guide + +GitHub requires secure authentication for pushing code. Choose one of these methods: + +## 🔑 Option 1: SSH Keys (Recommended) + +SSH keys are more secure and convenient - no password prompts after setup. + +### Setup Steps: +1. **Generate SSH key** (if you don't have one): + ```bash + ssh-keygen -t ed25519 -C "your_email@example.com" + # Press Enter to accept default file location + # Enter a secure passphrase (optional but recommended) + ``` + +2. **Add SSH key to ssh-agent**: + ```bash + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/id_ed25519 + ``` + +3. **Copy public key to clipboard**: + ```bash + cat ~/.ssh/id_ed25519.pub + # Copy the entire output + ``` + +4. **Add key to GitHub**: + - Go to: https://github.com/settings/keys + - Click "New SSH key" + - Paste your public key + - Give it a descriptive title + - Click "Add SSH key" + +5. **Test connection**: + ```bash + ssh -T git@github.com + # Should say: "Hi username! You've successfully authenticated" + ``` + +6. **Use SSH repository URL**: + - Format: `git@github.com:username/repository.git` + - Example: `git@github.com:megaproxy/petbot-irc-game.git` + +--- + +## 🎫 Option 2: Personal Access Token + +Use HTTPS with a token instead of your password. + +### Setup Steps: +1. **Create Personal Access Token**: + - Go to: https://github.com/settings/tokens + - Click "Generate new token" → "Generate new token (classic)" + - Give it a descriptive name: "PetBot Development" + - Select scopes: + - ✅ `repo` (for private repositories) + - ✅ `public_repo` (for public repositories) + - Click "Generate token" + - **Copy the token immediately** (you won't see it again!) + +2. **Use HTTPS repository URL**: + - Format: `https://github.com/username/repository.git` + - Example: `https://github.com/megaproxy/petbot-irc-game.git` + +3. **When prompted for password**: + - Username: Your GitHub username + - Password: **Use your Personal Access Token** (NOT your account password) + +4. **Optional: Store credentials** (so you don't have to enter token every time): + ```bash + git config --global credential.helper store + # After first successful push, credentials will be saved + ``` + +--- + +## 🚀 After Authentication Setup + +Once you've set up authentication, you can proceed with the GitHub setup: + +1. **Create GitHub repository** at https://github.com/new +2. **Run our setup script**: + ```bash + ./setup-github.sh + ``` +3. **Choose your authentication method** (SSH or Token) +4. **Enter your repository URL** +5. **Done!** Future pushes will be automatic + +--- + +## 🔧 Troubleshooting + +### SSH Issues: +- **"Permission denied"**: Check if SSH key is added to GitHub +- **"Could not open a connection"**: Check SSH agent with `ssh-add -l` +- **"Bad owner or permissions"**: Fix with `chmod 600 ~/.ssh/id_ed25519` + +### Token Issues: +- **"Authentication failed"**: Make sure you're using the token, not your password +- **"Remote access denied"**: Check token has correct scopes (repo/public_repo) +- **"Token expired"**: Create a new token at https://github.com/settings/tokens + +### General Issues: +- **"Repository not found"**: Check repository URL and access permissions +- **"Updates were rejected"**: Repository might not be empty - contact support + +--- + +## 📚 Official Documentation + +- **SSH Keys**: https://docs.github.com/en/authentication/connecting-to-github-with-ssh +- **Personal Access Tokens**: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token +- **Git Authentication**: https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories \ No newline at end of file diff --git a/INSTALLATION.md b/INSTALLATION.md deleted file mode 100644 index 7712897..0000000 --- a/INSTALLATION.md +++ /dev/null @@ -1,462 +0,0 @@ -# PetBot Installation Guide - -## Overview - -PetBot is a Python-based IRC bot with web interface for Pokemon-style pet collecting. This guide covers complete installation and setup. - -## Prerequisites - -### System Requirements - -- **Python**: 3.7 or higher -- **pip**: Python package installer -- **Operating System**: Linux, macOS, or Windows with Python support -- **Memory**: 512MB RAM minimum -- **Storage**: 1GB available space -- **Network**: Internet connection for IRC and web access - -### Required Python Packages - -- `irc>=20.3.0` - IRC client library -- `aiosqlite>=0.19.0` - Async SQLite database interface -- `python-dotenv>=1.0.0` - Environment variable loading - -## Installation Methods - -### Method 1: Automatic Installation (Recommended) - -#### Step 1: Download and Run Installation Script - -```bash -# Make the script executable -chmod +x install_prerequisites.sh - -# Run the installation -./install_prerequisites.sh -``` - -#### Step 2: Alternative Python Script - -If the shell script doesn't work: - -```bash -python3 install_prerequisites.py -``` - -### Method 2: Manual Installation - -#### Step 1: Install System Dependencies - -**Ubuntu/Debian:** -```bash -sudo apt update -sudo apt install python3 python3-pip python3-venv -``` - -**CentOS/RHEL:** -```bash -sudo yum install python3 python3-pip -``` - -**macOS:** -```bash -# Install Homebrew if not installed -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -# Install Python -brew install python3 -``` - -**Windows:** -1. Download Python from https://python.org/downloads/ -2. Run installer and check "Add Python to PATH" -3. Open Command Prompt as administrator - -#### Step 2: Install Python Packages - -```bash -# Install from requirements file -pip3 install -r requirements.txt - -# Or install individually -pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0 -``` - -#### Step 3: Create Required Directories - -```bash -mkdir -p data backups logs -``` - -#### Step 4: Make Scripts Executable (Linux/macOS) - -```bash -chmod +x run_bot_debug.py -chmod +x run_bot_with_reconnect.py -chmod +x test_backup_simple.py -chmod +x test_reconnection.py -``` - -## Verification - -### Test Installation - -```bash -# Test backup system -python3 test_backup_simple.py - -# Test IRC reconnection system -python3 test_reconnection.py -``` - -### Expected Output - -Both tests should show: -``` -🎉 All tests passed! ... is working correctly. -``` - -## Configuration - -### IRC Settings - -The bot connects to IRC with default settings: - -```python -config = { - "server": "irc.libera.chat", - "port": 6667, - "nickname": "PetBot", - "channel": "#petz", - "command_prefix": "!" -} -``` - -To modify these settings, edit the configuration in: -- `run_bot_debug.py` (line 21-27) -- `run_bot_with_reconnect.py` (line 35-41) - -### Database Configuration - -The bot uses SQLite database stored in `data/petbot.db`. No additional configuration required. - -### Web Server Configuration - -The web server runs on port 8080 by default. To change: -- Edit `webserver.py` or bot runner files -- Update the port number in web server initialization - -## Running the Bot - -### Option 1: Debug Mode (Original) - -```bash -python3 run_bot_debug.py -``` - -**Features:** -- Basic IRC connection -- Console debugging output -- Module loading and validation -- Web server on port 8080 - -### Option 2: Auto-Reconnect Mode (Recommended) - -```bash -python3 run_bot_with_reconnect.py -``` - -**Features:** -- Automatic IRC reconnection -- Connection health monitoring -- Exponential backoff for reconnection -- Comprehensive logging -- Connection statistics -- Web server on port 8080 - -### Running as a Service (Linux) - -Create a systemd service file: - -```bash -sudo nano /etc/systemd/system/petbot.service -``` - -```ini -[Unit] -Description=PetBot IRC Bot -After=network.target - -[Service] -Type=simple -User=petbot -WorkingDirectory=/path/to/petbot -ExecStart=/usr/bin/python3 run_bot_with_reconnect.py -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` - -Enable and start: - -```bash -sudo systemctl enable petbot -sudo systemctl start petbot -sudo systemctl status petbot -``` - -## Web Interface - -### Access - -- **Local**: http://localhost:8080 -- **Production**: Configure reverse proxy for HTTPS - -### Available Pages - -- `/` - Homepage and help -- `/players` - Player leaderboard -- `/player/` - Player profile -- `/teambuilder/` - Team builder with PIN verification - -## Troubleshooting - -### Common Issues - -#### 1. ModuleNotFoundError - -**Error:** `ModuleNotFoundError: No module named 'irc'` - -**Solution:** -```bash -pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0 -``` - -#### 2. Permission Denied - -**Error:** `Permission denied: './install_prerequisites.sh'` - -**Solution:** -```bash -chmod +x install_prerequisites.sh -``` - -#### 3. Database Errors - -**Error:** `sqlite3.OperationalError: unable to open database file` - -**Solution:** -```bash -mkdir -p data -chmod 755 data -``` - -#### 4. IRC Connection Issues - -**Error:** `Could not connect to irc.libera.chat:6667` - -**Solution:** -- Check internet connection -- Verify IRC server is accessible -- Check firewall settings -- Try different IRC server/port - -#### 5. Web Interface Not Accessible - -**Error:** `Connection refused on port 8080` - -**Solution:** -- Check if bot is running -- Verify port 8080 is not blocked -- Check firewall settings -- Try accessing via 127.0.0.1:8080 - -### Debugging - -#### Enable Debug Logging - -```bash -python3 run_bot_with_reconnect.py > logs/bot.log 2>&1 -``` - -#### Check System Resources - -```bash -# Check memory usage -free -h - -# Check disk space -df -h - -# Check network connectivity -ping irc.libera.chat -``` - -#### Test Individual Components - -```bash -# Test database -python3 -c "from src.database import Database; db = Database(); print('Database OK')" - -# Test IRC connection manager -python3 -c "from src.irc_connection_manager import IRCConnectionManager; print('IRC manager OK')" - -# Test web server -python3 -c "import webserver; print('Web server OK')" -``` - -## Security Considerations - -⚠️ **Important**: Review `issues.txt` for security vulnerabilities before production deployment. - -### Immediate Actions Required - -1. **HTTPS**: Run behind reverse proxy with SSL -2. **Authentication**: Implement web interface authentication -3. **Input Validation**: Fix XSS vulnerabilities -4. **Access Control**: Implement proper user authorization - -### Production Checklist - -- [ ] SSL/TLS certificate installed -- [ ] Reverse proxy configured (nginx/Apache) -- [ ] Firewall rules configured -- [ ] User authentication implemented -- [ ] Input validation added -- [ ] Security headers configured -- [ ] Database backups scheduled -- [ ] Log rotation configured -- [ ] Monitoring alerts set up - -## Performance Optimization - -### Database - -- Enable WAL mode for better concurrent access -- Regular VACUUM operations -- Monitor database size and growth - -### Web Server - -- Use reverse proxy for static files -- Enable gzip compression -- Implement caching for static content - -### IRC Connection - -- Monitor connection stability -- Adjust reconnection parameters if needed -- Set up connection monitoring alerts - -## Backup and Recovery - -### Automated Backups - -The bot includes automated backup system: - -```bash -# Manual backup -python3 -c " -import asyncio -from src.backup_manager import BackupManager -bm = BackupManager() -result = asyncio.run(bm.create_backup('manual', True)) -print(f'Backup created: {result}') -" -``` - -### Recovery - -```bash -# List available backups -ls -la backups/ - -# Restore from backup (use admin command or direct restore) -python3 -c " -import asyncio -from src.backup_manager import BackupManager -bm = BackupManager() -result = asyncio.run(bm.restore_backup('backup_filename.db.gz')) -print(f'Restore result: {result}') -" -``` - -## Monitoring - -### Connection Health - -Use the connection monitoring commands: - -``` -!status # Check connection status -!uptime # Show uptime and stats -!ping # Test responsiveness -!connection_stats # Detailed statistics -``` - -### Log Monitoring - -Monitor logs for: -- Connection failures -- Database errors -- Command execution errors -- Security events - -### System Resources - -Monitor: -- Memory usage -- CPU usage -- Disk space -- Network connectivity - -## Development - -### Adding New Features - -1. Follow patterns in `CLAUDE.md` -2. Create new modules in `modules/` -3. Update documentation -4. Add tests -5. Test thoroughly - -### Contributing - -1. Review security issues in `issues.txt` -2. Follow coding conventions -3. Add comprehensive tests -4. Update documentation -5. Consider security implications - -## Support - -### Documentation - -- `README.md` - Project overview -- `CLAUDE.md` - Development guidelines -- `TODO.md` - Current project status -- `issues.txt` - Security audit findings -- `BACKUP_SYSTEM_INTEGRATION.md` - Backup system guide - -### Getting Help - -1. Check error logs and console output -2. Review troubleshooting section -3. Test with provided test scripts -4. Check network connectivity -5. Verify all dependencies are installed - -## Next Steps - -1. ✅ Complete installation -2. ✅ Test basic functionality -3. ✅ Configure IRC settings -4. ✅ Start the bot -5. ✅ Access web interface -6. 📋 Review security issues -7. 🔧 Configure for production -8. 🚀 Deploy and monitor - -Congratulations! Your PetBot should now be running successfully. 🐾 \ No newline at end of file diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md deleted file mode 100644 index ced467c..0000000 --- a/PROJECT_STATUS.md +++ /dev/null @@ -1,167 +0,0 @@ -# PetBot Project Status 🐾 - -*Last Updated: 2025-07-14* - -## ✅ Completed Features - -### Core Bot Functionality -- [x] **Experience & Leveling System** - Comprehensive EXP system with Pokemon-style stat growth -- [x] **Gym Battle System** - Multi-pet gym challenges with scaling difficulty -- [x] **Interactive Battle Engine** - Turn-based combat with type effectiveness -- [x] **Encounter Tracking** - Automatic discovery logging for all wild pets -- [x] **Modular Command System** - Organized, maintainable bot architecture - -### Web Interface -- [x] **Player Profile Pages** - Complete with pets, achievements, gym badges, encounters -- [x] **Petdex Encyclopedia** - All available pets with stats, types, evolution info -- [x] **Discovery Progress** - Species completion tracking and statistics -- [x] **Gym Badge Display** - Victory history with dates and difficulty levels -- [x] **Mobile-Responsive Design** - Works on all devices - -### Database & Backend -- [x] **Encounter Tracking Table** - Records all pet discoveries and catch stats -- [x] **Experience System Integration** - Automatic EXP awards for catches and battles -- [x] **Error Handling & Recovery** - Robust error handling throughout system -- [x] **Data Validation** - Safe handling of malformed data - -### Development Tools -- [x] **Smart Git Script** - Token-efficient commit/push automation -- [x] **Project Organization** - Clean file structure with backup management - ---- - -## 🔄 Current Todo List - -### Planned Next Features (In Priority Order) - -#### Phase 1: Pet Nicknames System ✅ COMPLETED -- [x] **Database Schema** - Add nickname column to player_pets table -- [x] **IRC Commands** - Add !nickname command -- [x] **Web Interface** - Display nicknames on player profiles -- [x] **Validation** - Ensure appropriate nickname length/content limits - -#### Phase 2: Team Builder Tool (Secure PIN System) ✅ COMPLETED -- [x] **Web Team Editor** - Interface for modifying pet teams -- [x] **PIN Generation** - Create unique verification codes for each request -- [x] **Temporary Storage** - Hold pending team changes until PIN validation -- [x] **IRC PIN Delivery** - PM verification codes to players -- [x] **PIN Validation** - Web form to enter and confirm codes -- [x] **Database Updates** - Apply team changes only after successful PIN verification -- [x] **Security Cleanup** - Clear expired PINs and pending requests - -**Secure Technical Flow:** -``` -Web: Save Team → Generate PIN → Store Pending Changes → IRC: PM PIN - ↓ -Web: Enter PIN → Validate PIN → Apply Database Changes → Clear Request -``` -*Critical: No database changes until PIN verification succeeds* - -#### Phase 3: Auto-Battle Mode -- [ ] **Auto-Battle Logic** - Automated pet combat in current location -- [ ] **PM Notifications** - Send results to player via private message -- [ ] **Flood Control** - 5-second delays between battles, wins/losses only -- [ ] **Stop Mechanism** - Commands to start/stop auto-battle mode - -### Low Priority -- [ ] **Enhanced Petdex Filtering** - Add search by type, evolution chains, rarity filters - ---- - -## 🐛 Known Issues & Bugs - -### Current Issues - CRITICAL -- **Team Builder Auto-Move** - Active pets get moved to storage on page load automatically -- **Save Button Not Working** - Team builder save returns 501 "not implemented" error -- **Experience Not Saving** - Pet EXP doesn't persist or isn't visible in profiles -- **Achievement Travel Bug** - Players can't travel despite having achievements until catching a pet -- **Team Builder Save Auth** - PIN verification system not implemented in handlers - -### Current Issues - HIGH PRIORITY -- **Item Spawn Rate** - Items too common, need lower spawn rates -- **Location Wild Pets** - Same pets listed multiple times, broken "+(number) more" button -- **Petdex Accuracy** - Missing types in summary, showing pet instances instead of locations -- **Pet Spawns Logic** - Common pets should spawn in most compatible locations -- **Missing Database Tables** - No species_moves table for move learning system - -### Current Issues - MEDIUM PRIORITY -- **Team Command Navigation** - !team should link to web team page instead of IRC output -- **Pet Rename UX** - Should be done via team builder with edit buttons + PIN auth -- **Max Team Size** - Need to implement and enforce team size limits - -### Recently Fixed -- **Player Profile Crash** - Fixed 'int' object has no attribute 'split' error -- **Battle System Syntax** - Resolved indentation issues in gym battle completion -- **Encounter Data Mapping** - Corrected SQL column indices -- **Team Builder Drag-Drop** - Fixed drag and drop functionality with backup double-click - ---- - -## 💡 Future Enhancement Ideas - -### High Impact Features -- **Pet Evolution System** - Allow pets to evolve at certain levels with stat boosts -- **Breeding System** - Cross-breed pets to create new species with mixed stats -- **Item System Enhancement** - Usable items during battles (healing, stat boosts) -- **Tournament Mode** - Player vs Player battles with leaderboards -- **Quest System** - Daily/weekly challenges with rewards - -### Web Interface Improvements -- **Advanced Petdex Search** - Filter by stats, moves, locations, evolution stage -- **Battle Replay System** - View history of past battles with move-by-move breakdown -- **Team Builder Tool** - Drag-and-drop pet team composition planner -- **Achievement Gallery** - Visual showcase of all unlocked achievements -- **Statistics Dashboard** - Detailed analytics on player performance - -### Quality of Life -- **Pet Nicknames** - Allow players to give custom names to their pets -- **Auto-Battle Mode** - Quick battle resolution for farming -- **Battle Predictions** - Show estimated win/loss chance before battles -- **Move Learning** - Pets learn new moves as they level up -- **Shiny Pets** - Rare color variants with special visual indicators - -### Social Features -- **Trading System** - Exchange pets between players -- **Guild System** - Team up with other players for group challenges -- **Global Leaderboards** - Compare progress with all players -- **Battle Spectating** - Watch other players' battles in real-time -- **Friend System** - Add friends and see their progress - -### Technical Improvements -- **Real-time Battle Updates** - WebSocket integration for live battle viewing -- **Mobile App** - Native mobile application -- **Voice Commands** - Discord/voice integration for hands-free play -- **API Endpoints** - RESTful API for third-party integrations -- **Database Optimization** - Performance improvements for large player bases - -### Game Balance & Depth -- **Weather Effects in Battle** - Location weather affects battle mechanics -- **Terrain Types** - Different battle environments with unique effects -- **Status Conditions** - Poison, sleep, paralysis, etc. -- **Critical Hit System** - Enhanced critical hit mechanics -- **Equipment System** - Held items that modify pet stats/abilities - ---- - -## 🎯 Project Health - -### Status: **EXCELLENT** ✅ -- ✅ All core features implemented and working -- ✅ No known critical bugs -- ✅ Clean, maintainable codebase -- ✅ Comprehensive web interface -- ✅ Token-efficient development workflow -- ✅ Robust error handling - -### Next Development Phase -Ready for feature expansion and polish. The foundation is solid for implementing any of the enhancement ideas above. - -### Performance -- Bot: Stable and responsive -- Web Interface: Fast loading times -- Database: Efficient queries with proper indexing -- Git Workflow: Streamlined for rapid iteration - ---- - -*This project demonstrates a complete full-stack game implementation with modern development practices and comprehensive feature set.* \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 5b8c02f..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,206 +0,0 @@ -# PetBot Quick Start Guide - -## Prerequisites Installation - -### Method 1: Automatic Installation (Recommended) - -Run the automatic installation script: - -```bash -# Make the script executable -chmod +x install_prerequisites.sh - -# Run the installation -./install_prerequisites.sh -``` - -Or use the Python version: - -```bash -python3 install_prerequisites.py -``` - -### Method 2: Manual Installation - -If you prefer manual installation: - -```bash -# Install required packages -pip3 install -r requirements.txt - -# Or install individually -pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0 - -# Create required directories -mkdir -p data backups logs -``` - -## System Requirements - -- **Python**: 3.7 or higher -- **pip**: Python package installer -- **Operating System**: Linux, macOS, or Windows with Python support - -## Quick Start - -### 1. Install Prerequisites - -```bash -./install_prerequisites.sh -``` - -### 2. Test Basic Functionality - -```bash -# Test backup system -python3 test_backup_simple.py - -# Test IRC reconnection system -python3 test_reconnection.py -``` - -### 3. Run the Bot - -#### Option A: Debug Mode (Original) -```bash -python3 run_bot_debug.py -``` - -#### Option B: With Auto-Reconnect (Recommended) -```bash -python3 run_bot_with_reconnect.py -``` - -### 4. Access Web Interface - -Open your browser and go to: -- **Local**: http://localhost:8080 -- **Production**: http://petz.rdx4.com (if configured) - -## Configuration - -### IRC Settings - -Edit the configuration in the bot files: - -```python -config = { - "server": "irc.libera.chat", - "port": 6667, - "nickname": "PetBot", - "channel": "#petz", - "command_prefix": "!" -} -``` - -### Database - -The bot uses SQLite database stored in `data/petbot.db`. No additional setup required. - -## Available Commands - -### Basic Commands -- `!help` - Show available commands -- `!start` - Begin your pet journey -- `!stats` - View your stats -- `!pets` - View your pets - -### Connection Monitoring -- `!status` - Show bot connection status -- `!uptime` - Show bot uptime -- `!ping` - Test bot responsiveness - -### Admin Commands -- `!backup` - Create database backup -- `!restore ` - Restore database -- `!reload` - Reload bot modules -- `!reconnect` - Force IRC reconnection - -## Troubleshooting - -### Common Issues - -1. **ModuleNotFoundError**: Run the prerequisites installer -2. **Permission Denied**: Make sure scripts are executable (`chmod +x`) -3. **Database Errors**: Check that `data/` directory exists and is writable -4. **IRC Connection Issues**: Check network connectivity and IRC server status - -### Getting Help - -1. Check the error logs in console output -2. Review `CLAUDE.md` for development guidelines -3. Check `issues.txt` for known security issues -4. Review `TODO.md` for current project status - -### Log Files - -Logs are displayed in the console. For persistent logging, redirect output: - -```bash -python3 run_bot_with_reconnect.py > logs/bot.log 2>&1 -``` - -## Development - -### Running Tests - -```bash -# Test backup system -python3 test_backup_simple.py - -# Test reconnection system -python3 test_reconnection.py -``` - -### Code Structure - -``` -PetBot/ -├── src/ # Core system -│ ├── database.py # Database operations -│ ├── game_engine.py # Game logic -│ ├── bot.py # IRC bot core -│ ├── irc_connection_manager.py # Connection handling -│ └── backup_manager.py # Backup system -├── modules/ # Command modules -├── config/ # Configuration files -├── webserver.py # Web interface -└── run_bot_*.py # Bot runners -``` - -### Adding New Commands - -1. Create a new method in appropriate module -2. Add command to `get_commands()` list -3. Test thoroughly -4. Update documentation - -## Security - -⚠️ **Important**: Review `issues.txt` for security vulnerabilities before production deployment. - -Key security considerations: -- Run behind HTTPS reverse proxy -- Review XSS vulnerabilities in web interface -- Implement proper authentication -- Keep dependencies updated - -## Next Steps - -1. ✅ Run prerequisites installer -2. ✅ Test basic functionality -3. ✅ Start the bot -4. ✅ Access web interface -5. 📋 Review security issues -6. 🔧 Configure for production -7. 🚀 Deploy and monitor - -## Support - -For issues or questions: -1. Check this guide and documentation -2. Review error messages and logs -3. Test with the provided test scripts -4. Check network connectivity and dependencies - -Happy bot hosting! 🐾 \ No newline at end of file diff --git a/README.md b/README.md index ddbff95..da514c9 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,31 @@ # PetBot - IRC Pokemon-Style Pet Game Bot -A comprehensive IRC bot that brings Pokemon-style pet collecting and battling to your IRC channel! Players can catch pets, explore locations, battle wild creatures, earn achievements, and manage their collections through an integrated web interface. +A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to your IRC channel! Players can catch pets, explore locations, battle wild creatures, earn achievements, and more. ## 🎮 Features ### Core Gameplay -- **Pet Collection**: Catch and collect different species of pets with varying rarities -- **Exploration**: Travel between various themed locations with unique spawns -- **Battle System**: Engage in turn-based battles with wild pets and gym leaders -- **Team Management**: Build teams with drag-and-drop web interface and PIN verification -- **Achievement System**: Unlock new areas by completing challenges and milestones -- **Item Collection**: Discover 17+ useful items including healing potions, battle boosters, and treasure +- **Pet Collection**: Catch and collect different species of pets +- **Exploration**: Travel between various themed locations +- **Battle System**: Engage in turn-based battles with wild pets +- **Team Management**: Activate/deactivate pets, swap team members +- **Achievement System**: Unlock new areas by completing challenges +- **Item Collection**: Discover and collect useful items during exploration ### Advanced Systems -- **Dynamic Weather**: Real-time weather system affecting spawn rates and pet encounters -- **Web Interface**: Modern responsive web dashboard with unified navigation -- **Enhanced Leaderboards**: 8 different ranking categories (levels, experience, wealth, achievements, etc.) -- **Interactive Team Builder**: Drag-and-drop team management with numbered slots (1-6) -- **Location-Based Spawns**: Different pets spawn in different locations with weather modifiers -- **Level Progression**: Pets gain experience, level up, and can be nicknamed +- **Dynamic Weather**: Real-time weather system affecting spawn rates +- **Web Interface**: Modern web dashboard for player stats and pet collections +- **Location-Based Spawns**: Different pets spawn in different locations +- **Level Progression**: Pets gain experience and level up - **Type Effectiveness**: Strategic battle system with type advantages -- **Gym Battle System**: Challenge gym leaders and earn badges -- **Global Item Spawn Control**: Admin-configurable spawn rates with global multipliers - -### Modern Web Features -- **Unified Navigation**: Consistent navigation bar across all web pages -- **Player Profiles**: Comprehensive player statistics and pet collections -- **Team Builder**: Secure PIN-verified team changes with IRC delivery -- **Inventory Management**: Visual item display with usage commands -- **Multi-Category Leaderboards**: Interactive leaderboard switching -- **Responsive Design**: Mobile-friendly interface design +- **Item System**: 16+ unique items with rarity tiers and special effects ### Technical Features -- **Robust IRC Connection**: Auto-reconnecting IRC client with health monitoring -- **Rate Limiting System**: Token bucket rate limiting to prevent spam and abuse -- **Automated Backups**: Comprehensive database backup system with retention policies -- **Security Monitoring**: Security audit completed with 23 vulnerabilities identified -- **Modular Architecture**: Clean, extensible codebase with proper separation of concerns -- **Async Database**: SQLite with async operations and proper transaction handling -- **Background Tasks**: Automated weather updates and system monitoring -- **Error Handling**: Comprehensive error handling and user feedback +- **Modular Architecture**: Clean, extensible codebase +- **Async Database**: SQLite with async operations +- **Background Tasks**: Automated weather updates +- **PM Flood Prevention**: Web-based responses for large data sets +- **Persistent Data**: Player progress survives bot restarts ## 🚀 Quick Start @@ -51,8 +37,8 @@ A comprehensive IRC bot that brings Pokemon-style pet collecting and battling to ### Installation 1. Clone the repository: ```bash - git clone ssh://git@192.168.1.249:2230/megaproxy/Petbot.git - cd Petbot + git clone https://github.com/yourusername/petbot.git + cd petbot ``` 2. Install dependencies: @@ -92,6 +78,7 @@ A comprehensive IRC bot that brings Pokemon-style pet collecting and battling to ### Pet Management - `!activate ` - Activate a pet for battle - `!deactivate ` - Move a pet to storage +- `!swap ` - Swap two pets' active status ### Inventory Commands - `!inventory` / `!inv` / `!items` - View your collected items @@ -109,17 +96,12 @@ A comprehensive IRC bot that brings Pokemon-style pet collecting and battling to ### Unlocking Locations Locations are unlocked by completing achievements: -- **Pet Collector**: Catch 5 pets total → Whispering Woods +- **Nature Explorer**: Catch 3 different Grass-type pets → Whispering Woods - **Spark Collector**: Catch 2 different Electric-type pets → Electric Canyon - **Rock Hound**: Catch 3 different Rock-type pets → Crystal Caves - **Ice Breaker**: Catch 5 different Water/Ice-type pets → Frozen Tundra - **Dragon Tamer**: Catch 15 pets total + 3 Fire-types → Dragon's Peak -### Achievement Progression -- **Pet Collector** (5 pets) → Unlock Whispering Woods -- **Nature Explorer** (3 different Grass pets) → No location unlock -- **Advanced Trainer** (10 pets) → No location unlock - ## 🌤️ Weather System ### Weather Types & Effects @@ -159,7 +141,7 @@ Locations are unlocked by completing achievements: ## 🌐 Web Interface -Access the web dashboard at `http://petz.rdx4.com/`: +Access the web dashboard at `http://localhost:8080/`: - **Player Profiles**: Complete stats, pet collections, and inventories - **Leaderboard**: Top players by level and achievements - **Locations Guide**: All areas with spawn information @@ -184,16 +166,6 @@ Access the web dashboard at `http://petz.rdx4.com/`: ## 🐛 Recent Updates -### v0.3.0 - Team Swap System & Architecture Cleanup -- ✅ **Active Team Architecture**: Complete redesign allowing any team (1-3) to be set as active -- ✅ **Web-IRC Synchronization**: IRC battles now use teams selected via web interface -- ✅ **Team Management Hub**: Enhanced web interface with "Make Active" buttons and team status -- ✅ **Database Migration**: New `active_teams` table for flexible team management -- ✅ **PIN Security**: Secure team changes with IRC-delivered PINs and verification -- ✅ **Command Architecture Cleanup**: Eliminated 12 duplicate commands and standardized admin system -- ✅ **Legacy Code Removal**: Cleaned up 180+ lines of redundant code across modules -- ✅ **Modular System Enhancement**: Improved separation of concerns and maintainability - ### v0.2.0 - Item Collection System - ✅ Complete item system with 16 unique items across 5 categories - ✅ Item discovery during exploration (30% chance) diff --git a/README_git_script.md b/README_git_script.md deleted file mode 100644 index d0193cd..0000000 --- a/README_git_script.md +++ /dev/null @@ -1,73 +0,0 @@ -# Smart Git Push Script 🚀 - -Token-efficient git workflow for development. - -## Usage - -### Auto-commit (recommended for small changes): -```bash -./git_push.sh -``` - -### Custom commit message: -```bash -./git_push.sh "Fix critical bug in player profiles" -``` - -## Features - -- **Auto-detection**: Recognizes file types and generates appropriate commit messages -- **Smart categorization**: Groups changes by type (Python code, web interface, database, etc.) -- **Error handling**: Stops on git errors, graceful handling of edge cases -- **Minimal output**: Just shows success/failure, saves tokens -- **Safety checks**: Verifies git repo, checks for changes before committing - -## Auto-Generated Messages - -Examples of what the script generates: -- `Update Python code - 2024-01-15 14:30` -- `Update web interface, bot modules - 2024-01-15 14:31` -- `Update database, configuration - 2024-01-15 14:32` - -## Token Savings - -- **Before**: ~200-300 tokens per git operation -- **After**: ~20-50 tokens per git operation -- **Savings**: ~75% reduction in git-related token usage - -## Claude Usage - TOKEN EFFICIENT WORKFLOW - -**✅ CORRECT (Low Token Usage):** -``` -Please run: ./git_push.sh -``` -*Claude ignores output completely* - -**❌ AVOID (High Token Usage):** -``` -git status -git add . -git commit -m "..." -git push origin main -``` -*Reading outputs wastes tokens* - -## Logging System - -- All operations logged to `git_push.log` -- Only check log if there's an issue -- **Claude should NOT read logs unless asked** -- Log file ignored by git - -## Error Handling - -If push fails: -1. User will see error on console -2. User can ask: "Check the git log for errors" -3. Only then should Claude read `git_push.log` - -## Token Savings Protocol - -- **Normal operation**: Use script, ignore output -- **Only on errors**: Read logs when requested -- **Maximum efficiency**: No unnecessary output reading \ No newline at end of file diff --git a/TODO.md b/TODO.md deleted file mode 100644 index e1ddd16..0000000 --- a/TODO.md +++ /dev/null @@ -1,257 +0,0 @@ -# TODO.md - PetBot Development Tasks - -This file tracks completed work, pending bugs, enhancements, and feature ideas for the PetBot project. - -## 📊 Summary -- **✅ Completed**: 18 items -- **🐛 Bugs**: 0 items -- **🔧 Enhancements**: 3 items -- **💡 Ideas**: 9 items -- **📋 Total**: 30 items tracked - ---- - -## ✅ COMPLETED ITEMS - -### High Priority Completed ✅ -- [x] **Create unified theme and navigation bar for all webserver pages** - - Implemented comprehensive navigation system with hover dropdowns - - Added unified CSS variables and consistent styling across all pages - - Enhanced user experience with active page highlighting - -- [x] **Fix petdex repetition of pets issue** - - Added DISTINCT to SQL queries to prevent database-level duplicates - - Resolved display issues showing multiple entries for same pets - -- [x] **Fix exploration bug: prevent multiple !explore when encounter is active** - - Added state management to prevent multiple explores - - Users must resolve active encounters before exploring again - -- [x] **Fix battle bug: prevent starting multiple battles from exploration encounters** - - Implemented proper encounter workflow enforcement - - Prevents race conditions in battle system - -- [x] **Enforce exploration encounter workflow: must choose fight/capture/flee before exploring again** - - Added clear error messages for active encounters - - Improved game flow and state consistency - -- [x] **Fix team builder drag-and-drop functionality and center alignment** - - Complete rewrite of team builder interface - - Working drag-and-drop between storage and numbered team slots (1-6) - - Proper center alignment with `max-width: 1200px; margin: 0 auto` - - Added double-click backup method for accessibility - -- [x] **Implement IRC PIN delivery for team builder security** - - Added secure PIN verification system for team changes - - PINs sent via IRC private messages with 10-minute expiration - - Integrated bot instance with webserver for IRC messaging - -### Medium Priority Completed ✅ -- [x] **Redirect !items command to player profile URL instead of IRC response** - - Updated inventory commands to redirect to web interface - - Added #inventory jump points for direct section navigation - - Improved user experience with detailed web-based inventory management - -- [x] **Add jump points to player page (/#inventory) for direct linking to sections** - - Implemented anchor links for direct navigation to specific sections - - Enhanced accessibility and user workflow - -- [x] **Remove !swap command - team management moved to website** - - Streamlined pet management through unified web interface - - Removed redundant IRC command in favor of superior web experience - -- [x] **Implement player team pet order persistence in database** - - Added team_order column with numbered slots (1-6) - - Database migration for existing players - - Persistent team ordering across sessions - -- [x] **Fix !gym challenge to use player's current location instead of requiring location parameter** - - Simplified gym challenge workflow - - Uses player's current location automatically - -- [x] **Update all project documentation (CHANGELOG.md, README.md, help.html)** - - Comprehensive documentation updates reflecting new features - - Updated help system with web interface integration - - Enhanced project documentation for contributors - -- [x] **Implement automated database backup system** - - Complete backup management system with BackupManager class - - Automated scheduling with daily, weekly, and monthly backups - - Backup compression using gzip for space efficiency - - Retention policies (7 daily, 4 weekly, 12 monthly backups) - - IRC admin commands for backup management (!backup, !restore, !backups, !backup_stats, !backup_cleanup) - - Comprehensive testing suite and integration documentation - - Database integrity verification and safe restore procedures - -- [x] **IRC connection monitoring and auto-reconnect functionality** - - Advanced IRC connection manager with robust state tracking - - Health monitoring system with ping/pong heartbeat (60s intervals) - - Exponential backoff reconnection (1s to 5min with jitter) - - Connection statistics and monitoring commands (!status, !uptime, !ping, !reconnect, !connection_stats) - - Graceful error handling and recovery from network interruptions - - Comprehensive test suite covering 11 scenarios including edge cases - - Integration with existing bot architecture and module system - -### Low Priority Completed ✅ -- [x] **Create CLAUDE.md file documenting development patterns and conventions** - - Comprehensive development guide for AI-assisted development - - Documents coding conventions, patterns, and project structure - - Useful reference for future development sessions - ---- - -## 🐛 KNOWN BUGS - -### Medium Priority Bugs 🔴 -- [x] **IRC connection monitoring and auto-reconnect functionality** - - ✅ Bot may lose connection without proper recovery - - ✅ Need robust reconnection logic with exponential backoff - - ✅ Monitor connection health and implement graceful reconnection - - ✅ Implemented comprehensive IRC connection manager with state tracking - - ✅ Added health monitoring with ping/pong system - - ✅ Created exponential backoff with jitter for reconnection attempts - - ✅ Added connection statistics and monitoring commands - - ✅ Comprehensive test suite with 11 test scenarios - ---- - -## 🔧 ENHANCEMENTS NEEDED - -### High Priority Enhancements 🟠 -- [x] **Implement automated database backup system** - - ✅ Regular automated backups of SQLite database (daily, weekly, monthly) - - ✅ Backup rotation and retention policies (7 daily, 4 weekly, 12 monthly) - - ✅ Recovery procedures and testing (restore with confirmation) - - ✅ Compression support (gzip) for space efficiency - - ✅ IRC admin commands for backup management - - ✅ Automated scheduling with cleanup - -- [x] **Conduct security audit of web interface and IRC bot** - - ✅ Review all user input validation - - ✅ Audit authentication and authorization mechanisms - - ✅ Test for common web vulnerabilities (XSS, CSRF, injection attacks) - - ✅ Review IRC bot security practices - - ✅ Identified 23 security vulnerabilities (5 critical, 8 high, 7 medium, 3 low) - - ✅ Created comprehensive security report in issues.txt - -- [ ] **Address security vulnerabilities from audit** - - Fix XSS vulnerabilities by implementing HTML escaping - - Add HTTP security headers (CSP, X-Frame-Options, etc.) - - Implement web interface authentication and authorization - - Fix path traversal vulnerabilities - - Add input validation and sanitization - - See issues.txt for complete list and remediation priorities - -### Medium Priority Enhancements 🟡 -- [x] **Add rate limiting to prevent command spam and abuse** - - ✅ Implemented comprehensive token bucket rate limiting system - - ✅ Per-user rate limiting on IRC commands with category-based limits - - ✅ Web interface request throttling with IP-based tracking - - ✅ Graceful handling of rate limit violations with user-friendly messages - - ✅ Admin commands for monitoring and management (!rate_stats, !rate_user, !rate_unban, !rate_reset) - - ✅ Automatic cleanup of old violations and expired bans - - ✅ Central configuration system with single admin user control - -- [ ] **Implement comprehensive error logging and monitoring system** - - Structured logging with appropriate log levels - - Error tracking and alerting system - - Performance monitoring and metrics collection - -- [ ] **Optimize database queries and web interface loading times** - - Database query performance analysis - - Add proper indexing for frequently accessed data - - 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 - -### Medium Priority Ideas 🔵 -- [ ] **Add mobile-responsive design to web interface for better mobile experience** - - Responsive CSS for mobile devices - - Touch-friendly drag-and-drop alternatives - - Mobile-optimized navigation and layouts - -- [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 - - Recovery from browser crashes or accidental navigation - - Draft management and persistence - -- [ ] **Add search and filter functionality to pet collection page** - - Search pets by name, type, level, or stats - - Advanced filtering options - - Sorting by various criteria - -### Low Priority Ideas 🟢 -- [ ] **Implement pet evolution system with evolution stones and level requirements** - - Evolution trees for existing pet species - - Evolution stones as rare items - - Level and friendship requirements for evolution - -- [ ] **Add player-to-player pet trading system with web interface** - - Secure trading mechanism - - Trade history and verification - - Web-based trading interface - -- [ ] **Add visual battle animations to web interface** - - Animated battle sequences - - Visual effects for different move types - - Enhanced battle experience - -- [ ] **Add bulk actions for pet management (release multiple pets, mass healing)** - - Multi-select functionality for pet collections - - Bulk operations with confirmation dialogs - - Batch processing for efficiency - -- [ ] **Add real-time achievement unlock notifications to web interface** - - WebSocket or SSE for real-time updates - - Toast notifications for achievements - - Achievement celebration animations - -- [ ] **Add preset team configurations for different battle strategies** - - Pre-configured teams for different scenarios - - Team templates and sharing - - Strategic team building assistance - ---- - -## 📝 Notes for Future Development - -### Priorities for Next Development Session -1. **High Priority**: Address database backup system and security audit -2. **Medium Priority**: Implement rate limiting and error logging -3. **Feature Focus**: Mobile responsiveness and enhanced leaderboards - -### Development Guidelines -- Follow patterns established in CLAUDE.md -- Test thoroughly before committing changes -- Update documentation with any new features -- Maintain modular architecture for easy feature additions - -### Testing Checklist -- [ ] IRC bot functionality and command processing -- [ ] Web interface responsiveness and interaction -- [ ] Database operations and data integrity -- [ ] PIN verification and security features -- [ ] Cross-browser compatibility - ---- - -*Last Updated: Current development session* -*Next Review: Before major feature additions* \ No newline at end of file diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index 57727bc..0000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,369 +0,0 @@ -# PetBot Troubleshooting Guide - -## Common Installation Issues - -### Issue 1: externally-managed-environment Error - -**Error Message:** -``` -error: externally-managed-environment - -× This environment is externally managed -╰─> To install Python packages system-wide, try apt install - python3-xyz, where xyz is the package you are trying to - install. -``` - -**Cause:** Modern Python installations (3.11+) prevent direct pip installations to avoid conflicts with system packages. - -**Solutions:** - -#### Solution A: Use Fixed Installation Script (Recommended) -```bash -./install_prerequisites_simple.sh -``` - -This creates a virtual environment and installs packages there. - -#### Solution B: Manual Virtual Environment Setup -```bash -# Create virtual environment -python3 -m venv venv - -# Activate virtual environment -source venv/bin/activate - -# Install packages -pip install -r requirements.txt - -# Run bot (while venv is active) -python run_bot_with_reconnect.py - -# Deactivate when done -deactivate -``` - -#### Solution C: System Package Installation (Ubuntu/Debian) -```bash -# Install system packages instead -sudo apt update -sudo apt install python3-pip python3-aiosqlite python3-dotenv - -# For IRC library, you may still need pip in venv or --break-system-packages -pip install --break-system-packages irc>=20.3.0 -``` - -#### Solution D: Force Installation (Not Recommended) -```bash -pip install --break-system-packages -r requirements.txt -``` - -⚠️ **Warning:** This can break system Python packages. - -### Issue 2: venv Module Not Found - -**Error Message:** -``` -ModuleNotFoundError: No module named 'venv' -``` - -**Solution:** -```bash -# Ubuntu/Debian -sudo apt install python3-venv - -# CentOS/RHEL -sudo yum install python3-venv - -# Or try alternative -sudo apt install python3-virtualenv -``` - -### Issue 3: Permission Denied - -**Error Message:** -``` -Permission denied: './install_prerequisites_simple.sh' -``` - -**Solution:** -```bash -chmod +x install_prerequisites_simple.sh -./install_prerequisites_simple.sh -``` - -### Issue 4: Python Version Too Old - -**Error Message:** -``` -Python 3.6.x is not supported -``` - -**Solutions:** - -#### Ubuntu/Debian: -```bash -# Add deadsnakes PPA for newer Python -sudo apt update -sudo apt install software-properties-common -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt update -sudo apt install python3.11 python3.11-venv python3.11-pip - -# Use specific version -python3.11 -m venv venv -``` - -#### CentOS/RHEL: -```bash -# Enable EPEL and install Python 3.9+ -sudo yum install epel-release -sudo yum install python39 python39-pip -``` - -#### Manual Compilation: -```bash -# Download and compile Python (last resort) -wget https://www.python.org/ftp/python/3.11.0/Python-3.11.0.tgz -tar xzf Python-3.11.0.tgz -cd Python-3.11.0 -./configure --enable-optimizations -make -j 8 -sudo make altinstall -``` - -## Runtime Issues - -### Issue 5: IRC Connection Failed - -**Error Message:** -``` -Could not connect to irc.libera.chat:6667 -``` - -**Solutions:** -1. Check internet connection: `ping irc.libera.chat` -2. Try different IRC server/port -3. Check firewall settings -4. Use IRC over TLS (port 6697) - -### Issue 6: Database Locked - -**Error Message:** -``` -sqlite3.OperationalError: database is locked -``` - -**Solutions:** -1. Stop all bot instances -2. Check for stale lock files: `rm -f data/petbot.db-wal data/petbot.db-shm` -3. Restart the bot - -### Issue 7: Web Interface Not Accessible - -**Error Message:** -``` -Connection refused on port 8080 -``` - -**Solutions:** -1. Check if bot is running -2. Verify port 8080 is not in use: `netstat -tlnp | grep 8080` -3. Try different port in webserver.py -4. Check firewall: `sudo ufw allow 8080` - -### Issue 8: Module Import Errors - -**Error Message:** -``` -ModuleNotFoundError: No module named 'irc' -``` - -**Solutions:** -1. Make sure virtual environment is activated -2. Reinstall packages: `pip install -r requirements.txt` -3. Check Python path: `python -c "import sys; print(sys.path)"` - -## Using Virtual Environment - -### Daily Usage Pattern -```bash -# Activate virtual environment -source venv/bin/activate - -# Run bot -python run_bot_with_reconnect.py - -# Run tests -python test_backup_simple.py - -# Deactivate when done -deactivate -``` - -### Or Use Wrapper Scripts -```bash -# These automatically handle venv activation -./run_petbot.sh # Start bot -./run_petbot_debug.sh # Debug mode -./test_petbot.sh # Run tests -./activate_petbot.sh # Manual activation -``` - -## Virtual Environment Management - -### Check if Virtual Environment is Active -```bash -# Should show venv path if active -which python - -# Or check environment variable -echo $VIRTUAL_ENV -``` - -### Recreate Virtual Environment -```bash -# Remove old venv -rm -rf venv - -# Create new one -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt -``` - -### List Installed Packages -```bash -# Activate venv first -source venv/bin/activate - -# List packages -pip list - -# Show package info -pip show irc aiosqlite -``` - -## System-Specific Issues - -### Ubuntu 22.04+ (externally-managed-environment) -- Use virtual environment (recommended) -- Or install system packages: `sudo apt install python3-aiosqlite` -- IRC library may still need pip installation - -### CentOS/RHEL 8+ -- Enable EPEL repository -- Install python39 or newer -- Use virtual environment - -### macOS -- Install via Homebrew: `brew install python3` -- Virtual environment should work normally - -### Windows -- Install Python from python.org -- Use Command Prompt or PowerShell -- Virtual environment commands: - ```cmd - python -m venv venv - venv\Scripts\activate - pip install -r requirements.txt - ``` - -## Development Environment - -### IDE/Editor Setup - -#### VS Code -```json -{ - "python.defaultInterpreterPath": "./venv/bin/python", - "python.terminal.activateEnvironment": true -} -``` - -#### PyCharm -- Set Project Interpreter to `./venv/bin/python` -- Enable "Add content roots to PYTHONPATH" - -### Running Tests in IDE -Make sure to: -1. Set interpreter to venv Python -2. Set working directory to project root -3. Add project root to PYTHONPATH - -## Quick Diagnosis Commands - -### Check Python Setup -```bash -python3 --version -python3 -c "import sys; print(sys.executable)" -python3 -c "import sys; print(sys.path)" -``` - -### Check Package Installation -```bash -# In virtual environment -python -c "import irc, aiosqlite, dotenv; print('All packages OK')" -``` - -### Check File Permissions -```bash -ls -la *.sh *.py -ls -la data/ backups/ -``` - -### Check Network Connectivity -```bash -ping irc.libera.chat -telnet irc.libera.chat 6667 -curl -I http://localhost:8080 -``` - -### Check System Resources -```bash -free -h # Memory -df -h # Disk space -ps aux | grep python # Running Python processes -``` - -## Getting Help - -1. **Check Logs:** Look at console output for specific error messages -2. **Test Components:** Use individual test scripts to isolate issues -3. **Verify Environment:** Ensure virtual environment is activated -4. **Check Dependencies:** Verify all packages are installed correctly -5. **Review Documentation:** Check INSTALLATION.md for detailed setup - -## Emergency Recovery - -### Complete Reset -```bash -# Remove everything -rm -rf venv data/*.db backups/* logs/* - -# Reinstall -./install_prerequisites_simple.sh - -# Restart -./run_petbot.sh -``` - -### Backup Database Before Reset -```bash -# Create manual backup -cp data/petbot.db data/petbot_backup_$(date +%Y%m%d_%H%M%S).db -``` - -### Restore from Backup -```bash -# List available backups -ls -la backups/ - -# Restore (replace with actual filename) -cp backups/petbot_backup_daily_20240115_030000.db.gz /tmp/ -gunzip /tmp/petbot_backup_daily_20240115_030000.db -cp /tmp/petbot_backup_daily_20240115_030000.db data/petbot.db -``` - -Remember: When in doubt, use the virtual environment! It solves most installation issues. 🐾 \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index a666f01..0000000 --- a/config.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -""" -PetBot Configuration -Central configuration file for admin users and other settings -""" - -# ============================================================================= -# ADMIN CONFIGURATION - Edit this to change the admin user -# ============================================================================= -ADMIN_USER = "megasconed" # The single admin user who can run admin commands -# ============================================================================= - -# IRC Configuration -IRC_CONFIG = { - "server": "irc.libera.chat", - "port": 6667, - "nickname": "PetBot", - "channel": "#petz", - "command_prefix": "!" -} - -# Web Server Configuration -WEB_CONFIG = { - "port": 8080, - "host": "localhost" -} - -# Rate Limiting Configuration -RATE_LIMIT_CONFIG = { - "enabled": True, - "admin_users": [ADMIN_USER], # Uses the admin user from above - "categories": { - "basic": { - "requests_per_minute": 20, - "burst_capacity": 5, - "cooldown_seconds": 1 - }, - "gameplay": { - "requests_per_minute": 10, - "burst_capacity": 3, - "cooldown_seconds": 3 - }, - "management": { - "requests_per_minute": 5, - "burst_capacity": 2, - "cooldown_seconds": 5 - }, - "admin": { - "requests_per_minute": 100, - "burst_capacity": 10, - "cooldown_seconds": 0 - }, - "web": { - "requests_per_minute": 60, - "burst_capacity": 10, - "cooldown_seconds": 1 - } - }, - "global_limits": { - "max_requests_per_minute": 200, - "max_concurrent_users": 100 - }, - "violation_penalties": { - "warning_threshold": 3, - "temporary_ban_threshold": 10, - "temporary_ban_duration": 300 # 5 minutes - } -} \ No newline at end of file diff --git a/config/achievements.json b/config/achievements.json index afea7a2..301de16 100644 --- a/config/achievements.json +++ b/config/achievements.json @@ -4,7 +4,7 @@ "description": "Catch 3 different Grass-type pets", "requirement_type": "catch_type", "requirement_data": "3:Grass", - "unlock_location": null + "unlock_location": "Whispering Woods" }, { "name": "Spark Collector", @@ -39,7 +39,7 @@ "description": "Catch your first 5 pets", "requirement_type": "catch_total", "requirement_data": "5", - "unlock_location": "Whispering Woods" + "unlock_location": null }, { "name": "Advanced Trainer", diff --git a/config/backup_config.json b/config/backup_config.json deleted file mode 100644 index 58d66ff..0000000 --- a/config/backup_config.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "backup_settings": { - "database_path": "data/petbot.db", - "backup_directory": "backups", - "compression": { - "enabled": true, - "level": 6 - }, - "retention_policy": { - "daily_backups": 7, - "weekly_backups": 4, - "monthly_backups": 12, - "manual_backups": 20 - }, - "schedule": { - "daily": { - "enabled": true, - "hour": 3, - "minute": 0 - }, - "weekly": { - "enabled": true, - "day": "sunday", - "hour": 2, - "minute": 0 - }, - "monthly": { - "enabled": true, - "day": 1, - "hour": 1, - "minute": 0 - } - }, - "monitoring": { - "log_level": "INFO", - "alert_on_failure": true, - "max_backup_size_mb": 1000, - "min_free_space_mb": 500 - } - }, - "security": { - "admin_users": ["admin", "megaproxy"], - "backup_encryption": false, - "verify_integrity": true - }, - "notifications": { - "success_notifications": false, - "failure_notifications": true, - "cleanup_notifications": true - } -} \ No newline at end of file diff --git a/config/gyms.json b/config/gyms.json deleted file mode 100644 index 4c0c1a3..0000000 --- a/config/gyms.json +++ /dev/null @@ -1,176 +0,0 @@ -[ - { - "name": "Forest Guardian", - "location": "Starter Town", - "leader_name": "Trainer Verde", - "description": "Master of Grass-type pets and nature's harmony", - "theme": "Grass", - "badge": { - "name": "Leaf Badge", - "icon": "🍃", - "description": "Proof of victory over the Forest Guardian gym" - }, - "team": [ - { - "species": "Leafy", - "base_level": 8, - "moves": ["Vine Whip", "Synthesis", "Tackle", "Growth"], - "position": 1 - }, - { - "species": "Vinewrap", - "base_level": 10, - "moves": ["Entangle", "Absorb", "Bind", "Growth"], - "position": 2 - }, - { - "species": "Bloomtail", - "base_level": 12, - "moves": ["Petal Dance", "Quick Attack", "Sweet Scent", "Tackle"], - "position": 3 - } - ] - }, - { - "name": "Nature's Haven", - "location": "Whispering Woods", - "leader_name": "Elder Sage", - "description": "Ancient guardian of the deep forest mysteries", - "theme": "Grass", - "badge": { - "name": "Grove Badge", - "icon": "🌳", - "description": "Symbol of mastery over ancient forest powers" - }, - "team": [ - { - "species": "Bloomtail", - "base_level": 14, - "moves": ["Petal Blizzard", "Agility", "Sweet Scent", "Bullet Seed"], - "position": 1 - }, - { - "species": "Vinewrap", - "base_level": 15, - "moves": ["Power Whip", "Leech Seed", "Slam", "Synthesis"], - "position": 2 - }, - { - "species": "Leafy", - "base_level": 16, - "moves": ["Solar Beam", "Growth", "Double Edge", "Sleep Powder"], - "position": 3 - } - ] - }, - { - "name": "Storm Master", - "location": "Electric Canyon", - "leader_name": "Captain Volt", - "description": "Commander of lightning and electrical fury", - "theme": "Electric", - "badge": { - "name": "Bolt Badge", - "icon": "⚡", - "description": "Emblem of electric mastery and storm control" - }, - "team": [ - { - "species": "Sparky", - "base_level": 15, - "moves": ["Thunder Shock", "Quick Attack", "Thunder Wave", "Agility"], - "position": 1 - }, - { - "species": "Sparky", - "base_level": 17, - "moves": ["Thunderbolt", "Double Kick", "Thunder", "Spark"], - "position": 2 - } - ] - }, - { - "name": "Stone Crusher", - "location": "Crystal Caves", - "leader_name": "Miner Magnus", - "description": "Defender of the deep caverns and crystal formations", - "theme": "Rock", - "badge": { - "name": "Crystal Badge", - "icon": "💎", - "description": "Testament to conquering the underground depths" - }, - "team": [ - { - "species": "Rocky", - "base_level": 18, - "moves": ["Rock Throw", "Harden", "Tackle", "Rock Tomb"], - "position": 1 - }, - { - "species": "Rocky", - "base_level": 20, - "moves": ["Stone Edge", "Rock Slide", "Earthquake", "Iron Defense"], - "position": 2 - } - ] - }, - { - "name": "Ice Breaker", - "location": "Frozen Tundra", - "leader_name": "Arctic Queen", - "description": "Sovereign of ice and eternal winter's embrace", - "theme": "Ice", - "badge": { - "name": "Frost Badge", - "icon": "❄️", - "description": "Mark of triumph over the frozen wasteland" - }, - "team": [ - { - "species": "Hydrox", - "base_level": 22, - "moves": ["Ice Beam", "Water Gun", "Aurora Beam", "Mist"], - "position": 1 - }, - { - "species": "Hydrox", - "base_level": 24, - "moves": ["Blizzard", "Hydro Pump", "Ice Shard", "Freeze Dry"], - "position": 2 - } - ] - }, - { - "name": "Dragon Slayer", - "location": "Dragon's Peak", - "leader_name": "Champion Drake", - "description": "Ultimate master of fire and stone, peak challenger", - "theme": "Fire", - "badge": { - "name": "Dragon Badge", - "icon": "🐉", - "description": "Ultimate symbol of mastery over Dragon's Peak" - }, - "team": [ - { - "species": "Blazeon", - "base_level": 25, - "moves": ["Flamethrower", "Dragon Rush", "Fire Blast", "Agility"], - "position": 1 - }, - { - "species": "Rocky", - "base_level": 26, - "moves": ["Stone Edge", "Earthquake", "Fire Punch", "Rock Slide"], - "position": 2 - }, - { - "species": "Blazeon", - "base_level": 28, - "moves": ["Overheat", "Dragon Claw", "Solar Beam", "Explosion"], - "position": 3 - } - ] - } -] \ No newline at end of file diff --git a/config/items.json b/config/items.json index d8a039c..cbc7b54 100644 --- a/config/items.json +++ b/config/items.json @@ -1,9 +1,4 @@ { - "_config": { - "global_spawn_multiplier": 1.0, - "description": "Global multiplier for all item spawn rates. Set to 0.5 for half spawns, 2.0 for double spawns, etc.", - "admin_note": "Edit this value to globally adjust all item spawn rates. Individual item spawn_rate values can still be fine-tuned." - }, "healing_items": [ { "id": 1, @@ -14,7 +9,7 @@ "effect": "heal", "effect_value": 20, "locations": ["all"], - "spawn_rate": 0.0375 + "spawn_rate": 0.15 }, { "id": 2, @@ -25,7 +20,7 @@ "effect": "heal", "effect_value": 50, "locations": ["all"], - "spawn_rate": 0.02 + "spawn_rate": 0.08 }, { "id": 3, @@ -36,7 +31,7 @@ "effect": "full_heal", "effect_value": 100, "locations": ["all"], - "spawn_rate": 0.0075 + "spawn_rate": 0.03 }, { "id": 4, @@ -47,29 +42,7 @@ "effect": "heal_status", "effect_value": 15, "locations": ["mystic_forest", "enchanted_grove"], - "spawn_rate": 0.03 - }, - { - "id": 18, - "name": "Revive", - "description": "Revives a fainted pet and restores 50% of its HP", - "rarity": "rare", - "category": "healing", - "effect": "revive", - "effect_value": 50, - "locations": ["all"], - "spawn_rate": 0.005 - }, - { - "id": 19, - "name": "Max Revive", - "description": "Revives a fainted pet and fully restores its HP", - "rarity": "epic", - "category": "healing", - "effect": "max_revive", - "effect_value": 100, - "locations": ["all"], - "spawn_rate": 0.002 + "spawn_rate": 0.12 } ], "battle_items": [ @@ -82,7 +55,7 @@ "effect": "attack_boost", "effect_value": 25, "locations": ["all"], - "spawn_rate": 0.025 + "spawn_rate": 0.10 }, { "id": 6, @@ -93,7 +66,7 @@ "effect": "defense_boost", "effect_value": 20, "locations": ["crystal_caves", "frozen_peaks"], - "spawn_rate": 0.02 + "spawn_rate": 0.08 }, { "id": 7, @@ -104,7 +77,7 @@ "effect": "speed_boost", "effect_value": 100, "locations": ["all"], - "spawn_rate": 0.0125 + "spawn_rate": 0.05 } ], "rare_items": [ @@ -117,7 +90,7 @@ "effect": "none", "effect_value": 0, "locations": ["volcanic_chamber"], - "spawn_rate": 0.005 + "spawn_rate": 0.02 }, { "id": 9, @@ -128,7 +101,7 @@ "effect": "none", "effect_value": 0, "locations": ["crystal_caves"], - "spawn_rate": 0.005 + "spawn_rate": 0.02 }, { "id": 10, @@ -139,7 +112,7 @@ "effect": "lucky_boost", "effect_value": 50, "locations": ["all"], - "spawn_rate": 0.0025 + "spawn_rate": 0.01 }, { "id": 11, @@ -150,7 +123,7 @@ "effect": "none", "effect_value": 0, "locations": ["forgotten_ruins"], - "spawn_rate": 0.0025 + "spawn_rate": 0.01 } ], "location_items": [ @@ -163,7 +136,7 @@ "effect": "sell_value", "effect_value": 100, "locations": ["crystal_caves"], - "spawn_rate": 0.03 + "spawn_rate": 0.12 }, { "id": 13, @@ -174,7 +147,7 @@ "effect": "sell_value", "effect_value": 200, "locations": ["mystic_forest", "enchanted_grove"], - "spawn_rate": 0.015 + "spawn_rate": 0.06 }, { "id": 14, @@ -185,7 +158,7 @@ "effect": "sell_value", "effect_value": 150, "locations": ["volcanic_chamber"], - "spawn_rate": 0.025 + "spawn_rate": 0.10 }, { "id": 15, @@ -196,7 +169,7 @@ "effect": "sell_value", "effect_value": 250, "locations": ["frozen_peaks"], - "spawn_rate": 0.0125 + "spawn_rate": 0.05 }, { "id": 16, @@ -207,20 +180,7 @@ "effect": "sell_value", "effect_value": 500, "locations": ["forgotten_ruins"], - "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 + "spawn_rate": 0.03 } ], "rarity_info": { diff --git a/config/locations.json b/config/locations.json index 532f29e..168904a 100644 --- a/config/locations.json +++ b/config/locations.json @@ -5,11 +5,9 @@ "level_min": 1, "level_max": 3, "spawns": [ - {"species": "Leafy", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Flamey", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Aqua", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Seedling", "spawn_rate": 0.15, "min_level": 1, "max_level": 2}, - {"species": "Furry", "spawn_rate": 0.1, "min_level": 1, "max_level": 3} + {"species": "Leafy", "spawn_rate": 0.35, "min_level": 1, "max_level": 2}, + {"species": "Flamey", "spawn_rate": 0.35, "min_level": 1, "max_level": 2}, + {"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 2} ] }, { @@ -18,13 +16,9 @@ "level_min": 2, "level_max": 6, "spawns": [ - {"species": "Leafy", "spawn_rate": 0.2, "min_level": 2, "max_level": 4}, - {"species": "Vinewrap", "spawn_rate": 0.25, "min_level": 3, "max_level": 5}, - {"species": "Bloomtail", "spawn_rate": 0.2, "min_level": 4, "max_level": 6}, - {"species": "Flamey", "spawn_rate": 0.08, "min_level": 3, "max_level": 4}, - {"species": "Fernwhisk", "spawn_rate": 0.15, "min_level": 3, "max_level": 5}, - {"species": "Furry", "spawn_rate": 0.08, "min_level": 2, "max_level": 4}, - {"species": "Mossrock", "spawn_rate": 0.04, "min_level": 5, "max_level": 6} + {"species": "Leafy", "spawn_rate": 0.5, "min_level": 2, "max_level": 5}, + {"species": "Flamey", "spawn_rate": 0.2, "min_level": 3, "max_level": 4}, + {"species": "Aqua", "spawn_rate": 0.3, "min_level": 2, "max_level": 4} ] }, { @@ -33,11 +27,8 @@ "level_min": 4, "level_max": 9, "spawns": [ - {"species": "Sparky", "spawn_rate": 0.35, "min_level": 4, "max_level": 7}, - {"species": "Rocky", "spawn_rate": 0.25, "min_level": 5, "max_level": 8}, - {"species": "Zapper", "spawn_rate": 0.25, "min_level": 4, "max_level": 6}, - {"species": "Ember", "spawn_rate": 0.1, "min_level": 4, "max_level": 6}, - {"species": "Swiftpaw", "spawn_rate": 0.05, "min_level": 6, "max_level": 8} + {"species": "Sparky", "spawn_rate": 0.6, "min_level": 4, "max_level": 7}, + {"species": "Rocky", "spawn_rate": 0.4, "min_level": 5, "max_level": 8} ] }, { @@ -46,11 +37,8 @@ "level_min": 6, "level_max": 12, "spawns": [ - {"species": "Rocky", "spawn_rate": 0.4, "min_level": 6, "max_level": 10}, - {"species": "Sparky", "spawn_rate": 0.2, "min_level": 7, "max_level": 9}, - {"species": "Pebble", "spawn_rate": 0.25, "min_level": 6, "max_level": 8}, - {"species": "Crystalback", "spawn_rate": 0.1, "min_level": 9, "max_level": 12}, - {"species": "Voltmane", "spawn_rate": 0.05, "min_level": 10, "max_level": 12} + {"species": "Rocky", "spawn_rate": 0.7, "min_level": 6, "max_level": 10}, + {"species": "Sparky", "spawn_rate": 0.3, "min_level": 7, "max_level": 9} ] }, { @@ -59,13 +47,9 @@ "level_min": 10, "level_max": 16, "spawns": [ - {"species": "Hydrox", "spawn_rate": 0.25, "min_level": 10, "max_level": 14}, - {"species": "Rocky", "spawn_rate": 0.2, "min_level": 11, "max_level": 15}, - {"species": "Sparky", "spawn_rate": 0.15, "min_level": 12, "max_level": 14}, - {"species": "Snowball", "spawn_rate": 0.2, "min_level": 10, "max_level": 12}, - {"species": "Frostbite", "spawn_rate": 0.1, "min_level": 12, "max_level": 15}, - {"species": "Bubblin", "spawn_rate": 0.05, "min_level": 10, "max_level": 13}, - {"species": "Frostleaf", "spawn_rate": 0.05, "min_level": 14, "max_level": 16} + {"species": "Hydrox", "spawn_rate": 0.4, "min_level": 10, "max_level": 14}, + {"species": "Rocky", "spawn_rate": 0.3, "min_level": 11, "max_level": 15}, + {"species": "Sparky", "spawn_rate": 0.3, "min_level": 12, "max_level": 14} ] }, { @@ -74,19 +58,9 @@ "level_min": 15, "level_max": 25, "spawns": [ - {"species": "Blazeon", "spawn_rate": 0.22, "min_level": 15, "max_level": 20}, - {"species": "Hydrox", "spawn_rate": 0.18, "min_level": 16, "max_level": 22}, - {"species": "Rocky", "spawn_rate": 0.13, "min_level": 18, "max_level": 25}, - {"species": "Scorchclaw", "spawn_rate": 0.07, "min_level": 15, "max_level": 18}, - {"species": "Tidalfin", "spawn_rate": 0.07, "min_level": 16, "max_level": 19}, - {"species": "Infernowyrm", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Abyssal", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Thornking", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Stormcaller", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Steamvent", "spawn_rate": 0.04, "min_level": 19, "max_level": 23}, - {"species": "Mountainlord", "spawn_rate": 0.03, "min_level": 22, "max_level": 25}, - {"species": "Glaciarch", "spawn_rate": 0.03, "min_level": 22, "max_level": 25}, - {"species": "Harmonix", "spawn_rate": 0.03, "min_level": 18, "max_level": 22} + {"species": "Blazeon", "spawn_rate": 0.5, "min_level": 15, "max_level": 20}, + {"species": "Hydrox", "spawn_rate": 0.3, "min_level": 16, "max_level": 22}, + {"species": "Rocky", "spawn_rate": 0.2, "min_level": 18, "max_level": 25} ] } ] \ No newline at end of file diff --git a/config/pets.json b/config/pets.json index 0457d61..adebb27 100644 --- a/config/pets.json +++ b/config/pets.json @@ -8,8 +8,7 @@ "base_defense": 43, "base_speed": 65, "evolution_level": null, - "rarity": 1, - "emoji": "🔥" + "rarity": 1 }, { "name": "Aqua", @@ -20,8 +19,7 @@ "base_defense": 65, "base_speed": 43, "evolution_level": null, - "rarity": 1, - "emoji": "💧" + "rarity": 1 }, { "name": "Leafy", @@ -32,8 +30,7 @@ "base_defense": 49, "base_speed": 45, "evolution_level": null, - "rarity": 1, - "emoji": "🍃" + "rarity": 1 }, { "name": "Sparky", @@ -44,8 +41,7 @@ "base_defense": 40, "base_speed": 90, "evolution_level": null, - "rarity": 2, - "emoji": "⚡" + "rarity": 2 }, { "name": "Rocky", @@ -56,8 +52,7 @@ "base_defense": 100, "base_speed": 25, "evolution_level": null, - "rarity": 2, - "emoji": "🗿" + "rarity": 2 }, { "name": "Blazeon", @@ -68,8 +63,7 @@ "base_defense": 60, "base_speed": 95, "evolution_level": null, - "rarity": 3, - "emoji": "🌋" + "rarity": 3 }, { "name": "Hydrox", @@ -80,319 +74,6 @@ "base_defense": 90, "base_speed": 60, "evolution_level": null, - "rarity": 3, - "emoji": "🌊" - }, - { - "name": "Vinewrap", - "type1": "Grass", - "type2": null, - "base_hp": 55, - "base_attack": 45, - "base_defense": 70, - "base_speed": 40, - "evolution_level": null, - "rarity": 2, - "emoji": "🌿" - }, - { - "name": "Bloomtail", - "type1": "Grass", - "type2": null, - "base_hp": 60, - "base_attack": 70, - "base_defense": 50, - "base_speed": 80, - "evolution_level": null, - "rarity": 2, - "emoji": "🌺" - }, - { - "name": "Ember", - "type1": "Fire", - "type2": null, - "base_hp": 42, - "base_attack": 50, - "base_defense": 40, - "base_speed": 68, - "evolution_level": null, - "rarity": 1, - "emoji": "✨" - }, - { - "name": "Scorchclaw", - "type1": "Fire", - "type2": null, - "base_hp": 55, - "base_attack": 75, - "base_defense": 55, - "base_speed": 70, - "evolution_level": null, - "rarity": 2, - "emoji": "🐱" - }, - { - "name": "Infernowyrm", - "type1": "Fire", - "type2": null, - "base_hp": 90, - "base_attack": 120, - "base_defense": 75, - "base_speed": 85, - "evolution_level": null, - "rarity": 4, - "emoji": "🐉" - }, - { - "name": "Bubblin", - "type1": "Water", - "type2": null, - "base_hp": 48, - "base_attack": 40, - "base_defense": 60, - "base_speed": 52, - "evolution_level": null, - "rarity": 1, - "emoji": "🫧" - }, - { - "name": "Tidalfin", - "type1": "Water", - "type2": null, - "base_hp": 65, - "base_attack": 60, - "base_defense": 70, - "base_speed": 80, - "evolution_level": null, - "rarity": 2, - "emoji": "🐬" - }, - { - "name": "Abyssal", - "type1": "Water", - "type2": null, - "base_hp": 100, - "base_attack": 85, - "base_defense": 110, - "base_speed": 55, - "evolution_level": null, - "rarity": 4, - "emoji": "🐙" - }, - { - "name": "Seedling", - "type1": "Grass", - "type2": null, - "base_hp": 40, - "base_attack": 35, - "base_defense": 50, - "base_speed": 40, - "evolution_level": null, - "rarity": 1, - "emoji": "🌱" - }, - { - "name": "Fernwhisk", - "type1": "Grass", - "type2": null, - "base_hp": 50, - "base_attack": 55, - "base_defense": 65, - "base_speed": 75, - "evolution_level": null, - "rarity": 2, - "emoji": "🌾" - }, - { - "name": "Thornking", - "type1": "Grass", - "type2": null, - "base_hp": 85, - "base_attack": 95, - "base_defense": 120, - "base_speed": 50, - "evolution_level": null, - "rarity": 4, - "emoji": "👑" - }, - { - "name": "Zapper", - "type1": "Electric", - "type2": null, - "base_hp": 30, - "base_attack": 45, - "base_defense": 35, - "base_speed": 95, - "evolution_level": null, - "rarity": 1, - "emoji": "🐭" - }, - { - "name": "Voltmane", - "type1": "Electric", - "type2": null, - "base_hp": 60, - "base_attack": 85, - "base_defense": 50, - "base_speed": 110, - "evolution_level": null, - "rarity": 3, - "emoji": "🐎" - }, - { - "name": "Stormcaller", - "type1": "Electric", - "type2": null, - "base_hp": 75, - "base_attack": 130, - "base_defense": 60, - "base_speed": 125, - "evolution_level": null, - "rarity": 4, - "emoji": "🦅" - }, - { - "name": "Pebble", - "type1": "Rock", - "type2": null, - "base_hp": 45, - "base_attack": 60, - "base_defense": 80, - "base_speed": 20, - "evolution_level": null, - "rarity": 1, - "emoji": "🪨" - }, - { - "name": "Crystalback", - "type1": "Rock", - "type2": null, - "base_hp": 70, - "base_attack": 90, - "base_defense": 130, - "base_speed": 35, - "evolution_level": null, - "rarity": 3, - "emoji": "🐢" - }, - { - "name": "Mountainlord", - "type1": "Rock", - "type2": null, - "base_hp": 120, - "base_attack": 110, - "base_defense": 150, - "base_speed": 20, - "evolution_level": null, - "rarity": 4, - "emoji": "⛰️" - }, - { - "name": "Snowball", - "type1": "Ice", - "type2": null, - "base_hp": 40, - "base_attack": 35, - "base_defense": 55, - "base_speed": 45, - "evolution_level": null, - "rarity": 1, - "emoji": "☃️" - }, - { - "name": "Frostbite", - "type1": "Ice", - "type2": null, - "base_hp": 55, - "base_attack": 65, - "base_defense": 70, - "base_speed": 85, - "evolution_level": null, - "rarity": 2, - "emoji": "🦨" - }, - { - "name": "Glaciarch", - "type1": "Ice", - "type2": null, - "base_hp": 95, - "base_attack": 80, - "base_defense": 130, - "base_speed": 45, - "evolution_level": null, - "rarity": 4, - "emoji": "❄️" - }, - { - "name": "Furry", - "type1": "Normal", - "type2": null, - "base_hp": 50, - "base_attack": 45, - "base_defense": 45, - "base_speed": 60, - "evolution_level": null, - "rarity": 1, - "emoji": "🐹" - }, - { - "name": "Swiftpaw", - "type1": "Normal", - "type2": null, - "base_hp": 55, - "base_attack": 70, - "base_defense": 50, - "base_speed": 100, - "evolution_level": null, - "rarity": 2, - "emoji": "🐺" - }, - { - "name": "Harmonix", - "type1": "Normal", - "type2": null, - "base_hp": 80, - "base_attack": 75, - "base_defense": 75, - "base_speed": 80, - "evolution_level": null, - "rarity": 3, - "emoji": "🎵" - }, - { - "name": "Steamvent", - "type1": "Water", - "type2": "Fire", - "base_hp": 65, - "base_attack": 80, - "base_defense": 70, - "base_speed": 75, - "evolution_level": null, - "rarity": 3, - "emoji": "💨" - }, - { - "name": "Mossrock", - "type1": "Grass", - "type2": "Rock", - "base_hp": 70, - "base_attack": 65, - "base_defense": 100, - "base_speed": 40, - "evolution_level": null, - "rarity": 3, - "emoji": "🍄" - }, - { - "name": "Frostleaf", - "type1": "Ice", - "type2": "Grass", - "base_hp": 60, - "base_attack": 55, - "base_defense": 85, - "base_speed": 65, - "evolution_level": null, - "rarity": 3, - "emoji": "🧊" + "rarity": 3 } ] \ No newline at end of file diff --git a/config/weather_patterns.json b/config/weather_patterns.json index 93b14cc..2713076 100644 --- a/config/weather_patterns.json +++ b/config/weather_patterns.json @@ -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] }, - "storm": { + "Thunderstorm": { "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": ["storm", "sunny", "calm"], - "Crystal Caves": ["earthquake", "calm"], - "Frozen Tundra": ["blizzard", "calm"], - "Dragon's Peak": ["storm", "sunny", "calm"] + "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"] } } \ No newline at end of file diff --git a/git_push.sh b/git_push.sh deleted file mode 100755 index 310f8e3..0000000 --- a/git_push.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -# Smart Git Push Script - Token-efficient commits -# Usage: ./git_push.sh [optional custom message] - -set -e # Exit on any error - -# Create log file with timestamp -LOG_FILE="git_push.log" -echo "=== Git Push Log - $(date) ===" >> "$LOG_FILE" - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -echo -e "${BLUE}🚀 Smart Git Push${NC}" - -# Check if we're in a git repo -if ! git rev-parse --git-dir > /dev/null 2>&1; then - echo -e "${RED}❌ Not in a git repository${NC}" - echo "ERROR: Not in a git repository" >> "$LOG_FILE" - exit 1 -fi - -# Check if there are any changes (including untracked files) -if git diff --quiet && git diff --staged --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then - echo -e "${GREEN}✅ No changes to commit${NC}" - echo "INFO: No changes to commit" >> "$LOG_FILE" - exit 0 -fi - -# Auto-detect change types (include untracked files) -MODIFIED_FILES=$(git diff --name-only HEAD 2>/dev/null; git diff --name-only --staged 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null | head -10) -CHANGE_TYPES=() - -if echo "$MODIFIED_FILES" | grep -q "\.py$"; then - CHANGE_TYPES+=("Python code") -fi -if echo "$MODIFIED_FILES" | grep -q "webserver\.py\|\.html$\|\.css$"; then - CHANGE_TYPES+=("web interface") -fi -if echo "$MODIFIED_FILES" | grep -q "database\.py\|\.sql$"; then - CHANGE_TYPES+=("database") -fi -if echo "$MODIFIED_FILES" | grep -q "modules/"; then - CHANGE_TYPES+=("bot modules") -fi -if echo "$MODIFIED_FILES" | grep -q "\.json$\|config/"; then - CHANGE_TYPES+=("configuration") -fi -if echo "$MODIFIED_FILES" | grep -q "\.md$\|README\|\.txt$"; then - CHANGE_TYPES+=("documentation") -fi -if echo "$MODIFIED_FILES" | grep -q "\.sh$\|\.py$" && echo "$MODIFIED_FILES" | grep -q "test\|fix\|bug"; then - CHANGE_TYPES+=("bug fixes") -fi - -# Generate commit message -if [ $# -gt 0 ]; then - # Use provided message - COMMIT_MSG="$*" -else - # Auto-generate message - if [ ${#CHANGE_TYPES[@]} -eq 0 ]; then - COMMIT_MSG="Update project files" - elif [ ${#CHANGE_TYPES[@]} -eq 1 ]; then - COMMIT_MSG="Update ${CHANGE_TYPES[0]}" - else - # Join array elements with commas - IFS=', ' - COMMIT_MSG="Update ${CHANGE_TYPES[*]}" - unset IFS - fi - - # Add timestamp for auto-generated messages - COMMIT_MSG="$COMMIT_MSG - $(date '+%Y-%m-%d %H:%M')" -fi - -# Add footer -COMMIT_MSG="$COMMIT_MSG - -🤖 Generated with [Claude Code](https://claude.ai/code) - -Co-Authored-By: Claude " - -# Log the operation details -echo "Files changed: $MODIFIED_FILES" >> "$LOG_FILE" -echo "Commit message: $COMMIT_MSG" >> "$LOG_FILE" - -# Stage all changes -echo -e "${BLUE}📦 Staging changes...${NC}" -git add . 2>> "$LOG_FILE" - -# Commit -echo -e "${BLUE}💾 Committing...${NC}" -git commit -m "$COMMIT_MSG" >> "$LOG_FILE" 2>&1 - -# Push -echo -e "${BLUE}⬆️ Pushing to origin...${NC}" -git push origin main >> "$LOG_FILE" 2>&1 - -echo -e "${GREEN}✅ Successfully pushed to git!${NC}" - -# Show summary (minimal) -CHANGED_COUNT=$(echo "$MODIFIED_FILES" | wc -l) -echo -e "${GREEN}📊 Pushed $CHANGED_COUNT file(s)${NC}" - -# Log success -echo "SUCCESS: Push completed at $(date)" >> "$LOG_FILE" -echo "" >> "$LOG_FILE" \ No newline at end of file diff --git a/help.html b/help.html index 0bbcf30..00626fb 100644 --- a/help.html +++ b/help.html @@ -3,7 +3,7 @@ - PetBot - Command Help + PetBot IRC Commands Reference - ← Back to Game Hub -
-

📚 PetBot Commands

-

Complete guide to Pokemon-style pet collecting in IRC

+

🐾 PetBot Command Reference

+

Complete guide to IRC pet collection and battle commands

+

Connect to irc.libera.chat #petz to play!

- - -
-
🚀 Getting Started
-
-
+
+
🎮 Getting Started
+
!start
-
Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.
+
Begin your pet journey! Creates your trainer account and gives you a starter pet in Starter Town.
Example: !start
!help
-
Get a link to this comprehensive command reference page.
+
Display a quick list of available commands in the IRC channel.
Example: !help
-
-
!stats
-
View your basic trainer information including level, experience, and money.
-
Example: !stats
-
-
-
-
🌍 Exploration & Travel
-
-
+
+
🔍 Exploration & Travel
+
!explore
-
Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!
+
Explore your current location to find wild pets. Weather affects what types of pets you'll encounter!
Example: !explore
+
+
!location (or !where)
+
See where you currently are, including the location description.
+
Example: !location
+
!travel <location>
-
Move to a different location. Each area has unique pets and gyms. Some locations require achievements to unlock.
-
Example: !travel whispering woods
+
Travel to a different location. 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
-
-
-
!wild [location]
-
Show wild pets available in your current location or a specified location. Helps you see what pets you can encounter.
-
Example: !wild or !wild crystal caves
+
!wild <location>
+
Check what types of pets can be found in a specific location.
+
Example: !wild Electric Canyon
-
-

🗺️ Available Locations

+
+

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)
  • +
  • Starter Town - Where all trainers begin (always accessible)
  • +
  • Whispering Woods - Unlocked by catching 3 different Grass-type pets
  • +
  • Electric Canyon - Unlocked by catching 2 different Electric-type pets
  • +
  • Crystal Caves - Unlocked by catching 3 different Rock-type pets
  • +
  • Frozen Tundra - Unlocked by catching 5 different Water/Ice-type pets
  • +
  • Dragon's Peak - Unlocked by catching 15 pets total and having 3 Fire-type 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.
+
Start a battle with a wild pet you encountered during exploration. Strategic combat with type advantages!
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
+
Use a specific move during battle. Each pet has different moves based on their type.
+
Example: !attack Ember
!flee
-
Attempt to escape from the current battle. Not always successful!
+
Attempt to escape from battle. Success depends on your pet's speed vs the wild pet's speed.
Example: !flee
+
+
!moves
+
View your active pet's available moves (up to 4 moves). Shows move type and power for battle planning.
+
Example: !moves
- -
-

💀 Pet Fainting System

-
    -
  • Battle Defeat - Pets that lose battles will faint and cannot be used until healed
  • -
  • Healing Options - Use Revive items, !heal command, or wait 30 minutes for auto-recovery
  • -
  • Strategic Impact - Plan your battles carefully to avoid having all pets faint
  • -
  • Type Advantages - Use type matchups to win battles and avoid fainting
  • -
+
+
!catch (or !capture)
+
Try to catch a pet during exploration OR during battle. During battle, weaker pets (lower HP) have significantly higher catch rates - up to 90% for nearly defeated pets! Both !catch and !capture work identically.
+
Example: !catch or !capture
+ +
+ Battle Strategy: Use type advantages! Water beats Fire, Fire beats Grass, Grass beats Water, Electric beats Water, Rock beats Fire and Electric. Weaken wild pets in battle to increase catch rate! +
-
-
🏛️ 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"
-
-
-
!forfeit
-
Forfeit your current gym battle if you're losing or want to try a different strategy.
-
Example: !forfeit
-
-
- -
- 💡 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.
+
View all your pets with active pets marked by ⭐. Shows levels, HP, and storage status.
Example: !team
-
!pets
-
View your complete pet collection with detailed stats and information via web interface.
-
Example: !pets
+
!stats
+
View your player statistics including level, experience, and money.
+
Example: !stats
-
!activate <pet>
-
Add a pet to your active battle team. You can have multiple active pets for different situations.
-
Example: !activate flamey
+
!activate <pet> PM ONLY
+
Activate a pet for battle by nickname or species name. Only inactive pets can be activated. This command only works in private messages to prevent channel spam.
+
Example: /msg PetBot !activate Sparky
Example: /msg PetBot !activate Pikachu
-
!deactivate <pet>
-
Remove a pet from your active team and put it in storage.
-
Example: !deactivate aqua
+
!deactivate <pet> PM ONLY
+
Deactivate an active pet, removing it from battle readiness. This command only works in private messages to prevent channel spam.
+
Example: /msg PetBot !deactivate Sparky
Example: /msg PetBot !deactivate Pikachu
-
!nickname <pet> <new_nickname>
-
Give a custom nickname to one of your pets. Use their current name or ID to reference them.
-
Example: !nickname flamey "Blazer"
-
+
!swap <pet1> <pet2> PM ONLY
+
Swap activation status between two pets. The first pet becomes inactive, the second becomes active. This command only works in private messages to prevent channel spam.
+
Example: /msg PetBot !swap Sparky Flame
Example: /msg PetBot !swap Pikachu Charmander
+ +
+ Pet Management Tips: You can only have a limited number of active pets at once. Use !team to see which pets are active (⭐). Pet management commands (!activate, !deactivate, !swap) must be sent as private messages to the bot to prevent channel spam. Use /msg PetBot <command> format. +
-
-
🎒 Inventory & Healing System UPDATED!
-
-
-
-
!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, revive fainted pets, or provide other benefits.
-
Example: !use Small Potion, !use Revive
-
-
-
!heal
-
Heal all your active pets to full health. Has a 1-hour cooldown to prevent abuse.
-
Example: !heal
-
-
- -
-

🎯 Item Categories & Rarities

-
    -
  • ○ Common (15%) - Small Potions, basic healing items
  • -
  • ◇ Uncommon (8-12%) - Large Potions, battle boosters, special berries
  • -
  • ◆ Rare (3-6%) - Super Potions, Revive items, speed elixirs, location treasures
  • -
  • ★ Epic (2-3%) - Max Revive, 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. -
- -
-

🏥 Pet Healing System

-
    -
  • Fainted Pets - Pets that lose battles faint and cannot be used until healed
  • -
  • Revive Items - Use Revive (50% HP) or Max Revive (100% HP) to restore fainted pets
  • -
  • !heal Command - Heals all active pets to full health (1-hour cooldown)
  • -
  • Auto-Recovery - Fainted pets automatically recover to 1 HP after 30 minutes
  • -
  • Travel Allowed - You can still travel and explore with fainted pets
  • -
-
-
- -
-
🎯 Community Events NEW!
-
-
-
-
!events
-
View all active community events that all players can participate in together.
-
Example: !events
-
-
-
!event <id>
-
Get detailed information about a specific community event, including progress and leaderboard.
-
Example: !event 1
-
-
-
!contribute <id>
-
Contribute to a community event. Everyone who participates gets rewards when the event completes!
-
Example: !contribute 1
-
-
-
!eventhelp
-
Get detailed help about the community events system and how it works.
-
Example: !eventhelp
-
-
- -
-

🤝 How Community Events Work

-
    -
  • Collaborative Goals - All players work together toward shared objectives
  • -
  • Progress Tracking - Events show progress bars and contribution leaderboards
  • -
  • Time Limited - Events have deadlines and expire if not completed
  • -
  • Difficulty Levels - ⭐ Easy, ⭐⭐ Medium, ⭐⭐⭐ Hard events with better rewards
  • -
  • Automatic Spawning - New events appear regularly for ongoing engagement
  • -
-
- -
-

🎪 Event Types

-
    -
  • 🏪 Resource Gathering - Help collect supplies for the community
  • -
  • 🐾 Pet Rescue - Search for and rescue missing pets
  • -
  • 🎪 Community Projects - Work together on town improvement projects
  • -
  • 🚨 Emergency Response - Help during natural disasters or crises
  • -
  • 🔬 Research - Assist scientists with important discoveries
  • -
-
- -
- 💡 Event Strategy: Check !events regularly for new opportunities! Higher difficulty events give better rewards, and contributing more increases your reward multiplier when the event completes. -
-
-
- -
+
🏆 Achievements & Progress
-
-
+
!achievements
-
View your achievement progress and see which new locations you've unlocked.
+
View your earned achievements and progress. Achievements unlock new locations!
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
  • -
-
+
+ +
+

Key Achievements:

+
    +
  • Pet Collector - Catch your first 5 pets
  • +
  • Advanced Trainer - Catch 10 pets total
  • +
  • Nature Explorer - Catch 3 different Grass-type pets (unlocks Whispering Woods)
  • +
  • Spark Collector - Catch 2 different Electric-type pets (unlocks Electric Canyon)
  • +
  • Rock Hound - Catch 3 different Rock-type pets (unlocks Crystal Caves)
  • +
  • Ice Breaker - Catch 5 different Water/Ice-type pets (unlocks Frozen Tundra)
  • +
  • Dragon Tamer - Catch 15 pets total + 3 Fire types (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
  • -
  • Interactive Map - See where all players are exploring
  • -
  • Team Builder - Drag-and-drop team management with PIN verification
  • -
+
+
🌤️ Weather System
+
+
+
!weather
+
Check the current weather in your location and its effects on pet spawns.
+
Example: !weather
+
+
+ +
+

Weather Effects:

+
    +
  • Sunny - 1.5x Fire and Grass-type spawns
  • +
  • Rainy - 2.0x Water-type spawns
  • +
  • Thunderstorm - 2.0x Electric-type spawns
  • +
  • Blizzard - 1.7x Ice and Water-type spawns
  • +
  • Earthquake - 1.8x Rock-type spawns
  • +
  • Calm - Normal spawn rates for all types
  • +
+
+
+ +
+
📚 Game Mechanics
+
+
+
+ How to Play: +
    +
  1. Use !start to create your trainer and get a starter pet
  2. +
  3. Use !explore to find wild pets in your current location
  4. +
  5. Choose to !battle the wild pet (recommended) or !catch directly
  6. +
  7. In battle, use !attack <move> to weaken the wild pet
  8. +
  9. Use !catch (or !capture) during battle for much higher success rates on damaged pets
  10. +
  11. Battle-catch rates: 30% base + up to 50% bonus for low HP (90% max for nearly defeated pets)
  12. +
  13. Collect different types of pets to unlock achievements and new locations
  14. +
  15. Use !travel to explore new areas as you unlock them
  16. +
  17. Check !weather for optimal catching conditions
  18. +
+
-
-
🤖 Bot Status & Utilities
-
-
-
-
!status
-
Check the bot's current connection status and basic system information.
-
Example: !status
-
-
-
!uptime
-
See how long the bot has been running since last restart.
-
Example: !uptime
-
-
-
!ping
-
Test the bot's responsiveness with a simple ping-pong test.
-
Example: !ping
-
-
- -
-

🔧 System Status

-
    -
  • Connection Monitoring - Bot automatically monitors its IRC connection
  • -
  • Auto-Reconnect - Automatically reconnects if connection is lost
  • -
  • Background Tasks - Weather updates, event spawning, and data validation
  • -
  • Rate Limiting - Built-in protection against spam and abuse
  • -
-
-
-
- -
-
⚡ Rate Limiting & Fair Play
-
-
-

🛡️ Rate Limiting System

-

PetBot uses a sophisticated rate limiting system to ensure fair play and prevent spam. Commands are organized into categories with different limits:

-
    -
  • Basic Commands (!help, !ping, !status) - 20 per minute, 5 burst capacity
  • -
  • Gameplay Commands (!explore, !battle, !catch) - 10 per minute, 3 burst capacity
  • -
  • Management Commands (!pets, !activate, !stats) - 5 per minute, 2 burst capacity
  • -
  • Web Interface - 60 requests per minute, 10 burst capacity
  • -
-
- -
-

📊 How It Works

-
    -
  • Token Bucket Algorithm - You have a "bucket" of tokens that refills over time
  • -
  • Burst Capacity - You can use multiple commands quickly up to the burst limit
  • -
  • Refill Rate - Tokens refill based on the requests per minute limit
  • -
  • Cooldown Period - Brief cooldown after hitting limits before trying again
  • -
-
- -
-

⚠️ Violations & Penalties

-
    -
  • 3 violations - Warning threshold reached (logged)
  • -
  • 10 violations - Temporary 5-minute ban from all commands
  • -
  • Admin Override - Admins can unban users and reset violations
  • -
  • Automatic Cleanup - Old violations and bans are automatically cleared
  • -
-
- -
-
-
!rate_stats
-
View global rate limiting statistics (Admin only).
-
Example: !rate_stats
-
-
-
!rate_user <username>
-
Check rate limiting status for a specific user (Admin only).
-
Example: !rate_user playername
-
-
-
!rate_unban <username>
-
Manually unban a user from rate limiting (Admin only).
-
Example: !rate_unban playername
-
-
-
!rate_reset <username>
-
Reset violations for a user (Admin only).
-
Example: !rate_reset playername
-
-
- -
-

💡 Tips for Smooth Gameplay

-
    -
  • Play Naturally - Normal gameplay rarely hits rate limits
  • -
  • Use the Web Interface - Higher limits for browsing and pet management
  • -
  • Spread Out Commands - Avoid rapid-fire command spamming
  • -
  • Check Your Status - If you get rate limited, wait a moment before trying again
  • -
-
-
-
- -
-
🛡️ Admin Commands
-
-
-
-
!reload
-
Reload all bot modules without restarting the bot (Admin only).
-
Example: !reload
-
-
-
!setweather <location|all> <weather_type> [duration]
-
Manually set weather for a location or all locations (Admin only).
-
Example: !setweather all sunny 120
-
-
-
!spawnevent [difficulty]
-
Force spawn a community event with optional difficulty 1-3 (Admin only).
-
Example: !spawnevent 2
-
-
-
!startevent [type] [difficulty]
-
Start a specific event type. Without args, shows available types (Admin only).
-
Example: !startevent resource_gathering 2
-
-
- -
-

🔑 Admin Access

-
    -
  • Single Admin User - Only one designated admin user can use these commands
  • -
  • Module Management - Reload modules without restarting the entire bot
  • -
  • Weather Control - Force weather changes for testing or events
  • -
  • Event Management - Spawn community events on demand
  • -
-
- -
- ⚠️ Admin Note: These commands require admin privileges and can affect the entire bot system. Use with caution and always test changes in a development environment first. +
+
🔧 Type Effectiveness Chart
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attacking TypeStrong Against (2x)Weak Against (0.5x)
FireGrass, IceWater, Rock
WaterFire, RockElectric, Grass
GrassWater, RockFire, Ice
ElectricWaterRock
RockFire, ElectricWater, Grass
NormalNoneRock
- - - - - \ No newline at end of file diff --git a/install_prerequisites.py b/install_prerequisites.py deleted file mode 100644 index b1a59a2..0000000 --- a/install_prerequisites.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -""" -PetBot Prerequisites Installation Script -This script installs all required Python packages and dependencies for running PetBot. -""" - -import subprocess -import sys -import os -import importlib.util -from pathlib import Path - - -def print_header(): - """Print installation header.""" - print("=" * 60) - print("🐾 PetBot Prerequisites Installation Script") - print("=" * 60) - print() - - -def check_python_version(): - """Check if Python version is compatible.""" - print("🔍 Checking Python version...") - - version = sys.version_info - required_major = 3 - required_minor = 7 - - if version.major < required_major or (version.major == required_major and version.minor < required_minor): - print(f"❌ Python {required_major}.{required_minor}+ required. Current version: {version.major}.{version.minor}.{version.micro}") - print("Please upgrade Python and try again.") - return False - - print(f"✅ Python {version.major}.{version.minor}.{version.micro} is compatible") - return True - - -def check_pip_available(): - """Check if pip is available.""" - print("\n🔍 Checking pip availability...") - - try: - import pip - print("✅ pip is available") - return True - except ImportError: - pass - - # Try alternative ways to check pip - try: - result = subprocess.run([sys.executable, "-m", "pip", "--version"], - capture_output=True, text=True, check=True) - print(f"✅ pip is available: {result.stdout.strip()}") - return True - except (subprocess.CalledProcessError, FileNotFoundError): - print("❌ pip is not available") - print("Please install pip first:") - print(" - On Ubuntu/Debian: sudo apt install python3-pip") - print(" - On CentOS/RHEL: sudo yum install python3-pip") - print(" - On macOS: brew install python3") - return False - - -def check_package_installed(package_name): - """Check if a package is already installed.""" - try: - importlib.import_module(package_name) - return True - except ImportError: - return False - - -def install_package(package_spec): - """Install a package using pip.""" - print(f"📦 Installing {package_spec}...") - - try: - result = subprocess.run([ - sys.executable, "-m", "pip", "install", package_spec - ], capture_output=True, text=True, check=True) - - print(f"✅ Successfully installed {package_spec}") - return True - - except subprocess.CalledProcessError as e: - print(f"❌ Failed to install {package_spec}") - print(f"Error output: {e.stderr}") - return False - - -def install_requirements(): - """Install packages from requirements.txt if it exists.""" - print("\n📋 Installing packages from requirements.txt...") - - requirements_file = Path("requirements.txt") - if not requirements_file.exists(): - print("❌ requirements.txt not found") - return False - - try: - result = subprocess.run([ - sys.executable, "-m", "pip", "install", "-r", "requirements.txt" - ], capture_output=True, text=True, check=True) - - print("✅ Successfully installed packages from requirements.txt") - return True - - except subprocess.CalledProcessError as e: - print("❌ Failed to install from requirements.txt") - print(f"Error output: {e.stderr}") - return False - - -def install_individual_packages(): - """Install individual packages if requirements.txt fails.""" - print("\n📦 Installing individual packages...") - - packages = [ - ("irc", "irc>=20.3.0", "IRC client library"), - ("aiosqlite", "aiosqlite>=0.19.0", "Async SQLite database interface"), - ("dotenv", "python-dotenv>=1.0.0", "Environment variable loading") - ] - - results = [] - - for import_name, package_spec, description in packages: - print(f"\n🔍 Checking {import_name} ({description})...") - - if check_package_installed(import_name): - print(f"✅ {import_name} is already installed") - results.append(True) - else: - print(f"❌ {import_name} is not installed") - success = install_package(package_spec) - results.append(success) - - return all(results) - - -def verify_installation(): - """Verify that all required packages are installed correctly.""" - print("\n🔍 Verifying installation...") - - test_imports = [ - ("irc", "IRC client library"), - ("aiosqlite", "Async SQLite database"), - ("dotenv", "Environment variable loading"), - ("asyncio", "Async programming (built-in)"), - ("sqlite3", "SQLite database (built-in)"), - ("json", "JSON handling (built-in)"), - ("socket", "Network communication (built-in)"), - ("threading", "Threading (built-in)") - ] - - all_good = True - - for module_name, description in test_imports: - try: - importlib.import_module(module_name) - print(f"✅ {module_name}: {description}") - except ImportError as e: - print(f"❌ {module_name}: {description} - {e}") - all_good = False - - return all_good - - -def check_directory_structure(): - """Check if we're in the correct directory and required files exist.""" - print("\n🔍 Checking directory structure...") - - required_files = [ - "requirements.txt", - "src/database.py", - "src/game_engine.py", - "src/bot.py", - "webserver.py", - "run_bot_debug.py" - ] - - missing_files = [] - - for file_path in required_files: - if not Path(file_path).exists(): - missing_files.append(file_path) - - if missing_files: - print("❌ Missing required files:") - for file_path in missing_files: - print(f" - {file_path}") - print("\nPlease make sure you're running this script from the PetBot project directory.") - return False - - print("✅ All required files found") - return True - - -def create_data_directory(): - """Create data directory if it doesn't exist.""" - print("\n🔍 Checking data directory...") - - data_dir = Path("data") - if not data_dir.exists(): - try: - data_dir.mkdir() - print("✅ Created data directory") - except Exception as e: - print(f"❌ Failed to create data directory: {e}") - return False - else: - print("✅ Data directory already exists") - - return True - - -def create_backups_directory(): - """Create backups directory if it doesn't exist.""" - print("\n🔍 Checking backups directory...") - - backups_dir = Path("backups") - if not backups_dir.exists(): - try: - backups_dir.mkdir() - print("✅ Created backups directory") - except Exception as e: - print(f"❌ Failed to create backups directory: {e}") - return False - else: - print("✅ Backups directory already exists") - - return True - - -def test_basic_imports(): - """Test basic functionality by importing key modules.""" - print("\n🧪 Testing basic imports...") - - try: - # Test database import - sys.path.append(os.path.dirname(os.path.abspath(__file__))) - from src.database import Database - print("✅ Database module imports successfully") - - # Test connection manager import - from src.irc_connection_manager import IRCConnectionManager - print("✅ IRC connection manager imports successfully") - - # Test backup manager import - from src.backup_manager import BackupManager - print("✅ Backup manager imports successfully") - - return True - - except ImportError as e: - print(f"❌ Import test failed: {e}") - return False - - -def show_next_steps(): - """Show next steps after successful installation.""" - print("\n🎉 Installation completed successfully!") - print("\n📋 Next steps:") - print("1. Review and update configuration files in the config/ directory") - print("2. Test the bot with: python3 run_bot_debug.py") - print("3. Or use the new reconnection system: python3 run_bot_with_reconnect.py") - print("4. Check the web interface at: http://localhost:8080") - print("5. Review CLAUDE.md for development guidelines") - print("\n🔧 Available test commands:") - print(" python3 test_backup_simple.py - Test backup system") - print(" python3 test_reconnection.py - Test IRC reconnection") - print("\n📚 Documentation:") - print(" README.md - Project overview") - print(" CLAUDE.md - Development guide") - print(" TODO.md - Current status") - print(" issues.txt - Security audit findings") - print(" BACKUP_SYSTEM_INTEGRATION.md - Backup system guide") - - -def main(): - """Main installation function.""" - print_header() - - # Check prerequisites - if not check_python_version(): - return False - - if not check_pip_available(): - return False - - if not check_directory_structure(): - return False - - # Create directories - if not create_data_directory(): - return False - - if not create_backups_directory(): - return False - - # Install packages - success = install_requirements() - - if not success: - print("\n⚠️ requirements.txt installation failed, trying individual packages...") - success = install_individual_packages() - - if not success: - print("\n❌ Package installation failed") - return False - - # Verify installation - if not verify_installation(): - print("\n❌ Installation verification failed") - return False - - # Test imports - if not test_basic_imports(): - print("\n❌ Basic import tests failed") - return False - - # Show next steps - show_next_steps() - - return True - - -if __name__ == "__main__": - try: - success = main() - if success: - print("\n✅ PetBot prerequisites installation completed successfully!") - sys.exit(0) - else: - print("\n❌ Installation failed. Please check the errors above.") - sys.exit(1) - except KeyboardInterrupt: - print("\n🛑 Installation interrupted by user") - sys.exit(1) - except Exception as e: - print(f"\n💥 Unexpected error: {e}") - import traceback - traceback.print_exc() - sys.exit(1) \ No newline at end of file diff --git a/install_prerequisites.sh b/install_prerequisites.sh deleted file mode 100755 index a626a3c..0000000 --- a/install_prerequisites.sh +++ /dev/null @@ -1,257 +0,0 @@ -#!/bin/bash -# PetBot Prerequisites Installation Script (Shell Version) -# This script installs all required packages and sets up the environment - -set -e # Exit on any error - -echo "============================================================" -echo "🐾 PetBot Prerequisites Installation Script (Shell Version)" -echo "============================================================" -echo - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print colored output -print_status() { - echo -e "${GREEN}✅${NC} $1" -} - -print_error() { - echo -e "${RED}❌${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}⚠️${NC} $1" -} - -print_info() { - echo -e "${BLUE}ℹ️${NC} $1" -} - -# Check if we're in the right directory -echo "🔍 Checking directory structure..." -if [ ! -f "requirements.txt" ] || [ ! -f "src/database.py" ] || [ ! -f "webserver.py" ]; then - print_error "Required files not found. Please run this script from the PetBot project directory." - exit 1 -fi -print_status "Directory structure is correct" - -# Check Python version -echo -echo "🔍 Checking Python version..." -python_version=$(python3 --version 2>&1) -if [ $? -ne 0 ]; then - print_error "Python 3 is not installed or not in PATH" - print_info "Please install Python 3.7+ and try again" - exit 1 -fi - -# Extract version numbers -version_string=$(echo "$python_version" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') -major=$(echo "$version_string" | cut -d'.' -f1) -minor=$(echo "$version_string" | cut -d'.' -f2) - -if [ "$major" -lt 3 ] || ([ "$major" -eq 3 ] && [ "$minor" -lt 7 ]); then - print_error "Python 3.7+ required. Current version: $python_version" - exit 1 -fi - -print_status "Python version is compatible: $python_version" - -# Check pip availability -echo -echo "🔍 Checking pip availability..." -if ! command -v pip3 &> /dev/null; then - if ! python3 -m pip --version &> /dev/null; then - print_error "pip is not available" - print_info "Install pip with:" - print_info " Ubuntu/Debian: sudo apt install python3-pip" - print_info " CentOS/RHEL: sudo yum install python3-pip" - print_info " macOS: brew install python3" - exit 1 - fi - PIP_CMD="python3 -m pip" -else - PIP_CMD="pip3" -fi - -pip_version=$($PIP_CMD --version) -print_status "pip is available: $pip_version" - -# Create required directories -echo -echo "🔍 Creating required directories..." - -if [ ! -d "data" ]; then - mkdir -p data - print_status "Created data directory" -else - print_status "Data directory already exists" -fi - -if [ ! -d "backups" ]; then - mkdir -p backups - print_status "Created backups directory" -else - print_status "Backups directory already exists" -fi - -if [ ! -d "logs" ]; then - mkdir -p logs - print_status "Created logs directory" -else - print_status "Logs directory already exists" -fi - -# Install packages from requirements.txt -echo -echo "📦 Installing packages from requirements.txt..." -if $PIP_CMD install -r requirements.txt; then - print_status "Successfully installed packages from requirements.txt" -else - print_error "Failed to install from requirements.txt" - echo - echo "📦 Trying individual package installation..." - - # Try individual packages - packages=( - "irc>=20.3.0" - "aiosqlite>=0.19.0" - "python-dotenv>=1.0.0" - ) - - failed_packages=() - - for package in "${packages[@]}"; do - echo "Installing $package..." - if $PIP_CMD install "$package"; then - print_status "Successfully installed $package" - else - print_error "Failed to install $package" - failed_packages+=("$package") - fi - done - - if [ ${#failed_packages[@]} -gt 0 ]; then - print_error "Failed to install the following packages:" - for package in "${failed_packages[@]}"; do - echo " - $package" - done - exit 1 - fi -fi - -# Verify installation -echo -echo "🔍 Verifying installation..." - -# List of modules to test -modules=( - "irc:IRC client library" - "aiosqlite:Async SQLite database" - "dotenv:Environment variable loading" - "asyncio:Async programming (built-in)" - "sqlite3:SQLite database (built-in)" - "json:JSON handling (built-in)" - "socket:Network communication (built-in)" - "threading:Threading (built-in)" -) - -failed_imports=() - -for module_info in "${modules[@]}"; do - module=$(echo "$module_info" | cut -d':' -f1) - description=$(echo "$module_info" | cut -d':' -f2) - - if python3 -c "import $module" 2>/dev/null; then - print_status "$module: $description" - else - print_error "$module: $description - Import failed" - failed_imports+=("$module") - fi -done - -if [ ${#failed_imports[@]} -gt 0 ]; then - print_error "Some modules failed to import:" - for module in "${failed_imports[@]}"; do - echo " - $module" - done - exit 1 -fi - -# Test basic project imports -echo -echo "🧪 Testing project imports..." - -if python3 -c " -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath('.'))) -from src.database import Database -from src.irc_connection_manager import IRCConnectionManager -from src.backup_manager import BackupManager -print('All project imports successful') -" 2>/dev/null; then - print_status "Project imports successful" -else - print_error "Project imports failed" - print_info "This might be due to missing dependencies or path issues" - exit 1 -fi - -# Check for optional system dependencies -echo -echo "🔍 Checking optional system dependencies..." - -if command -v git &> /dev/null; then - print_status "Git is available" -else - print_warning "Git is not available (optional for development)" -fi - -if command -v curl &> /dev/null; then - print_status "curl is available" -else - print_warning "curl is not available (optional for testing)" -fi - -# Make scripts executable -echo -echo "🔧 Making scripts executable..." -chmod +x install_prerequisites.py -chmod +x run_bot_debug.py -chmod +x run_bot_with_reconnect.py -chmod +x test_backup_simple.py -chmod +x test_reconnection.py - -print_status "Made scripts executable" - -# Show summary -echo -echo "🎉 Installation completed successfully!" -echo -echo "📋 Next steps:" -echo "1. Review and update configuration files in the config/ directory" -echo "2. Test the bot with: python3 run_bot_debug.py" -echo "3. Or use the new reconnection system: python3 run_bot_with_reconnect.py" -echo "4. Check the web interface at: http://localhost:8080" -echo "5. Review CLAUDE.md for development guidelines" -echo -echo "🔧 Available test commands:" -echo " python3 test_backup_simple.py - Test backup system" -echo " python3 test_reconnection.py - Test IRC reconnection" -echo -echo "📚 Documentation:" -echo " README.md - Project overview" -echo " CLAUDE.md - Development guide" -echo " TODO.md - Current status" -echo " issues.txt - Security audit findings" -echo " BACKUP_SYSTEM_INTEGRATION.md - Backup system guide" -echo -echo "🚀 You're ready to run PetBot!" -echo "✅ All prerequisites have been installed successfully!" \ No newline at end of file diff --git a/install_prerequisites_fixed.py b/install_prerequisites_fixed.py deleted file mode 100755 index 5d06dfc..0000000 --- a/install_prerequisites_fixed.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python3 -""" -PetBot Prerequisites Installation Script (Fixed for externally-managed-environment) -This script handles the externally-managed-environment error by using virtual environments. -""" - -import subprocess -import sys -import os -import importlib.util -import venv -from pathlib import Path - - -def print_header(): - """Print installation header.""" - print("=" * 60) - print("🐾 PetBot Prerequisites Installation Script (Fixed)") - print("=" * 60) - print() - - -def check_python_version(): - """Check if Python version is compatible.""" - print("🔍 Checking Python version...") - - version = sys.version_info - required_major = 3 - required_minor = 7 - - if version.major < required_major or (version.major == required_major and version.minor < required_minor): - print(f"❌ Python {required_major}.{required_minor}+ required. Current version: {version.major}.{version.minor}.{version.micro}") - print("Please upgrade Python and try again.") - return False - - print(f"✅ Python {version.major}.{version.minor}.{version.micro} is compatible") - return True - - -def check_venv_available(): - """Check if venv module is available.""" - print("\n🔍 Checking venv availability...") - - try: - import venv - print("✅ venv module is available") - return True - except ImportError: - print("❌ venv module is not available") - print("Please install python3-venv:") - print(" Ubuntu/Debian: sudo apt install python3-venv") - print(" CentOS/RHEL: sudo yum install python3-venv") - return False - - -def create_virtual_environment(): - """Create a virtual environment for the project.""" - print("\n🔧 Creating virtual environment...") - - venv_path = Path("venv") - - if venv_path.exists(): - print("✅ Virtual environment already exists") - return str(venv_path) - - try: - venv.create(venv_path, with_pip=True) - print("✅ Virtual environment created successfully") - return str(venv_path) - except Exception as e: - print(f"❌ Failed to create virtual environment: {e}") - return None - - -def get_venv_python_path(venv_path): - """Get the Python executable path in the virtual environment.""" - if os.name == 'nt': # Windows - return os.path.join(venv_path, 'Scripts', 'python.exe') - else: # Linux/macOS - return os.path.join(venv_path, 'bin', 'python') - - -def get_venv_pip_path(venv_path): - """Get the pip executable path in the virtual environment.""" - if os.name == 'nt': # Windows - return os.path.join(venv_path, 'Scripts', 'pip.exe') - else: # Linux/macOS - return os.path.join(venv_path, 'bin', 'pip') - - -def install_packages_in_venv(venv_path): - """Install packages in the virtual environment.""" - print("\n📦 Installing packages in virtual environment...") - - venv_pip = get_venv_pip_path(venv_path) - - if not os.path.exists(venv_pip): - print("❌ pip not found in virtual environment") - return False - - # Upgrade pip first - print("🔄 Upgrading pip...") - try: - subprocess.run([venv_pip, "install", "--upgrade", "pip"], check=True, capture_output=True) - print("✅ pip upgraded successfully") - except subprocess.CalledProcessError as e: - print(f"⚠️ pip upgrade failed: {e}") - - # Install from requirements.txt - requirements_file = Path("requirements.txt") - if requirements_file.exists(): - print("📋 Installing from requirements.txt...") - try: - result = subprocess.run([venv_pip, "install", "-r", "requirements.txt"], - check=True, capture_output=True, text=True) - print("✅ Successfully installed packages from requirements.txt") - return True - except subprocess.CalledProcessError as e: - print(f"❌ Failed to install from requirements.txt: {e.stderr}") - - # Install individual packages - print("📦 Installing individual packages...") - packages = [ - "irc>=20.3.0", - "aiosqlite>=0.19.0", - "python-dotenv>=1.0.0" - ] - - success = True - for package in packages: - print(f"Installing {package}...") - try: - subprocess.run([venv_pip, "install", package], check=True, capture_output=True) - print(f"✅ Successfully installed {package}") - except subprocess.CalledProcessError as e: - print(f"❌ Failed to install {package}") - success = False - - return success - - -def verify_installation_in_venv(venv_path): - """Verify installation in virtual environment.""" - print("\n🔍 Verifying installation in virtual environment...") - - venv_python = get_venv_python_path(venv_path) - - test_modules = [ - "irc", - "aiosqlite", - "dotenv", - "asyncio", - "sqlite3", - "json", - "socket" - ] - - all_good = True - - for module in test_modules: - try: - result = subprocess.run([venv_python, "-c", f"import {module}"], - check=True, capture_output=True) - print(f"✅ {module}: Available") - except subprocess.CalledProcessError: - print(f"❌ {module}: Not available") - all_good = False - - return all_good - - -def create_activation_script(): - """Create a script to activate the virtual environment.""" - print("\n🔧 Creating activation script...") - - activation_script = """#!/bin/bash -# PetBot Virtual Environment Activation Script - -echo "🐾 Activating PetBot virtual environment..." - -if [ -d "venv" ]; then - source venv/bin/activate - echo "✅ Virtual environment activated" - echo "🚀 You can now run:" - echo " python run_bot_with_reconnect.py" - echo " python test_backup_simple.py" - echo " python test_reconnection.py" - echo "" - echo "💡 To deactivate: deactivate" -else - echo "❌ Virtual environment not found" - echo "Run: python3 install_prerequisites_fixed.py" -fi -""" - - try: - with open("activate_petbot.sh", "w") as f: - f.write(activation_script) - os.chmod("activate_petbot.sh", 0o755) - print("✅ Created activate_petbot.sh") - return True - except Exception as e: - print(f"❌ Failed to create activation script: {e}") - return False - - -def create_run_scripts(): - """Create wrapper scripts that use the virtual environment.""" - print("\n🔧 Creating wrapper scripts...") - - scripts = { - "run_petbot.sh": """#!/bin/bash -# PetBot Runner Script (uses virtual environment) - -if [ -d "venv" ]; then - source venv/bin/activate - echo "🚀 Starting PetBot with auto-reconnect..." - python run_bot_with_reconnect.py -else - echo "❌ Virtual environment not found" - echo "Run: python3 install_prerequisites_fixed.py" -fi -""", - "run_petbot_debug.sh": """#!/bin/bash -# PetBot Debug Runner Script (uses virtual environment) - -if [ -d "venv" ]; then - source venv/bin/activate - echo "🚀 Starting PetBot in debug mode..." - python run_bot_debug.py -else - echo "❌ Virtual environment not found" - echo "Run: python3 install_prerequisites_fixed.py" -fi -""", - "test_petbot.sh": """#!/bin/bash -# PetBot Test Script (uses virtual environment) - -if [ -d "venv" ]; then - source venv/bin/activate - echo "🧪 Running PetBot tests..." - echo "1. Testing backup system..." - python test_backup_simple.py - echo "" - echo "2. Testing reconnection system..." - python test_reconnection.py -else - echo "❌ Virtual environment not found" - echo "Run: python3 install_prerequisites_fixed.py" -fi -""" - } - - success = True - for script_name, script_content in scripts.items(): - try: - with open(script_name, "w") as f: - f.write(script_content) - os.chmod(script_name, 0o755) - print(f"✅ Created {script_name}") - except Exception as e: - print(f"❌ Failed to create {script_name}: {e}") - success = False - - return success - - -def create_directories(): - """Create required directories.""" - print("\n🔧 Creating required directories...") - - directories = ["data", "backups", "logs"] - - for directory in directories: - dir_path = Path(directory) - if not dir_path.exists(): - try: - dir_path.mkdir() - print(f"✅ Created {directory}/ directory") - except Exception as e: - print(f"❌ Failed to create {directory}/ directory: {e}") - return False - else: - print(f"✅ {directory}/ directory already exists") - - return True - - -def show_usage_instructions(): - """Show usage instructions after installation.""" - print("\n🎉 Installation completed successfully!") - print("\n📋 How to use PetBot:") - print("\n🔧 Method 1: Using activation script (recommended)") - print(" ./activate_petbot.sh") - print(" python run_bot_with_reconnect.py") - print("\n🔧 Method 2: Using wrapper scripts") - print(" ./run_petbot.sh # Start bot with auto-reconnect") - print(" ./run_petbot_debug.sh # Start bot in debug mode") - print(" ./test_petbot.sh # Run tests") - print("\n🔧 Method 3: Manual activation") - print(" source venv/bin/activate") - print(" python run_bot_with_reconnect.py") - print(" deactivate # when done") - print("\n📚 Documentation:") - print(" QUICKSTART.md # Quick start guide") - print(" INSTALLATION.md # Detailed installation guide") - print(" CLAUDE.md # Development guidelines") - print("\n🌐 Web Interface:") - print(" http://localhost:8080 # Access after starting bot") - print("\n⚠️ Important Notes:") - print(" - Always activate the virtual environment before running Python scripts") - print(" - Use the wrapper scripts for convenience") - print(" - The virtual environment is in the 'venv' directory") - - -def main(): - """Main installation function.""" - print_header() - - # Check prerequisites - if not check_python_version(): - return False - - if not check_venv_available(): - return False - - # Create virtual environment - venv_path = create_virtual_environment() - if not venv_path: - return False - - # Install packages in virtual environment - if not install_packages_in_venv(venv_path): - print("\n❌ Package installation failed") - return False - - # Verify installation - if not verify_installation_in_venv(venv_path): - print("\n❌ Installation verification failed") - return False - - # Create directories - if not create_directories(): - return False - - # Create helper scripts - if not create_activation_script(): - return False - - if not create_run_scripts(): - return False - - # Show usage instructions - show_usage_instructions() - - return True - - -if __name__ == "__main__": - try: - success = main() - if success: - print("\n✅ PetBot virtual environment setup completed successfully!") - print("🚀 Use './run_petbot.sh' to start the bot") - sys.exit(0) - else: - print("\n❌ Installation failed. Please check the errors above.") - sys.exit(1) - except KeyboardInterrupt: - print("\n🛑 Installation interrupted by user") - sys.exit(1) - except Exception as e: - print(f"\n💥 Unexpected error: {e}") - import traceback - traceback.print_exc() - sys.exit(1) \ No newline at end of file diff --git a/install_prerequisites_simple.sh b/install_prerequisites_simple.sh deleted file mode 100755 index 55af563..0000000 --- a/install_prerequisites_simple.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -# PetBot Prerequisites Installation Script (Simple - fixes externally-managed-environment) - -set -e # Exit on any error - -echo "============================================================" -echo "🐾 PetBot Prerequisites Installation (Simple Fix)" -echo "============================================================" -echo - -# Colors -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[0;33m' -NC='\033[0m' - -print_success() { echo -e "${GREEN}✅${NC} $1"; } -print_error() { echo -e "${RED}❌${NC} $1"; } -print_warning() { echo -e "${YELLOW}⚠️${NC} $1"; } - -echo "🔍 Checking Python and venv..." - -# Check Python -if ! command -v python3 &> /dev/null; then - print_error "Python 3 is not installed" - exit 1 -fi - -python_version=$(python3 --version) -print_success "Python available: $python_version" - -# Check venv -if ! python3 -c "import venv" 2>/dev/null; then - print_error "python3-venv is not available" - echo "Install with: sudo apt install python3-venv" - exit 1 -fi - -print_success "venv module is available" - -echo -echo "🔧 Setting up virtual environment..." - -# Create virtual environment if it doesn't exist -if [ ! -d "venv" ]; then - python3 -m venv venv - print_success "Virtual environment created" -else - print_success "Virtual environment already exists" -fi - -# Activate virtual environment -source venv/bin/activate -print_success "Virtual environment activated" - -echo -echo "📦 Installing packages in virtual environment..." - -# Upgrade pip -pip install --upgrade pip - -# Install packages -if [ -f "requirements.txt" ]; then - pip install -r requirements.txt - print_success "Installed from requirements.txt" -else - # Install individual packages - pip install "irc>=20.3.0" "aiosqlite>=0.19.0" "python-dotenv>=1.0.0" - print_success "Installed individual packages" -fi - -echo -echo "🔍 Verifying installation..." - -# Test imports -python -c "import irc, aiosqlite, dotenv, asyncio, sqlite3; print('All modules imported successfully')" -print_success "All required modules are available" - -echo -echo "🔧 Creating directories..." - -# Create required directories -mkdir -p data backups logs -print_success "Created required directories" - -echo -echo "🔧 Creating helper scripts..." - -# Create activation script -cat > activate_petbot.sh << 'EOF' -#!/bin/bash -echo "🐾 Activating PetBot virtual environment..." -if [ -d "venv" ]; then - source venv/bin/activate - echo "✅ Virtual environment activated" - echo "🚀 You can now run:" - echo " python run_bot_with_reconnect.py" - echo " python test_backup_simple.py" - echo " python test_reconnection.py" - echo "" - echo "💡 To deactivate: deactivate" -else - echo "❌ Virtual environment not found" -fi -EOF - -# Create run script -cat > run_petbot.sh << 'EOF' -#!/bin/bash -if [ -d "venv" ]; then - source venv/bin/activate - echo "🚀 Starting PetBot with auto-reconnect..." - python run_bot_with_reconnect.py -else - echo "❌ Virtual environment not found" - echo "Run: ./install_prerequisites_simple.sh" -fi -EOF - -# Create debug run script -cat > run_petbot_debug.sh << 'EOF' -#!/bin/bash -if [ -d "venv" ]; then - source venv/bin/activate - echo "🚀 Starting PetBot in debug mode..." - python run_bot_debug.py -else - echo "❌ Virtual environment not found" - echo "Run: ./install_prerequisites_simple.sh" -fi -EOF - -# Create test script -cat > test_petbot.sh << 'EOF' -#!/bin/bash -if [ -d "venv" ]; then - source venv/bin/activate - echo "🧪 Running PetBot tests..." - echo "1. Testing backup system..." - python test_backup_simple.py - echo - echo "2. Testing reconnection system..." - python test_reconnection.py -else - echo "❌ Virtual environment not found" - echo "Run: ./install_prerequisites_simple.sh" -fi -EOF - -# Make scripts executable -chmod +x activate_petbot.sh run_petbot.sh run_petbot_debug.sh test_petbot.sh - -print_success "Created helper scripts" - -echo -echo "🧪 Testing installation..." - -# Test basic imports -python -c " -import sys, os -sys.path.append('.') -from src.database import Database -from src.irc_connection_manager import IRCConnectionManager -from src.backup_manager import BackupManager -print('✅ All project modules imported successfully') -" - -print_success "Installation test completed" - -# Deactivate for clean finish -deactivate - -echo -echo "🎉 Installation completed successfully!" -echo -echo "📋 How to use PetBot:" -echo -echo "🔧 Method 1: Using wrapper scripts (recommended)" -echo " ./run_petbot.sh # Start bot with auto-reconnect" -echo " ./run_petbot_debug.sh # Start bot in debug mode" -echo " ./test_petbot.sh # Run tests" -echo -echo "🔧 Method 2: Manual activation" -echo " source venv/bin/activate" -echo " python run_bot_with_reconnect.py" -echo " deactivate # when done" -echo -echo "🔧 Method 3: Using activation helper" -echo " ./activate_petbot.sh" -echo " # Then run commands normally" -echo -echo "🌐 Web Interface:" -echo " http://localhost:8080 # Access after starting bot" -echo -echo "⚠️ Important:" -echo " - Always use the wrapper scripts OR activate venv first" -echo " - The virtual environment is in the 'venv' directory" -echo " - This fixes the externally-managed-environment issue" -echo -echo "✅ Ready to run PetBot!" \ No newline at end of file diff --git a/issues.txt b/issues.txt deleted file mode 100644 index 2e28538..0000000 --- a/issues.txt +++ /dev/null @@ -1,264 +0,0 @@ -PETBOT SECURITY AUDIT - ISSUES REPORT -===================================== - -Generated: 2025-01-15 -Auditor: Claude Code Assistant -Scope: Complete security audit of PetBot IRC bot and web interface - -EXECUTIVE SUMMARY -================ -This security audit identified 23 distinct security vulnerabilities across the PetBot application, ranging from critical to low severity. The most concerning issues are Cross-Site Scripting (XSS) vulnerabilities in the web interface, missing security headers, and inadequate access controls. - -CRITICAL VULNERABILITIES (5 issues) -=================================== - -1. CRITICAL: XSS - Direct nickname injection in HTML output - File: webserver.py (lines 1191-1193) - Impact: Arbitrary JavaScript execution - Description: Player nicknames are directly inserted into HTML without escaping - Example: nickname = '">' - Recommendation: Implement HTML escaping for all user output - -2. CRITICAL: XSS - Page title injection - File: webserver.py (lines 2758, 2842, 4608) - Impact: JavaScript execution in page titles - Description: Nicknames inserted directly into tags - Recommendation: Escape all dynamic content in page titles - -3. CRITICAL: Missing HTTP security headers - File: webserver.py (entire file) - Impact: XSS, clickjacking, MIME sniffing attacks - Description: No CSP, X-Frame-Options, X-Content-Type-Options headers - Recommendation: Add security headers to all responses - -4. CRITICAL: No HTTPS configuration - File: webserver.py (line 4773) - Impact: Data transmitted in plaintext - Description: Server runs HTTP only, no SSL/TLS - Recommendation: Implement HTTPS with valid certificates - -5. CRITICAL: No web interface authentication - File: webserver.py (lines 564-588) - Impact: Unauthorized access to all player data - Description: Any user can access any player's profile via URL manipulation - Recommendation: Implement proper authentication and authorization - -HIGH SEVERITY VULNERABILITIES (8 issues) -======================================== - -6. HIGH: XSS - Pet data injection - File: webserver.py (lines 2139-2154) - Impact: JavaScript execution through pet names - Description: Pet nicknames and species names inserted without escaping - Recommendation: Escape all pet data before HTML output - -7. HIGH: XSS - Achievement data injection - File: webserver.py (lines 2167-2175) - Impact: JavaScript execution through achievement data - Description: Achievement names and descriptions not escaped - Recommendation: Escape achievement data in HTML output - -8. HIGH: XSS - Inventory item injection - File: webserver.py (lines 2207-2214) - Impact: JavaScript execution through item data - Description: Item names and descriptions inserted without escaping - Recommendation: Escape all item data before HTML output - -9. HIGH: Path traversal vulnerability - File: webserver.py (lines 564-565, 573-574, 584-585, 587-588) - Impact: Access to unauthorized resources - Description: Direct path extraction without validation - Example: /player/../../../etc/passwd - Recommendation: Implement path validation and sanitization - -10. HIGH: SQL injection in reset script - File: reset_players.py (lines 57, 63) - Impact: Arbitrary SQL execution - Description: F-string interpolation in SQL queries - Recommendation: Use parameterized queries or validate table names - -11. HIGH: Input validation gaps - File: Multiple modules - Impact: Various injection attacks - Description: Inconsistent input validation across modules - Recommendation: Implement comprehensive input validation - -12. HIGH: Admin authentication bypass - File: admin.py (line 18), backup_commands.py (line 58) - Impact: Unauthorized admin access - Description: Hard-coded admin checks vulnerable to IRC spoofing - Recommendation: Implement secure admin authentication - -13. HIGH: Information disclosure in error messages - File: webserver.py (lines 1111, 1331, 1643, 1872) - Impact: System information leakage - Description: Detailed error messages expose internal structure - Recommendation: Implement generic error messages for users - -MEDIUM SEVERITY VULNERABILITIES (7 issues) -========================================== - -14. MEDIUM: XSS - Error message injection - File: webserver.py (lines 1258-1267, 2075-2084, 2030-2039) - Impact: JavaScript execution through error messages - Description: Error messages containing user data not escaped - Recommendation: Escape all error message content - -15. MEDIUM: Missing rate limiting - File: webserver.py (entire file) - Impact: Brute force attacks, DoS - Description: No rate limiting on any endpoints - Recommendation: Implement rate limiting especially for PIN verification - -16. MEDIUM: Insecure session management - File: webserver.py (entire file) - Impact: Session attacks, CSRF - Description: No session tokens, CSRF protection, or timeouts - Recommendation: Implement proper session management - -17. MEDIUM: SQL injection in backup manager - File: backup_manager.py (lines 349, 353) - Impact: Potential SQL execution - Description: F-string usage with table names from sqlite_master - Recommendation: Use proper SQL escaping for dynamic table names - -18. MEDIUM: PIN system vulnerabilities - File: team_builder.py, database.py - Impact: Unauthorized team changes - Description: PIN delivery via IRC without additional verification - Recommendation: Enhance PIN system with additional verification - -19. MEDIUM: Missing access controls - File: webserver.py (lines 584-588) - Impact: Unauthorized profile access - Description: Team builder accessible by anyone - Recommendation: Implement access control for team builders - -20. MEDIUM: Debug information exposure - File: webserver.py (line 2766) - Impact: Information disclosure - Description: Extensive console logging exposes internals - Recommendation: Implement proper logging levels - -LOW SEVERITY VULNERABILITIES (3 issues) -======================================= - -21. LOW: Server binds to all interfaces - File: webserver.py (line 4773) - Impact: Increased attack surface - Description: Server accessible from all network interfaces - Recommendation: Bind to specific interface if possible - -22. LOW: No request size limits - File: webserver.py (entire file) - Impact: DoS attacks - Description: No limits on request size or JSON payload - Recommendation: Implement request size limits - -23. LOW: Missing security monitoring - File: webserver.py (entire file) - Impact: Limited attack detection - Description: No access logging or security monitoring - Recommendation: Implement comprehensive security logging - -REMEDIATION PRIORITIES -===================== - -IMMEDIATE (Critical Issues): -1. Fix XSS vulnerabilities by implementing HTML escaping -2. Add HTTP security headers (CSP, X-Frame-Options, etc.) -3. Implement HTTPS with valid SSL certificates -4. Add basic authentication for web interface -5. Fix path traversal vulnerabilities - -HIGH PRIORITY (Within 1 week): -1. Implement input validation and sanitization -2. Fix SQL injection vulnerabilities -3. Enhance admin authentication system -4. Add rate limiting for all endpoints -5. Improve error handling to prevent information disclosure - -MEDIUM PRIORITY (Within 1 month): -1. Implement proper session management -2. Add CSRF protection -3. Enhance PIN verification system -4. Implement access controls for all resources -5. Add security logging and monitoring - -LOW PRIORITY (Within 3 months): -1. Network security hardening -2. Request size limits -3. Advanced security monitoring -4. Security testing automation -5. Security documentation updates - -SECURITY TESTING RECOMMENDATIONS -================================ - -1. Automated vulnerability scanning -2. Penetration testing by security professionals -3. Code review by security experts -4. Input fuzzing tests -5. Authentication bypass testing -6. Session management testing -7. SQL injection testing -8. XSS testing with various payloads -9. CSRF testing -10. Rate limiting testing - -POSITIVE SECURITY PRACTICES FOUND -================================= - -1. Consistent use of parameterized SQL queries (prevents SQL injection) -2. PIN verification system uses cryptographically secure random generation -3. Database queries properly use ? placeholders for user input -4. No dangerous functions like eval() or exec() found -5. No system command execution with user input -6. JSON parsing includes proper error handling -7. Input normalization implemented in base module -8. PIN expiration mechanism (10 minutes) implemented -9. Single-use PIN system prevents replay attacks -10. Proper database transaction handling in critical operations - -TECHNICAL DEBT CONSIDERATIONS -============================ - -1. Implement proper templating engine with auto-escaping -2. Add web application firewall (WAF) -3. Implement Content Security Policy (CSP) -4. Add security headers middleware -5. Implement proper logging framework -6. Add security unit tests -7. Implement secure configuration management -8. Add API rate limiting -9. Implement proper error handling framework -10. Add security monitoring and alerting - -COMPLIANCE CONSIDERATIONS -======================== - -1. Data protection: Player data is publicly accessible -2. Access control: No authorization mechanism -3. Encryption: No HTTPS implementation -4. Logging: No security audit logs -5. Authentication: No proper user authentication - -CONCLUSION -========== - -The PetBot application has significant security vulnerabilities that should be addressed before production deployment. The most critical issues are XSS vulnerabilities and missing authentication controls. However, the application demonstrates good security practices in database operations and PIN generation. - -Priority should be given to: -1. Implementing proper input validation and output escaping -2. Adding authentication and authorization mechanisms -3. Securing the web interface with HTTPS and security headers -4. Implementing rate limiting and session management - -The development team should establish security practices including: -- Security code reviews -- Automated vulnerability scanning -- Regular security testing -- Security training for developers -- Incident response procedures - -This audit provides a comprehensive foundation for improving the security posture of the PetBot application. \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py index 2735fcf..624c44e 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -8,10 +8,6 @@ from .pet_management import PetManagement from .achievements import Achievements from .admin import Admin from .inventory import Inventory -from .gym_battles import GymBattles -from .team_builder import TeamBuilder -from .npc_events import NPCEventsModule -from .backup_commands import BackupCommands __all__ = [ 'CoreCommands', @@ -20,9 +16,5 @@ __all__ = [ 'PetManagement', 'Achievements', 'Admin', - 'Inventory', - 'GymBattles', - 'TeamBuilder', - 'NPCEventsModule', - 'BackupCommands' + 'Inventory' ] \ No newline at end of file diff --git a/modules/achievements.py b/modules/achievements.py index 98c5ccf..7b4f805 100644 --- a/modules/achievements.py +++ b/modules/achievements.py @@ -19,12 +19,14 @@ class Achievements(BaseModule): if not player: return - # Redirect to web interface for better achievements display - self.send_message(channel, f"🏆 {nickname}: View your complete achievements at: http://petz.rdx4.com/player/{nickname}#achievements") - - # Show quick summary in channel achievements = await self.database.get_player_achievements(player["id"]) + if achievements: - self.send_message(channel, f"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.") + self.send_message(channel, f"🏆 {nickname}'s Achievements:") + for achievement in achievements[:5]: # Show last 5 achievements + self.send_message(channel, f"• {achievement['name']}: {achievement['description']}") + + if len(achievements) > 5: + self.send_message(channel, f"... and {len(achievements) - 5} more!") else: - self.send_message(channel, f"💡 No achievements yet! Keep exploring and catching pets to unlock new areas!") \ No newline at end of file + self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!") \ No newline at end of file diff --git a/modules/admin.py b/modules/admin.py index a9b0795..8b7b40d 100644 --- a/modules/admin.py +++ b/modules/admin.py @@ -1,53 +1,21 @@ #!/usr/bin/env python3 """Admin commands module for PetBot""" -import sys -import os - -# Add parent directory to path for config import -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - from .base_module import BaseModule -from config import ADMIN_USER # Import admin user from central config - -# ============================================================================= -# ADMIN CONFIGURATION -# ============================================================================= -# To change the admin user, edit config.py in the project root -# Current admin user: {ADMIN_USER} -# ============================================================================= class Admin(BaseModule): """Handles admin-only commands like reload""" def get_commands(self): - return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather", "spawnevent", "startevent", "heal"] + return ["reload"] async def handle_command(self, channel, nickname, command, args): if command == "reload": await self.cmd_reload(channel, nickname) - elif command == "rate_stats": - await self.cmd_rate_stats(channel, nickname) - elif command == "rate_user": - await self.cmd_rate_user(channel, nickname, args) - elif command == "rate_unban": - 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) - elif command == "spawnevent": - await self.cmd_spawnevent(channel, nickname, args) - elif command == "startevent": - await self.cmd_startevent(channel, nickname, args) - elif command == "heal": - await self.cmd_heal(channel, nickname) async def cmd_reload(self, channel, nickname): - """Reload bot modules (admin only)""" - if not self.is_admin(nickname): + """Reload bot modules (megasconed only)""" + if nickname.lower() != "megasconed": self.send_message(channel, f"{nickname}: Access denied. Admin command.") return @@ -59,397 +27,4 @@ class Admin(BaseModule): else: self.send_message(channel, f"{nickname}: ❌ Module reload failed!") except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}") - - def is_admin(self, nickname): - """Check if user is admin""" - return nickname.lower() == ADMIN_USER.lower() - - async def cmd_rate_stats(self, channel, nickname): - """Show global rate limiting statistics""" - if not self.is_admin(nickname): - self.send_message(channel, f"{nickname}: Access denied. Admin command.") - return - - if not self.bot.rate_limiter: - self.send_message(channel, f"{nickname}: Rate limiter not available.") - return - - try: - stats = self.bot.rate_limiter.get_global_stats() - - response = f"{nickname}: 📊 Rate Limiter Stats:\n" - response += f"• Status: {'Enabled' if stats['enabled'] else 'Disabled'}\n" - response += f"• Requests this minute: {stats['requests_this_minute']}\n" - response += f"• Active users: {stats['active_users']}\n" - response += f"• Total requests: {stats['total_requests']}\n" - response += f"• Blocked requests: {stats['blocked_requests']}\n" - response += f"• Banned users: {stats['banned_users']}\n" - response += f"• Tracked users: {stats['tracked_users']}\n" - response += f"• Total violations: {stats['total_violations']}" - - self.send_message(channel, response) - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error getting rate stats: {str(e)}") - - async def cmd_rate_user(self, channel, nickname, args): - """Show rate limiting stats for a specific user""" - 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: !rate_user <username>") - return - - if not self.bot.rate_limiter: - self.send_message(channel, f"{nickname}: Rate limiter not available.") - return - - try: - target_user = args[0] - stats = self.bot.rate_limiter.get_user_stats(target_user) - - response = f"{nickname}: 👤 Rate Stats for {stats['user']}:\n" - response += f"• Admin exemption: {'Yes' if stats['admin_exemption'] else 'No'}\n" - response += f"• Currently banned: {'Yes' if stats['is_banned'] else 'No'}\n" - if stats['ban_expires']: - response += f"• Ban expires: {stats['ban_expires']}\n" - response += f"• Total violations: {stats['violations']}\n" - if stats['buckets']: - response += f"• Available tokens: {stats['buckets']['tokens']}\n" - response += f"• Last request: {stats['buckets']['last_request']}" - else: - response += "• No recent activity" - - self.send_message(channel, response) - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error getting user stats: {str(e)}") - - async def cmd_rate_unban(self, channel, nickname, args): - """Manually unban a user from rate limiting""" - 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: !rate_unban <username>") - return - - if not self.bot.rate_limiter: - self.send_message(channel, f"{nickname}: Rate limiter not available.") - return - - try: - target_user = args[0] - success = self.bot.rate_limiter.unban_user(target_user) - - if success: - self.send_message(channel, f"{nickname}: ✅ User {target_user} has been unbanned.") - else: - self.send_message(channel, f"{nickname}: ℹ️ User {target_user} was not banned.") - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error unbanning user: {str(e)}") - - async def cmd_rate_reset(self, channel, nickname, args): - """Reset rate limiting violations for a user""" - 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: !rate_reset <username>") - return - - if not self.bot.rate_limiter: - self.send_message(channel, f"{nickname}: Rate limiter not available.") - return - - try: - target_user = args[0] - success = self.bot.rate_limiter.reset_user_violations(target_user) - - if success: - self.send_message(channel, f"{nickname}: ✅ Violations reset for user {target_user}.") - else: - 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)}") - - 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]) - - result = 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 result.get("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") - - # Announce weather change if it actually changed - if result.get("changed"): - await self.game_engine.announce_weather_change( - location_name, result.get("previous_weather"), weather_type, "admin" - ) - else: - self.send_message(channel, f"❌ {nickname}: Failed to set weather for '{location_name}'. {result.get('error', '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)}") - - async def cmd_spawnevent(self, channel, nickname, args): - """Force spawn an NPC event (admin only)""" - if not self.is_admin(nickname): - self.send_message(channel, f"{nickname}: Access denied. Admin command.") - return - - # Default to difficulty 1 if no args provided - difficulty = 1 - if args: - try: - difficulty = int(args[0]) - if difficulty not in [1, 2, 3]: - self.send_message(channel, f"{nickname}: ❌ Difficulty must be 1, 2, or 3.") - return - except ValueError: - self.send_message(channel, f"{nickname}: ❌ Invalid difficulty. Use 1, 2, or 3.") - return - - try: - # Get the NPC events manager from the bot - if hasattr(self.bot, 'npc_events') and self.bot.npc_events: - event_id = await self.bot.npc_events.force_spawn_event(difficulty) - if event_id: - self.send_message(channel, f"🎯 {nickname}: Spawned new NPC event! Check `!events` to see it.") - else: - self.send_message(channel, f"❌ {nickname}: Failed to spawn NPC event.") - else: - self.send_message(channel, f"❌ {nickname}: NPC events system not available.") - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error spawning event: {str(e)}") - - async def cmd_startevent(self, channel, nickname, args): - """Start a specific event type (admin only)""" - if not self.is_admin(nickname): - self.send_message(channel, f"{nickname}: Access denied. Admin command.") - return - - # If no args provided, show available event types - if not args: - self.send_message(channel, f"{nickname}: Available types: resource_gathering, pet_rescue, community_project, emergency_response, festival_preparation, research_expedition, crisis_response, legendary_encounter, ancient_mystery") - return - - event_type = args[0].lower() - valid_types = ["resource_gathering", "pet_rescue", "community_project", "emergency_response", - "festival_preparation", "research_expedition", "crisis_response", - "legendary_encounter", "ancient_mystery"] - - if event_type not in valid_types: - self.send_message(channel, f"{nickname}: ❌ Invalid type. Available: {', '.join(valid_types)}") - return - - # Optional difficulty parameter - difficulty = 1 - if len(args) > 1: - try: - difficulty = int(args[1]) - if difficulty not in [1, 2, 3]: - self.send_message(channel, f"{nickname}: ❌ Difficulty must be 1, 2, or 3.") - return - except ValueError: - self.send_message(channel, f"{nickname}: ❌ Invalid difficulty. Use 1, 2, or 3.") - return - - try: - # Get the NPC events manager from the bot - if hasattr(self.bot, 'npc_events') and self.bot.npc_events: - event_id = await self.bot.npc_events.force_spawn_specific_event(event_type, difficulty) - if event_id: - self.send_message(channel, f"🎯 {nickname}: Started {event_type} event (ID: {event_id})! Check `!events` to see it.") - else: - self.send_message(channel, f"❌ {nickname}: Failed to start {event_type} event.") - else: - self.send_message(channel, f"❌ {nickname}: NPC events system not available.") - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error starting event: {str(e)}") - - - async def cmd_heal(self, channel, nickname): - """Heal active pets (available to all users with 1-hour cooldown)""" - try: - player = await self.require_player(channel, nickname) - if not player: - return - - # Check cooldown - from datetime import datetime, timedelta - last_heal = await self.database.get_last_heal_time(player["id"]) - if last_heal: - time_since_heal = datetime.now() - last_heal - if time_since_heal < timedelta(hours=1): - remaining = timedelta(hours=1) - time_since_heal - minutes_remaining = int(remaining.total_seconds() / 60) - self.send_message(channel, f"⏰ {nickname}: Heal command is on cooldown! {minutes_remaining} minutes remaining.") - return - - # Get active pets - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - self.send_message(channel, f"❌ {nickname}: You don't have any active pets to heal!") - return - - # Count how many pets need healing - pets_healed = 0 - for pet in active_pets: - if pet["hp"] < pet["max_hp"]: - # Heal pet to full HP - await self.database.update_pet_hp(pet["id"], pet["max_hp"]) - pets_healed += 1 - - if pets_healed == 0: - self.send_message(channel, f"✅ {nickname}: All your active pets are already at full health!") - return - - # Update cooldown - await self.database.update_last_heal_time(player["id"]) - - self.send_message(channel, f"💊 {nickname}: Healed {pets_healed} pet{'s' if pets_healed != 1 else ''} to full health! Next heal available in 1 hour.") - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error with heal command: {str(e)}") \ No newline at end of file + self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}") \ No newline at end of file diff --git a/modules/backup_commands.py b/modules/backup_commands.py deleted file mode 100644 index 1952e39..0000000 --- a/modules/backup_commands.py +++ /dev/null @@ -1,268 +0,0 @@ -from modules.base_module import BaseModule -from src.backup_manager import BackupManager, BackupScheduler -import asyncio -import logging -from datetime import datetime -import sys -import os - -# Add parent directory to path for config import -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from config import ADMIN_USER - - -class BackupCommands(BaseModule): - """Module for database backup management commands.""" - - def __init__(self, bot, database, game_engine): - super().__init__(bot, database, game_engine) - self.backup_manager = BackupManager() - self.scheduler = BackupScheduler(self.backup_manager) - self.scheduler_task = None - - # Setup logging - self.logger = logging.getLogger(__name__) - - # Initialize scheduler flag (will be started when needed) - self._scheduler_started = False - - async def _start_scheduler(self): - """Start the backup scheduler task.""" - if not self._scheduler_started and (self.scheduler_task is None or self.scheduler_task.done()): - try: - self.scheduler_task = asyncio.create_task(self.scheduler.start_scheduler()) - self._scheduler_started = True - self.logger.info("Backup scheduler started") - except RuntimeError: - # No event loop running, scheduler will be started later - self.logger.info("No event loop available, scheduler will start when commands are used") - - def get_commands(self): - """Return list of available backup commands.""" - return [ - "backup", "restore", "backups", "backup_stats", "backup_cleanup" - ] - - async def handle_command(self, channel, nickname, command, args): - """Handle backup-related commands.""" - - # Start scheduler if not already running - if not self._scheduler_started: - await self._start_scheduler() - - # Check if user has admin privileges for backup commands - if not await self._is_admin(nickname): - self.send_message(channel, f"{nickname}: Backup commands require admin privileges.") - return - - if command == "backup": - await self.cmd_backup(channel, nickname, args) - elif command == "restore": - await self.cmd_restore(channel, nickname, args) - elif command == "backups": - await self.cmd_list_backups(channel, nickname) - elif command == "backup_stats": - await self.cmd_backup_stats(channel, nickname) - elif command == "backup_cleanup": - await self.cmd_backup_cleanup(channel, nickname) - - async def _is_admin(self, nickname): - """Check if user has admin privileges.""" - return nickname.lower() == ADMIN_USER.lower() - - async def cmd_backup(self, channel, nickname, args): - """Create a manual backup.""" - try: - # Parse backup type from args - backup_type = "manual" - compress = True - - if args: - if "uncompressed" in args: - compress = False - if "daily" in args: - backup_type = "daily" - elif "weekly" in args: - backup_type = "weekly" - elif "monthly" in args: - backup_type = "monthly" - - self.send_message(channel, f"{nickname}: Creating {backup_type} backup...") - - result = await self.backup_manager.create_backup(backup_type, compress) - - if result["success"]: - compression_text = "compressed" if compress else "uncompressed" - self.send_message(channel, - f"✅ {nickname}: Backup created successfully! " - f"File: {result['backup_filename']} " - f"({result['size_mb']:.1f}MB, {compression_text})" - ) - else: - self.send_message(channel, f"❌ {nickname}: Backup failed: {result['error']}") - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Error creating backup: {str(e)}") - - async def cmd_restore(self, channel, nickname, args): - """Restore database from backup.""" - try: - if not args: - self.send_message(channel, f"{nickname}: Usage: !restore <backup_filename>") - return - - backup_filename = args[0] - - # Confirmation check - self.send_message(channel, - f"⚠️ {nickname}: This will restore the database from {backup_filename}. " - f"Current database will be backed up first. Type '!restore {backup_filename} confirm' to proceed." - ) - - if len(args) < 2 or args[1] != "confirm": - return - - self.send_message(channel, f"{nickname}: Restoring database from {backup_filename}...") - - result = await self.backup_manager.restore_backup(backup_filename) - - if result["success"]: - self.send_message(channel, - f"✅ {nickname}: Database restored successfully! " - f"Current database backed up as: {result['current_backup']} " - f"Verified {result['tables_verified']} tables." - ) - - # Restart bot to reload data - self.send_message(channel, f"{nickname}: ⚠️ Bot restart recommended to reload data.") - - else: - self.send_message(channel, f"❌ {nickname}: Restore failed: {result['error']}") - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Error restoring backup: {str(e)}") - - async def cmd_list_backups(self, channel, nickname): - """List available backups.""" - try: - backups = await self.backup_manager.list_backups() - - if not backups: - self.send_message(channel, f"{nickname}: No backups found.") - return - - self.send_message(channel, f"{nickname}: Available backups:") - - # Show up to 10 most recent backups - for backup in backups[:10]: - age = datetime.now() - backup["created_at"] - age_str = self._format_age(age) - - compression = "📦" if backup["compressed"] else "📄" - type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup["type"], "📋") - - self.send_message(channel, - f" {type_emoji}{compression} {backup['filename']} " - f"({backup['size_mb']:.1f}MB, {age_str} ago)" - ) - - if len(backups) > 10: - self.send_message(channel, f" ... and {len(backups) - 10} more backups") - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Error listing backups: {str(e)}") - - async def cmd_backup_stats(self, channel, nickname): - """Show backup statistics.""" - try: - stats = await self.backup_manager.get_backup_stats() - - if not stats["success"]: - self.send_message(channel, f"❌ {nickname}: Error getting stats: {stats['error']}") - return - - if stats["total_backups"] == 0: - self.send_message(channel, f"{nickname}: No backups found.") - return - - self.send_message(channel, f"{nickname}: Backup Statistics:") - self.send_message(channel, f" 📊 Total backups: {stats['total_backups']}") - self.send_message(channel, f" 💾 Total size: {stats['total_size_mb']:.1f}MB") - - if stats["oldest_backup"]: - oldest_age = datetime.now() - stats["oldest_backup"] - newest_age = datetime.now() - stats["newest_backup"] - self.send_message(channel, f" 📅 Oldest: {self._format_age(oldest_age)} ago") - self.send_message(channel, f" 🆕 Newest: {self._format_age(newest_age)} ago") - - # Show breakdown by type - for backup_type, type_stats in stats["by_type"].items(): - type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup_type, "📋") - self.send_message(channel, - f" {type_emoji} {backup_type.title()}: {type_stats['count']} backups " - f"({type_stats['size_mb']:.1f}MB)" - ) - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Error getting backup stats: {str(e)}") - - async def cmd_backup_cleanup(self, channel, nickname): - """Clean up old backups based on retention policy.""" - try: - self.send_message(channel, f"{nickname}: Cleaning up old backups...") - - result = await self.backup_manager.cleanup_old_backups() - - if result["success"]: - if result["cleaned_count"] > 0: - self.send_message(channel, - f"✅ {nickname}: Cleaned up {result['cleaned_count']} old backups. " - f"{result['remaining_backups']} backups remaining." - ) - else: - self.send_message(channel, f"{nickname}: No old backups to clean up.") - else: - self.send_message(channel, f"❌ {nickname}: Cleanup failed: {result['error']}") - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Error during cleanup: {str(e)}") - - def _format_age(self, age): - """Format a timedelta as human-readable age.""" - if age.days > 0: - return f"{age.days}d {age.seconds // 3600}h" - elif age.seconds > 3600: - return f"{age.seconds // 3600}h {(age.seconds % 3600) // 60}m" - elif age.seconds > 60: - return f"{age.seconds // 60}m" - else: - return f"{age.seconds}s" - - async def get_backup_status(self): - """Get current backup system status for monitoring.""" - try: - stats = await self.backup_manager.get_backup_stats() - - return { - "scheduler_running": self.scheduler.running, - "total_backups": stats.get("total_backups", 0), - "total_size_mb": stats.get("total_size_mb", 0), - "last_backup": stats.get("newest_backup"), - "backup_types": stats.get("by_type", {}) - } - except Exception as e: - return {"error": str(e)} - - async def shutdown(self): - """Shutdown the backup system gracefully.""" - if self.scheduler: - self.scheduler.stop_scheduler() - - if self.scheduler_task and not self.scheduler_task.done(): - self.scheduler_task.cancel() - try: - await self.scheduler_task - except asyncio.CancelledError: - pass - - self.logger.info("Backup system shutdown complete") \ No newline at end of file diff --git a/modules/base_module.py b/modules/base_module.py index 51c5a12..8a4854d 100644 --- a/modules/base_module.py +++ b/modules/base_module.py @@ -2,7 +2,6 @@ """Base module class for PetBot command modules""" import asyncio -import logging from abc import ABC, abstractmethod class BaseModule(ABC): @@ -12,16 +11,6 @@ class BaseModule(ABC): self.bot = bot self.database = database self.game_engine = game_engine - self.logger = logging.getLogger(self.__class__.__name__) - - @staticmethod - def normalize_input(user_input): - """Normalize user input by converting to lowercase for case-insensitive command processing""" - if isinstance(user_input, str): - return user_input.lower() - elif isinstance(user_input, list): - return [item.lower() if isinstance(item, str) else item for item in user_input] - return user_input @abstractmethod def get_commands(self): @@ -35,19 +24,11 @@ class BaseModule(ABC): def send_message(self, target, message): """Send message through the bot""" - # Use sync wrapper if available (new bot), otherwise fallback to old method - if hasattr(self.bot, 'send_message_sync'): - self.bot.send_message_sync(target, message) - else: - self.bot.send_message(target, message) + self.bot.send_message(target, message) def send_pm(self, nickname, message): """Send private message to user""" - # Use sync wrapper if available (new bot), otherwise fallback to old method - if hasattr(self.bot, 'send_message_sync'): - self.bot.send_message_sync(nickname, message) - else: - self.bot.send_message(nickname, message) + self.bot.send_message(nickname, message) async def get_player(self, nickname): """Get player from database""" diff --git a/modules/battle_system.py b/modules/battle_system.py index 228bc6c..7e98d5b 100644 --- a/modules/battle_system.py +++ b/modules/battle_system.py @@ -52,12 +52,6 @@ class BattleSystem(BaseModule): self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.") return - # Check if already in gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - if gym_battle: - self.send_message(channel, f"{nickname}: You're already in a gym battle! Finish your gym battle first.") - return - # Get player's active pet pets = await self.database.get_player_pets(player["id"], active_only=True) if not pets: @@ -93,7 +87,7 @@ class BattleSystem(BaseModule): if not player: return - move_name = " ".join(self.normalize_input(args)).title() # Normalize to Title Case + move_name = " ".join(args).title() # Normalize to Title Case result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name) if "error" in result: @@ -130,32 +124,16 @@ class BattleSystem(BaseModule): self.send_message(channel, battle_msg) if result["battle_over"]: - # Check if this is a gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - - if gym_battle: - await self.handle_gym_battle_completion(channel, nickname, player, result, gym_battle) + if result["winner"] == "player": + self.send_message(channel, f"🎉 {nickname}: You won the battle!") + # Remove encounter since battle is over + if player["id"] in self.bot.active_encounters: + del self.bot.active_encounters[player["id"]] else: - # Regular wild battle - if result["winner"] == "player": - self.send_message(channel, f"🎉 {nickname}: You won the battle!") - - # Award experience for victory - if player["id"] in self.bot.active_encounters: - wild_pet = self.bot.active_encounters[player["id"]] - await self.award_battle_experience(channel, nickname, player, wild_pet, "wild") - del self.bot.active_encounters[player["id"]] - else: - self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...") - - # Mark pet as fainted in database - pets = await self.database.get_player_pets(player["id"], active_only=True) - if pets: - await self.database.faint_pet(pets[0]["id"]) - - # Remove encounter - if player["id"] in self.bot.active_encounters: - del self.bot.active_encounters[player["id"]] + self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...") + # Remove encounter + if player["id"] in self.bot.active_encounters: + del self.bot.active_encounters[player["id"]] else: # Battle continues - show available moves with type-based colors moves_colored = " | ".join([ @@ -170,14 +148,6 @@ class BattleSystem(BaseModule): if not player: return - # Check if this is a gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - - if gym_battle: - # Can't flee from gym battles - self.send_message(channel, f"❌ {nickname}: You can't flee from a gym battle! Fight or forfeit with your honor intact!") - return - success = await self.game_engine.battle_engine.flee_battle(player["id"]) if success: @@ -219,165 +189,4 @@ class BattleSystem(BaseModule): pet_name = active_pet["nickname"] or active_pet["species_name"] moves_line = " | ".join(move_info) - self.send_message(channel, f"🎯 {nickname}'s {pet_name}: {moves_line}") - - async def handle_gym_battle_completion(self, channel, nickname, player, battle_result, gym_battle): - """Handle completion of a gym battle turn""" - try: - if battle_result["winner"] == "player": - # Player won this individual battle - current_pet_index = gym_battle["current_pet_index"] - gym_team = gym_battle["gym_team"] - - # Safety check for index bounds - if current_pet_index >= len(gym_team): - self.send_message(channel, f"❌ {nickname}: Gym battle state error - please !forfeit and try again") - return - - defeated_pet = gym_team[current_pet_index] - - self.send_message(channel, f"🎉 {nickname}: You defeated {defeated_pet['species_name']}!") - - # Award experience for defeating gym pet - await self.award_battle_experience(channel, nickname, player, defeated_pet, "gym") - - # Check if there are more pets - if await self.database.advance_gym_battle(player["id"]): - # More pets to fight - next_index = current_pet_index + 1 - next_pet = gym_team[next_index] - - self.send_message(channel, - f"🥊 {gym_battle['leader_name']} sends out {next_pet['species_name']} (Lv.{next_pet['level']})!") - - # Start battle with next gym pet - active_pets = await self.database.get_active_pets(player["id"]) - player_pet = active_pets[0] # Use first active pet - - # Create gym pet data for battle engine - next_gym_pet_data = { - "species_name": next_pet["species_name"], - "level": next_pet["level"], - "type1": next_pet["type1"], - "type2": next_pet["type2"], - "stats": { - "hp": next_pet["hp"], - "attack": next_pet["attack"], - "defense": next_pet["defense"], - "speed": next_pet["speed"] - } - } - - # Start next battle - battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, next_gym_pet_data) - - self.send_message(channel, - f"⚔️ Your {player_pet['species_name']} (HP: {battle['player_hp']}/{player_pet['max_hp']}) vs {next_pet['species_name']} (HP: {battle['wild_hp']}/{next_pet['hp']})") - - # Show available moves - moves_colored = " | ".join([ - f"{self.get_move_color(move['type'])}{move['name']}\x0F" - for move in battle["available_moves"] - ]) - self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>") - - else: - # All gym pets defeated - gym victory! - result = await self.database.end_gym_battle(player["id"], victory=True) - - self.send_message(channel, f"🏆 {nickname}: You defeated all of {gym_battle['leader_name']}'s pets!") - self.send_message(channel, - f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Impressive! You've earned the {gym_battle['gym_name']} badge!\"") - self.send_message(channel, f"🎉 {nickname} earned the {gym_battle['gym_name']} badge {gym_battle['badge_icon']}!") - - # Award rewards based on difficulty - money_reward = 500 + (result["difficulty_level"] * 100) - self.send_message(channel, f"💰 Rewards: ${money_reward} | 🌟 Gym mastery increased!") - - else: - # Player lost gym battle - result = await self.database.end_gym_battle(player["id"], victory=False) - - # Mark pet as fainted in database - pets = await self.database.get_player_pets(player["id"], active_only=True) - if pets: - await self.database.faint_pet(pets[0]["id"]) - - self.send_message(channel, f"💀 {nickname}: Your pet fainted!") - self.send_message(channel, - f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"") - self.send_message(channel, - f"💡 {nickname}: Try leveling up your pets or bringing items to heal during battle!") - - except Exception as e: - self.send_message(channel, f"❌ {nickname}: Gym battle error occurred - please !forfeit and try again") - self.logger.error(f"Gym battle completion error: {e}") - import traceback - self.logger.error(traceback.format_exc()) - - async def award_battle_experience(self, channel, nickname, player, defeated_pet, battle_type="wild"): - """Award experience to active pets for battle victory""" - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - return - - # Calculate experience based on defeated pet and battle type - base_exp = self.calculate_battle_exp(defeated_pet, battle_type) - - # Award to first active pet (the one that was battling) - main_pet = active_pets[0] - exp_result = await self.database.award_experience(main_pet["id"], base_exp) - - if exp_result["success"]: - # Display experience gain - self.send_message(channel, - f"⭐ {exp_result['pet_name']} gained {exp_result['exp_gained']} EXP!") - - # Handle level up - if exp_result["leveled_up"]: - await self.handle_level_up_display(channel, nickname, exp_result) - - def calculate_battle_exp(self, defeated_pet, battle_type="wild"): - """Calculate experience gain for defeating a pet""" - base_exp = defeated_pet["level"] * 10 # Base: 10 EXP per level - - # Battle type multipliers - multipliers = { - "wild": 1.0, - "gym": 2.0, # Double EXP for gym battles - "trainer": 1.5 # Future: trainer battles - } - - multiplier = multipliers.get(battle_type, 1.0) - return int(base_exp * multiplier) - - async def handle_level_up_display(self, channel, nickname, exp_result): - """Display level up information""" - levels_gained = exp_result["levels_gained"] - pet_name = exp_result["pet_name"] - - if levels_gained == 1: - self.send_message(channel, - f"🎉 {pet_name} leveled up! Now level {exp_result['new_level']}!") - else: - self.send_message(channel, - f"🎉 {pet_name} gained {levels_gained} levels! Now level {exp_result['new_level']}!") - - # Show stat increases - if "stat_increases" in exp_result: - stats = exp_result["stat_increases"] - stat_msg = f"📈 Stats increased: " - stat_parts = [] - - if stats["hp"] > 0: - stat_parts.append(f"HP +{stats['hp']}") - if stats["attack"] > 0: - stat_parts.append(f"ATK +{stats['attack']}") - if stats["defense"] > 0: - stat_parts.append(f"DEF +{stats['defense']}") - if stats["speed"] > 0: - stat_parts.append(f"SPD +{stats['speed']}") - - if stat_parts: - stat_msg += " | ".join(stat_parts) - self.send_message(channel, stat_msg) \ No newline at end of file + self.send_message(channel, f"🎯 {nickname}'s {pet_name}: {moves_line}") \ No newline at end of file diff --git a/modules/connection_monitor.py b/modules/connection_monitor.py deleted file mode 100644 index 147a94d..0000000 --- a/modules/connection_monitor.py +++ /dev/null @@ -1,240 +0,0 @@ -from modules.base_module import BaseModule -from datetime import datetime, timedelta -import asyncio -import json -import sys -import os - -# Add parent directory to path for config import -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from config import ADMIN_USER - - -class ConnectionMonitor(BaseModule): - """Module for monitoring IRC connection status and providing connection commands.""" - - def __init__(self, bot, database, game_engine=None): - super().__init__(bot, database) - self.game_engine = game_engine - self.start_time = datetime.now() - - def get_commands(self): - """Return list of available connection monitoring commands.""" - return [ - "status", "uptime", "ping", "reconnect", "connection_stats" - ] - - async def handle_command(self, channel, nickname, command, args): - """Handle connection monitoring commands.""" - - if command == "status": - await self.cmd_status(channel, nickname) - elif command == "uptime": - await self.cmd_uptime(channel, nickname) - elif command == "ping": - await self.cmd_ping(channel, nickname) - elif command == "reconnect": - await self.cmd_reconnect(channel, nickname) - elif command == "connection_stats": - await self.cmd_connection_stats(channel, nickname) - - async def cmd_status(self, channel, nickname): - """Show bot connection status.""" - try: - # Get connection manager if available - connection_manager = getattr(self.bot, 'connection_manager', None) - - if not connection_manager: - self.send_message(channel, f"{nickname}: Connection manager not available") - return - - stats = connection_manager.get_connection_stats() - state = stats.get("state", "unknown") - connected = stats.get("connected", False) - - # Status emoji - status_emoji = "🟢" if connected else "🔴" - - # Build status message - status_msg = f"{status_emoji} {nickname}: Bot Status - {state.upper()}" - - if connected: - uptime = stats.get("uptime", "unknown") - message_count = stats.get("message_count", 0) - status_msg += f" | Uptime: {uptime} | Messages: {message_count}" - else: - reconnect_attempts = stats.get("reconnect_attempts", 0) - status_msg += f" | Reconnect attempts: {reconnect_attempts}" - - self.send_message(channel, status_msg) - - except Exception as e: - self.send_message(channel, f"{nickname}: Error getting status: {str(e)}") - - async def cmd_uptime(self, channel, nickname): - """Show bot uptime.""" - try: - uptime = datetime.now() - self.start_time - - # Format uptime - days = uptime.days - hours, remainder = divmod(uptime.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - - uptime_str = f"{days}d {hours}h {minutes}m {seconds}s" - - # Get additional stats if available - connection_manager = getattr(self.bot, 'connection_manager', None) - if connection_manager: - stats = connection_manager.get_connection_stats() - reconnections = stats.get("total_reconnections", 0) - failures = stats.get("connection_failures", 0) - - uptime_str += f" | Reconnections: {reconnections} | Failures: {failures}" - - self.send_message(channel, f"⏰ {nickname}: Bot uptime: {uptime_str}") - - except Exception as e: - self.send_message(channel, f"{nickname}: Error getting uptime: {str(e)}") - - async def cmd_ping(self, channel, nickname): - """Test bot responsiveness.""" - try: - start_time = datetime.now() - - # Get connection status - connection_manager = getattr(self.bot, 'connection_manager', None) - connected = False - - if connection_manager: - connected = connection_manager.is_connected() - - end_time = datetime.now() - response_time = (end_time - start_time).total_seconds() * 1000 - - # Status indicators - connection_status = "🟢 Connected" if connected else "🔴 Disconnected" - ping_emoji = "🏓" if response_time < 100 else "🐌" - - self.send_message(channel, - f"{ping_emoji} {nickname}: Pong! Response time: {response_time:.1f}ms | {connection_status}" - ) - - except Exception as e: - self.send_message(channel, f"{nickname}: Error during ping: {str(e)}") - - async def cmd_reconnect(self, channel, nickname): - """Force reconnection (admin only).""" - try: - # Check if user is admin - if not await self._is_admin(nickname): - self.send_message(channel, f"{nickname}: This command requires admin privileges.") - return - - connection_manager = getattr(self.bot, 'connection_manager', None) - if not connection_manager: - self.send_message(channel, f"{nickname}: Connection manager not available.") - return - - self.send_message(channel, f"{nickname}: Initiating manual reconnection...") - - # Force reconnection by stopping and starting - await connection_manager.stop() - await asyncio.sleep(2) - - # Start connection manager in background - asyncio.create_task(connection_manager.start()) - - self.send_message(channel, f"{nickname}: Reconnection initiated.") - - except Exception as e: - self.send_message(channel, f"{nickname}: Error during reconnection: {str(e)}") - - async def cmd_connection_stats(self, channel, nickname): - """Show detailed connection statistics.""" - try: - connection_manager = getattr(self.bot, 'connection_manager', None) - - if not connection_manager: - self.send_message(channel, f"{nickname}: Connection manager not available") - return - - stats = connection_manager.get_connection_stats() - - # Build detailed stats message - lines = [ - f"📊 {nickname}: Connection Statistics", - f"State: {stats.get('state', 'unknown').upper()}", - f"Connected: {'Yes' if stats.get('connected') else 'No'}", - f"Uptime: {stats.get('uptime', 'unknown')}", - f"Messages: {stats.get('message_count', 0)}", - f"Reconnections: {stats.get('total_reconnections', 0)}", - f"Failures: {stats.get('connection_failures', 0)}", - f"Reconnect attempts: {stats.get('reconnect_attempts', 0)}" - ] - - # Add timing information - last_message = stats.get('last_message_time') - if last_message: - lines.append(f"Last message: {last_message}") - - last_ping = stats.get('last_ping_time') - if last_ping: - lines.append(f"Last ping: {last_ping}") - - # Send each line - for line in lines: - self.send_message(channel, line) - await asyncio.sleep(0.3) # Small delay to prevent flooding - - except Exception as e: - self.send_message(channel, f"{nickname}: Error getting connection stats: {str(e)}") - - async def _is_admin(self, nickname): - """Check if user has admin privileges.""" - return nickname.lower() == ADMIN_USER.lower() - - async def get_connection_health(self): - """Get connection health status for monitoring.""" - try: - connection_manager = getattr(self.bot, 'connection_manager', None) - - if not connection_manager: - return { - "healthy": False, - "error": "Connection manager not available" - } - - stats = connection_manager.get_connection_stats() - connected = stats.get("connected", False) - - # Check if connection is healthy - healthy = connected and stats.get("reconnect_attempts", 0) < 5 - - return { - "healthy": healthy, - "connected": connected, - "state": stats.get("state", "unknown"), - "uptime": stats.get("uptime"), - "message_count": stats.get("message_count", 0), - "reconnect_attempts": stats.get("reconnect_attempts", 0), - "total_reconnections": stats.get("total_reconnections", 0), - "connection_failures": stats.get("connection_failures", 0) - } - - except Exception as e: - return { - "healthy": False, - "error": str(e) - } - - def get_module_stats(self): - """Get module-specific statistics.""" - uptime = datetime.now() - self.start_time - - return { - "module_name": "ConnectionMonitor", - "module_uptime": str(uptime), - "commands_available": len(self.get_commands()), - "start_time": self.start_time - } \ No newline at end of file diff --git a/modules/core_commands.py b/modules/core_commands.py index ec4c497..3f2d73b 100644 --- a/modules/core_commands.py +++ b/modules/core_commands.py @@ -19,7 +19,7 @@ class CoreCommands(BaseModule): async def cmd_help(self, channel, nickname): """Send help URL to prevent rate limiting""" - self.send_message(channel, f"{nickname}: Complete command reference available at: http://petz.rdx4.com/help") + self.send_message(channel, f"{nickname}: Complete command reference available at: http://localhost:8080/help") async def cmd_start(self, channel, nickname): """Start a new player""" @@ -40,8 +40,5 @@ class CoreCommands(BaseModule): if not player: return - # Show quick summary and direct to web interface for detailed stats self.send_message(channel, - f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") - self.send_message(channel, - f"🌐 View detailed statistics at: http://petz.rdx4.com/player/{nickname}#stats") \ No newline at end of file + f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") \ No newline at end of file diff --git a/modules/exploration.py b/modules/exploration.py index 46c3195..0b1c865 100644 --- a/modules/exploration.py +++ b/modules/exploration.py @@ -7,7 +7,7 @@ class Exploration(BaseModule): """Handles exploration, travel, location, weather, and wild commands""" def get_commands(self): - return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture", "flee"] + return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"] async def handle_command(self, channel, nickname, command, args): if command == "explore": @@ -22,8 +22,6 @@ class Exploration(BaseModule): await self.cmd_wild(channel, nickname, args) elif command in ["catch", "capture"]: await self.cmd_catch(channel, nickname) - elif command == "flee": - await self.cmd_flee_encounter(channel, nickname) async def cmd_explore(self, channel, nickname): """Explore current location""" @@ -31,18 +29,6 @@ class Exploration(BaseModule): if not player: return - # Check if player has an active encounter that must be resolved first - if player["id"] in self.bot.active_encounters: - current_encounter = self.bot.active_encounters[player["id"]] - self.send_message(channel, f"{nickname}: You already have an active encounter with a wild {current_encounter['species_name']}! You must choose to !battle, !catch, or !flee before exploring again.") - return - - # Check if player is in an active battle - active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - if active_battle: - self.send_message(channel, f"{nickname}: You're currently in battle! Finish your battle before exploring.") - return - encounter = await self.game_engine.explore_location(player["id"]) if encounter["type"] == "error": @@ -60,12 +46,9 @@ class Exploration(BaseModule): if pet["type2"]: type_str += f"/{pet['type2']}" - # Record the encounter - await self.database.record_encounter(player["id"], pet["species_name"]) - self.send_message(channel, f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!") - self.send_message(channel, f"Choose your action: !battle to fight it, !catch to try catching it directly, or !flee to escape!") + self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!") async def cmd_travel(self, channel, nickname, args): """Travel to a different location""" @@ -77,43 +60,14 @@ class Exploration(BaseModule): if not player: return - # Handle various input formats and normalize location names - destination_input = self.normalize_input(" ".join(args)) - - # Map common variations to exact location names - location_mappings = { - "starter town": "Starter Town", - "whispering woods": "Whispering Woods", - "electric canyon": "Electric Canyon", - "crystal caves": "Crystal Caves", - "frozen tundra": "Frozen Tundra", - "dragon's peak": "Dragon's Peak", - "dragons peak": "Dragon's Peak", - "dragon peak": "Dragon's Peak", - "dragons-peak": "Dragon's Peak" - } - - destination = location_mappings.get(destination_input) - if not destination: - # Fall back to title case if no mapping found - destination = " ".join(self.normalize_input(args)).title() - + destination = " ".join(args).title() # Normalize to Title Case location = await self.database.get_location_by_name(destination) if not location: self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!") return - # CRITICAL FIX: Check and award any outstanding achievements before checking travel requirements - # This ensures players get credit for achievements they've earned but haven't been awarded yet - - # Check ALL possible achievements comprehensively - all_new_achievements = await self.game_engine.check_all_achievements(player["id"]) - if all_new_achievements: - for achievement in all_new_achievements: - self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}") - - # Now check if player can access this location (after awarding achievements) + # Check if player can access this location missing_requirements = await self.database.get_missing_location_requirements(player["id"], location["id"]) if missing_requirements: # Build specific message about required achievements @@ -184,7 +138,7 @@ class Exploration(BaseModule): if args: # Specific location requested - location_name = " ".join(self.normalize_input(args)).title() + location_name = " ".join(args).title() else: # Default to current location current_location = await self.database.get_player_location(player["id"]) @@ -209,13 +163,6 @@ class Exploration(BaseModule): # Check if player is in an active battle active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - gym_battle = await self.database.get_active_gym_battle(player["id"]) - - if gym_battle: - # Can't catch pets during gym battles - self.send_message(channel, f"{nickname}: You can't catch pets during gym battles! Focus on the challenge!") - return - if active_battle: # Catching during battle wild_pet = active_battle["wild_pet"] @@ -233,9 +180,6 @@ class Exploration(BaseModule): # Successful catch during battle result = await self.game_engine.attempt_catch_current_location(player["id"], wild_pet) - # Record the successful catch - await self.database.record_encounter(player["id"], wild_pet["species_name"], was_caught=True) - # End the battle await_result = await self.game_engine.battle_engine.end_battle(player["id"], "caught") @@ -248,9 +192,6 @@ class Exploration(BaseModule): for achievement in all_achievements: self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}") - # Award experience for successful catch - await self.award_catch_experience(channel, nickname, player, wild_pet) - # Remove encounter if player["id"] in self.bot.active_encounters: del self.bot.active_encounters[player["id"]] @@ -272,12 +213,6 @@ class Exploration(BaseModule): # Check for achievements after successful catch if "Success!" in result: - # Record the successful catch - await self.database.record_encounter(player["id"], target_pet["species_name"], was_caught=True) - - # Award experience for successful catch - await self.award_catch_experience(channel, nickname, player, target_pet) - type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "") total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "") @@ -289,64 +224,4 @@ class Exploration(BaseModule): # Remove the encounter regardless of success del self.bot.active_encounters[player["id"]] - self.send_message(channel, f"🎯 {nickname}: {result}") - - async def award_catch_experience(self, channel, nickname, player, caught_pet): - """Award experience to active pets for successful catch""" - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - return - - # Calculate experience for catch (less than battle victory) - base_exp = caught_pet["level"] * 5 # 5 EXP per level for catches - - # Award to first active pet - main_pet = active_pets[0] - exp_result = await self.database.award_experience(main_pet["id"], base_exp) - - if exp_result["success"]: - # Display experience gain - self.send_message(channel, - f"⭐ {exp_result['pet_name']} gained {exp_result['exp_gained']} EXP for the catch!") - - # Handle level up - if exp_result["leveled_up"]: - await self.handle_level_up_display(channel, nickname, exp_result) - - async def handle_level_up_display(self, channel, nickname, exp_result): - """Display level up information (shared with battle system)""" - from .battle_system import BattleSystem - battle_system = BattleSystem(self.bot, self.database, self.game_engine) - await battle_system.handle_level_up_display(channel, nickname, exp_result) - - async def cmd_flee_encounter(self, channel, nickname): - """Flee from an active encounter without battling""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Check if player has an active encounter to flee from - if player["id"] not in self.bot.active_encounters: - self.send_message(channel, f"{nickname}: You don't have an active encounter to flee from!") - return - - # Check if player is in an active battle - can't flee from exploration if in battle - active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - if active_battle: - self.send_message(channel, f"{nickname}: You're in battle! Use the battle system's !flee command to escape combat.") - return - - # Check if player is in a gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - if gym_battle: - self.send_message(channel, f"{nickname}: You're in a gym battle! Use !forfeit to leave the gym challenge.") - return - - # Get encounter details for message - encounter = self.bot.active_encounters[player["id"]] - - # Remove the encounter - del self.bot.active_encounters[player["id"]] - - self.send_message(channel, f"💨 {nickname}: You fled from the wild {encounter['species_name']}! You can now explore again.") - self.send_message(channel, f"💡 Use !explore to search for another encounter!") \ No newline at end of file + self.send_message(channel, f"🎯 {nickname}: {result}") \ No newline at end of file diff --git a/modules/gym_battles.py b/modules/gym_battles.py deleted file mode 100644 index 0fbd481..0000000 --- a/modules/gym_battles.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env python3 -"""Gym battle module for PetBot""" - -from .base_module import BaseModule - -class GymBattles(BaseModule): - """Handles gym challenges, battles, and badge tracking""" - - def get_commands(self): - return ["gym", "forfeit"] - - async def handle_command(self, channel, nickname, command, args): - if command == "gym": - if not args: - await self.cmd_gym_list(channel, nickname) - elif self.normalize_input(args[0]) == "list": - await self.cmd_gym_list_all(channel, nickname) - elif self.normalize_input(args[0]) == "challenge": - await self.cmd_gym_challenge(channel, nickname, args[1:]) - elif self.normalize_input(args[0]) == "info": - await self.cmd_gym_info(channel, nickname, args[1:]) - elif self.normalize_input(args[0]) == "status": - await self.cmd_gym_status(channel, nickname) - else: - await self.cmd_gym_list(channel, nickname) - elif command == "forfeit": - await self.cmd_forfeit(channel, nickname) - - async def cmd_gym_list(self, channel, nickname): - """List gyms in current location""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Get player's current location - location = await self.database.get_player_location(player["id"]) - if not location: - self.send_message(channel, f"{nickname}: You are not in a valid location!") - return - - # Get gyms in current location - gyms = await self.database.get_gyms_in_location(location["id"], player["id"]) - - if not gyms: - self.send_message(channel, f"🏛️ {nickname}: No gyms found in {location['name']}.") - return - - self.send_message(channel, f"🏛️ Gyms in {location['name']}:") - - for gym in gyms: - victories = gym.get("victories", 0) - if victories == 0: - status = "Not challenged" - difficulty = "Beginner" - else: - status = f"{victories} victories" - difficulty = f"Level {victories + 1}" - - badge_icon = gym["badge_icon"] - leader = gym["leader_name"] - theme = gym["theme"] - - self.send_message(channel, - f" {badge_icon} {gym['name']} - Leader: {leader} ({theme})") - self.send_message(channel, - f" Status: {status} | Next difficulty: {difficulty}") - - self.send_message(channel, - f"💡 Use '!gym challenge' to battle (gym name optional if only one gym in location)!") - - async def cmd_gym_list_all(self, channel, nickname): - """List all gyms across all locations""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Get all locations and their gyms - all_locations = await self.database.get_all_locations() - - self.send_message(channel, f"🗺️ {nickname}: All Gym Locations:") - - total_badges = 0 - for location in all_locations: - gyms = await self.database.get_gyms_in_location(location["id"], player["id"]) - - if gyms: - for gym in gyms: - victories = gym.get("victories", 0) - status_icon = "✅" if victories > 0 else "❌" - if victories > 0: - total_badges += 1 - - self.send_message(channel, - f" {status_icon} {location['name']}: {gym['name']} ({gym['theme']}) - {victories} victories") - - self.send_message(channel, f"🏆 Total badges earned: {total_badges}") - - async def cmd_gym_challenge(self, channel, nickname, args): - """Challenge a gym""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Get player's current location first - location = await self.database.get_player_location(player["id"]) - if not location: - self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.") - return - - # Get available gyms in current location - available_gyms = await self.database.get_gyms_in_location(location["id"]) - if not available_gyms: - self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.") - return - - gym = None - - if not args: - # No gym name provided - auto-challenge if only one gym, otherwise list options - if len(available_gyms) == 1: - gym = available_gyms[0] - self.send_message(channel, f"🏛️ {nickname}: Challenging the {gym['name']} gym in {location['name']}!") - else: - # Multiple gyms - show list and ask user to specify - gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms]) - self.send_message(channel, f"{nickname}: Multiple gyms found in {location['name']}! Specify which gym to challenge:") - self.send_message(channel, f"Available gyms: {gym_list}") - self.send_message(channel, f"💡 Use: !gym challenge \"<gym name>\"") - return - else: - # Gym name provided - find specific gym - gym_name = " ".join(self.normalize_input(args)).strip('"') - - # Look for gym in player's current location (case-insensitive) - gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"]) - if not gym: - # List available gyms in current location for helpful error message - gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms]) - self.send_message(channel, f"{nickname}: No gym named '{gym_name}' found in {location['name']}! Available gyms: {gym_list}") - return - - # Check if player has active pets - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - self.send_message(channel, f"{nickname}: You need at least one active pet to challenge a gym! Use !activate <pet> first.") - return - - # Get player's gym progress - progress = await self.database.get_player_gym_progress(player["id"], gym["id"]) - difficulty_level = (progress["victories"] if progress else 0) + 1 - difficulty_multiplier = 1.0 + (difficulty_level - 1) * 0.2 # 20% increase per victory - - # Start gym battle - await self.start_gym_battle(channel, nickname, player, gym, difficulty_level, difficulty_multiplier) - - async def start_gym_battle(self, channel, nickname, player, gym, difficulty_level, difficulty_multiplier): - """Start a gym battle""" - # Check if player is already in any battle - regular_battle = await self.game_engine.battle_engine.get_active_battle(player["id"]) - gym_battle = await self.database.get_active_gym_battle(player["id"]) - - if regular_battle or gym_battle: - self.send_message(channel, f"{nickname}: You're already in a battle! Finish your current battle first.") - return - - # Get gym team with scaling - gym_team = await self.database.get_gym_team(gym["id"], difficulty_multiplier) - - if not gym_team: - self.send_message(channel, f"{nickname}: {gym['name']} gym has no team configured!") - return - - # Display battle start - badge_icon = gym["badge_icon"] - leader = gym["leader_name"] - difficulty_name = f"Level {difficulty_level}" if difficulty_level > 1 else "Beginner" - - self.send_message(channel, - f"🏛️ {nickname} challenges the {gym['name']} gym!") - self.send_message(channel, - f"{badge_icon} Leader {leader}: \"Welcome to my {gym['theme']}-type gym! Let's see what you've got!\"") - self.send_message(channel, - f"⚔️ Difficulty: {difficulty_name} ({len(gym_team)} pets)") - - # Start gym battle state - battle_id = await self.database.start_gym_battle(player["id"], gym["id"], difficulty_level, gym_team) - - # Start battle with first gym pet - first_gym_pet = gym_team[0] - active_pets = await self.database.get_active_pets(player["id"]) - player_pet = active_pets[0] # Use first active pet - - # Create gym pet in wild pet format for battle engine - gym_pet_data = { - "species_name": first_gym_pet["species_name"], - "level": first_gym_pet["level"], - "type1": first_gym_pet["type1"], - "type2": first_gym_pet["type2"], - "stats": { - "hp": first_gym_pet["hp"], - "attack": first_gym_pet["attack"], - "defense": first_gym_pet["defense"], - "speed": first_gym_pet["speed"] - } - } - - # Start the battle using existing battle engine - battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, gym_pet_data) - - # Display first battle start - self.send_message(channel, - f"🥊 {leader} sends out {first_gym_pet['species_name']} (Lv.{first_gym_pet['level']})!") - self.send_message(channel, - f"⚔️ Your {player_pet['species_name']} (Lv.{player_pet['level']}, {battle['player_hp']}/{player_pet['max_hp']} HP) vs {first_gym_pet['species_name']} (Lv.{first_gym_pet['level']}, {battle['wild_hp']}/{first_gym_pet['hp']} HP)") - - # Show available moves - from .battle_system import BattleSystem - battle_system = BattleSystem(self.bot, self.database, self.game_engine) - moves_colored = " | ".join([ - f"{battle_system.get_move_color(move['type'])}{move['name']}\x0F" - for move in battle["available_moves"] - ]) - self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>") - - async def handle_gym_victory(self, channel, nickname, player, gym, difficulty_level): - """Handle gym battle victory""" - # Record victory in database - result = await self.database.record_gym_victory(player["id"], gym["id"]) - - badge_icon = gym["badge_icon"] - leader = gym["leader_name"] - - self.send_message(channel, f"🎉 {nickname} defeats the {gym['name']} gym!") - - if result["is_first_victory"]: - # First time victory - award badge - self.send_message(channel, - f"{badge_icon} Leader {leader}: \"Impressive! You've earned the {gym['badge_name']}!\"") - self.send_message(channel, - f"🏆 {nickname} earned the {gym['badge_name']} {badge_icon}!") - - # Award bonus rewards for first victory - money_reward = 500 + (difficulty_level * 100) - exp_reward = 200 + (difficulty_level * 50) - else: - # Repeat victory - self.send_message(channel, - f"{badge_icon} Leader {leader}: \"Well fought! Your skills keep improving!\"") - - money_reward = 200 + (difficulty_level * 50) - exp_reward = 100 + (difficulty_level * 25) - - # Award rewards (we'll implement this when we have currency/exp systems) - victories = result["victories"] - next_difficulty = result["next_difficulty"] - - self.send_message(channel, - f"💰 Rewards: ${money_reward} | 🌟 {exp_reward} EXP") - self.send_message(channel, - f"📊 {gym['name']} record: {victories} victories | Next challenge: Level {next_difficulty}") - - async def handle_gym_defeat(self, channel, nickname, gym, difficulty_level): - """Handle gym battle defeat""" - badge_icon = gym["badge_icon"] - leader = gym["leader_name"] - - self.send_message(channel, f"💥 {nickname} was defeated by the {gym['name']} gym!") - self.send_message(channel, - f"{badge_icon} Leader {leader}: \"Good battle! Train more and come back stronger!\"") - self.send_message(channel, - f"💡 Tip: Level up your pets, get better items, or try a different strategy!") - - async def cmd_gym_info(self, channel, nickname, args): - """Get detailed information about a gym""" - if not args: - self.send_message(channel, f"{nickname}: Specify a gym name! Example: !gym info \"Forest Guardian\"") - return - - player = await self.require_player(channel, nickname) - if not player: - return - - gym_name = " ".join(self.normalize_input(args)).strip('"') - - # First try to find gym in player's current location - location = await self.database.get_player_location(player["id"]) - gym = None - - if location: - gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"]) - - # If not found in current location, search globally - if not gym: - gym = await self.database.get_gym_by_name(gym_name) - - if not gym: - self.send_message(channel, f"{nickname}: Gym '{gym_name}' not found!") - return - - # Get gym team info - gym_team = await self.database.get_gym_team(gym["id"]) - - badge_icon = gym["badge_icon"] - - self.send_message(channel, f"🏛️ {gym['name']} Gym Information:") - self.send_message(channel, f"📍 Location: {gym['location_name']}") - self.send_message(channel, f"👤 Leader: {gym['leader_name']}") - self.send_message(channel, f"🎯 Theme: {gym['theme']}-type") - self.send_message(channel, f"📝 {gym['description']}") - self.send_message(channel, f"🏆 Badge: {gym['badge_name']} {badge_icon}") - - if gym_team: - self.send_message(channel, f"⚔️ Team ({len(gym_team)} pets):") - for pet in gym_team: - type_str = pet["type1"] - if pet["type2"]: - type_str += f"/{pet['type2']}" - self.send_message(channel, - f" • Level {pet['level']} {pet['species_name']} ({type_str})") - - async def cmd_gym_status(self, channel, nickname): - """Show player's overall gym progress""" - player = await self.require_player(channel, nickname) - if not player: - return - - # This will show a summary - for detailed view they can use !gym list - self.send_message(channel, f"🏆 {nickname}: Use !gym list to see all gym progress, or check your profile at: http://petz.rdx4.com/player/{nickname}#gym-badges") - - async def cmd_forfeit(self, channel, nickname): - """Forfeit the current gym battle""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Check if player is in a gym battle - gym_battle = await self.database.get_active_gym_battle(player["id"]) - if not gym_battle: - self.send_message(channel, f"{nickname}: You're not currently in a gym battle!") - return - - # End any active regular battle first - await self.game_engine.battle_engine.end_battle(player["id"], "forfeit") - - # End gym battle - result = await self.database.end_gym_battle(player["id"], victory=False) - - self.send_message(channel, f"🏳️ {nickname} forfeited the gym battle!") - self.send_message(channel, - f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Sometimes retreat is the wisest strategy. Come back when you're ready!\"") - self.send_message(channel, - f"💡 {nickname}: Train your pets and try again. You can challenge the gym anytime!") \ No newline at end of file diff --git a/modules/inventory.py b/modules/inventory.py index df6cc1e..1fe60ef 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -16,14 +16,51 @@ class Inventory(BaseModule): await self.cmd_use_item(channel, nickname, args) async def cmd_inventory(self, channel, nickname): - """Redirect player to their web profile for inventory management""" + """Display player's inventory""" player = await self.require_player(channel, nickname) if not player: return - # Redirect to web interface for better inventory management - self.send_message(channel, f"🎒 {nickname}: View your complete inventory at: http://petz.rdx4.com/player/{nickname}#inventory") - self.send_message(channel, f"💡 The web interface shows detailed item information, categories, and usage options!") + inventory = await self.database.get_player_inventory(player["id"]) + + if not inventory: + self.send_message(channel, f"🎒 {nickname}: Your inventory is empty! Try exploring to find items.") + return + + # Group items by category + categories = {} + for item in inventory: + category = item["category"] + if category not in categories: + categories[category] = [] + categories[category].append(item) + + # Send inventory summary first + total_items = sum(item["quantity"] for item in inventory) + self.send_message(channel, f"🎒 {nickname}'s Inventory ({total_items} items):") + + # Display items by category + rarity_symbols = { + "common": "○", + "uncommon": "◇", + "rare": "◆", + "epic": "★", + "legendary": "✦" + } + + for category, items in categories.items(): + category_display = category.replace("_", " ").title() + self.send_message(channel, f"📦 {category_display}:") + + for item in items[:5]: # Limit to 5 items per category to avoid spam + symbol = rarity_symbols.get(item["rarity"], "○") + quantity_str = f" x{item['quantity']}" if item["quantity"] > 1 else "" + self.send_message(channel, f" {symbol} {item['name']}{quantity_str} - {item['description']}") + + if len(items) > 5: + self.send_message(channel, f" ... and {len(items) - 5} more items") + + self.send_message(channel, f"💡 Use '!use <item name>' to use consumable items!") async def cmd_use_item(self, channel, nickname, args): """Use an item from inventory""" @@ -35,7 +72,7 @@ class Inventory(BaseModule): if not player: return - item_name = " ".join(self.normalize_input(args)) + item_name = " ".join(args) result = await self.database.use_item(player["id"], item_name) if not result["success"]: @@ -99,57 +136,6 @@ 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 == "revive": - # Handle revive items for fainted pets - fainted_pets = await self.database.get_fainted_pets(player["id"]) - if not fainted_pets: - self.send_message(channel, f"❌ {nickname}: You don't have any fainted pets to revive!") - return - - # Use the first fainted pet (can be expanded to choose specific pet) - pet = fainted_pets[0] - new_hp = int(pet["max_hp"] * (effect_value / 100.0)) # Convert percentage to HP - - # Revive the pet and restore HP - await self.database.revive_pet(pet["id"], new_hp) - - self.send_message(channel, - f"💫 {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " - f"Revived and restored {new_hp}/{pet['max_hp']} HP!") - - elif effect == "max_revive": - # Handle max revive items for fainted pets - fainted_pets = await self.database.get_fainted_pets(player["id"]) - if not fainted_pets: - self.send_message(channel, f"❌ {nickname}: You don't have any fainted pets to revive!") - return - - # Use the first fainted pet (can be expanded to choose specific pet) - pet = fainted_pets[0] - - # Revive the pet and fully restore HP - await self.database.revive_pet(pet["id"], pet["max_hp"]) - - self.send_message(channel, - f"✨ {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " - f"Revived and fully restored HP! ({pet['max_hp']}/{pet['max_hp']})") - - 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/npc_events.py b/modules/npc_events.py deleted file mode 100644 index acd0ad2..0000000 --- a/modules/npc_events.py +++ /dev/null @@ -1,234 +0,0 @@ -""" -NPC Events Module -Handles player commands for NPC events system -""" - -from modules.base_module import BaseModule -from src.npc_events import NPCEventsManager -from datetime import datetime - -class NPCEventsModule(BaseModule): - """Module for NPC events system commands""" - - def __init__(self, bot, database, game_engine): - super().__init__(bot, database, game_engine) - self.events_manager = NPCEventsManager(database) - - def get_commands(self): - return ['events', 'event', 'contribute', 'eventhelp'] - - async def handle_command(self, command, channel, nickname, args): - """Handle NPC events commands""" - - # Normalize command - command = self.normalize_input(command) - - if command == 'events': - await self.cmd_events(channel, nickname) - elif command == 'event': - await self.cmd_event(channel, nickname, args) - elif command == 'contribute': - await self.cmd_contribute(channel, nickname, args) - elif command == 'eventhelp': - await self.cmd_event_help(channel, nickname) - - async def cmd_events(self, channel, nickname): - """Show all active NPC events""" - try: - active_events = await self.events_manager.get_active_events() - - if not active_events: - self.send_message(channel, "📅 No active community events at the moment. Check back later!") - return - - message = "🎯 **Active Community Events:**\n" - - for event in active_events: - progress = self.events_manager.get_progress_bar( - event['current_contributions'], - event['target_contributions'] - ) - - # Calculate time remaining - expires_at = datetime.fromisoformat(event['expires_at']) - time_left = expires_at - datetime.now() - - if time_left.total_seconds() > 0: - hours_left = int(time_left.total_seconds() / 3600) - minutes_left = int((time_left.total_seconds() % 3600) / 60) - time_str = f"{hours_left}h {minutes_left}m" - else: - time_str = "Expired" - - difficulty_stars = "⭐" * event['difficulty'] - - message += f"\n**#{event['id']} - {event['title']}** {difficulty_stars}\n" - message += f"📝 {event['description']}\n" - message += f"📊 Progress: {progress}\n" - message += f"⏰ Time left: {time_str}\n" - message += f"💰 Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" - message += f"🤝 Use `!contribute {event['id']}` to help!\n" - - self.send_message(channel, message) - - except Exception as e: - self.logger.error(f"Error in cmd_events: {e}") - self.send_message(channel, f"❌ Error fetching events: {str(e)}") - - async def cmd_event(self, channel, nickname, args): - """Show details for a specific event""" - if not args: - self.send_message(channel, "❌ Usage: !event <event_id>") - return - - try: - event_id = int(args[0]) - event = await self.events_manager.get_event_details(event_id) - - if not event: - self.send_message(channel, f"❌ Event #{event_id} not found or expired.") - return - - # Calculate time remaining - expires_at = datetime.fromisoformat(event['expires_at']) - time_left = expires_at - datetime.now() - - if time_left.total_seconds() > 0: - hours_left = int(time_left.total_seconds() / 3600) - minutes_left = int((time_left.total_seconds() % 3600) / 60) - time_str = f"{hours_left}h {minutes_left}m" - else: - time_str = "Expired" - - progress = self.events_manager.get_progress_bar( - event['current_contributions'], - event['target_contributions'] - ) - - difficulty_stars = "⭐" * event['difficulty'] - status_emoji = "🔄" if event['status'] == 'active' else "✅" if event['status'] == 'completed' else "❌" - - message = f"{status_emoji} **Event #{event['id']}: {event['title']}** {difficulty_stars}\n" - message += f"📝 {event['description']}\n" - message += f"📊 Progress: {progress}\n" - message += f"⏰ Time left: {time_str}\n" - message += f"💰 Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" - message += f"🏆 Status: {event['status'].title()}\n" - - # Show leaderboard if there are contributors - if event['leaderboard']: - message += "\n🏅 **Top Contributors:**\n" - for i, contributor in enumerate(event['leaderboard'][:5]): # Top 5 - rank_emoji = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"][i] - message += f"{rank_emoji} {contributor['nickname']}: {contributor['contributions']} contributions\n" - - if event['status'] == 'active': - message += f"\n🤝 Use `!contribute {event['id']}` to help!" - - self.send_message(channel, message) - - except ValueError: - self.send_message(channel, "❌ Invalid event ID. Please use a number.") - except Exception as e: - self.logger.error(f"Error in cmd_event: {e}") - self.send_message(channel, f"❌ Error fetching event details: {str(e)}") - - async def cmd_contribute(self, channel, nickname, args): - """Allow player to contribute to an event""" - if not args: - self.send_message(channel, "❌ Usage: !contribute <event_id>") - return - - player = await self.require_player(channel, nickname) - if not player: - return - - try: - event_id = int(args[0]) - - # Check if event exists and is active - event = await self.events_manager.get_event_details(event_id) - if not event: - self.send_message(channel, f"❌ Event #{event_id} not found or expired.") - return - - if event['status'] != 'active': - self.send_message(channel, f"❌ Event #{event_id} is not active.") - return - - # Check if event has expired - expires_at = datetime.fromisoformat(event['expires_at']) - if datetime.now() >= expires_at: - self.send_message(channel, f"❌ Event #{event_id} has expired.") - return - - # Add contribution - result = await self.events_manager.contribute_to_event(event_id, player['id']) - - if not result['success']: - self.send_message(channel, f"❌ Failed to contribute: {result.get('error', 'Unknown error')}") - return - - # Get updated event details - updated_event = await self.events_manager.get_event_details(event_id) - player_contributions = await self.events_manager.get_player_contributions(player['id'], event_id) - - progress = self.events_manager.get_progress_bar( - updated_event['current_contributions'], - updated_event['target_contributions'] - ) - - if result['event_completed']: - self.send_message(channel, f"🎉 **EVENT COMPLETED!** {updated_event['completion_message']}") - self.send_message(channel, f"🏆 Thanks to everyone who participated! Rewards will be distributed shortly.") - else: - self.send_message(channel, f"✅ {nickname} contributed to '{updated_event['title']}'!") - self.send_message(channel, f"📊 Progress: {progress}") - self.send_message(channel, f"🤝 Your total contributions: {player_contributions}") - - # Show encouragement based on progress - progress_percent = (updated_event['current_contributions'] / updated_event['target_contributions']) * 100 - if progress_percent >= 75: - self.send_message(channel, "🔥 Almost there! Keep it up!") - elif progress_percent >= 50: - self.send_message(channel, "💪 Great progress! We're halfway there!") - elif progress_percent >= 25: - self.send_message(channel, "🌟 Good start! Keep contributing!") - - except ValueError: - self.send_message(channel, "❌ Invalid event ID. Please use a number.") - except Exception as e: - self.logger.error(f"Error in cmd_contribute: {e}") - self.send_message(channel, f"❌ Error contributing to event: {str(e)}") - - async def cmd_event_help(self, channel, nickname): - """Show help for NPC events system""" - message = """🎯 **Community Events System Help** - -**Available Commands:** -• `!events` - Show all active community events -• `!event <id>` - Show details for a specific event -• `!contribute <id>` - Contribute to an event -• `!eventhelp` - Show this help message - -**How Events Work:** -🌟 Random community events spawn regularly -🤝 All players can contribute to the same events -📊 Events have progress bars and time limits -🏆 Everyone who contributes gets rewards when completed -⭐ Events have different difficulty levels (1-3 stars) - -**Event Types:** -• 🏪 Resource Gathering - Help collect supplies -• 🐾 Pet Rescue - Search for missing pets -• 🎪 Community Projects - Work together on town projects -• 🚨 Emergency Response - Help during crises -• 🔬 Research - Assist with scientific discoveries - -**Tips:** -• Check `!events` regularly for new opportunities -• Higher difficulty events give better rewards -• Contributing more increases your reward multiplier -• Events expire after their time limit""" - - self.send_message(channel, message) \ No newline at end of file diff --git a/modules/pet_management.py b/modules/pet_management.py index f7e9378..0d68b4d 100644 --- a/modules/pet_management.py +++ b/modules/pet_management.py @@ -7,7 +7,7 @@ class PetManagement(BaseModule): """Handles team, pets, and future pet management commands""" def get_commands(self): - return ["team", "pets", "activate", "deactivate", "nickname", "heal", "teamname", "teamswap", "teamlist", "activeteam", "verifyteamswap"] + return ["team", "pets", "activate", "deactivate", "swap"] async def handle_command(self, channel, nickname, command, args): if command == "team": @@ -18,29 +18,40 @@ class PetManagement(BaseModule): await self.cmd_activate(channel, nickname, args) elif command == "deactivate": await self.cmd_deactivate(channel, nickname, args) - elif command == "nickname": - await self.cmd_nickname(channel, nickname, args) - elif command == "heal": - await self.cmd_heal(channel, nickname) - elif command == "teamname": - await self.cmd_teamname(channel, nickname, args) - elif command == "teamswap": - await self.cmd_teamswap(channel, nickname, args) - elif command == "teamlist": - await self.cmd_teamlist(channel, nickname) - elif command == "activeteam": - await self.cmd_activeteam(channel, nickname) - elif command == "verifyteamswap": - await self.cmd_verifyteamswap(channel, nickname, args) + elif command == "swap": + await self.cmd_swap(channel, nickname, args) async def cmd_team(self, channel, nickname): - """Redirect player to their team builder page""" + """Show active pets (channel display)""" player = await self.require_player(channel, nickname) if not player: return - # Redirect to web interface for team management - self.send_message(channel, f"⚔️ {nickname}: Manage your team at: http://petz.rdx4.com/teambuilder/{nickname}") + 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 + for pet in active_pets: + name = pet["nickname"] or pet["species_name"] + team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP") + + # 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)) async def cmd_pets(self, channel, nickname): """Show link to pet collection web page""" @@ -49,7 +60,7 @@ class PetManagement(BaseModule): return # Send URL to player's profile page instead of PM spam - self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}#pets") + self.send_message(channel, f"{nickname}: View your complete pet collection at: http://localhost:8080/player/{nickname}") async def cmd_activate(self, channel, nickname, args): """Activate a pet for battle (PM only)""" @@ -63,14 +74,13 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(self.normalize_input(args)) + pet_name = " ".join(args) result = await self.database.activate_pet(player["id"], pet_name) if result["success"]: pet = result["pet"] display_name = pet["nickname"] or pet["species_name"] - position = result.get("team_position", "?") - self.send_pm(nickname, f"✅ {display_name} is now active for battle! Team position: {position}") + self.send_pm(nickname, f"✅ {display_name} is now active for battle!") self.send_message(channel, f"{nickname}: Pet activated successfully!") else: self.send_pm(nickname, f"❌ {result['error']}") @@ -88,7 +98,7 @@ class PetManagement(BaseModule): if not player: return - pet_name = " ".join(self.normalize_input(args)) + pet_name = " ".join(args) result = await self.database.deactivate_pet(player["id"], pet_name) if result["success"]: @@ -100,295 +110,40 @@ class PetManagement(BaseModule): self.send_pm(nickname, f"❌ {result['error']}") self.send_message(channel, f"{nickname}: Pet deactivation failed - check PM for details!") - async def cmd_nickname(self, channel, nickname, args): - """Set a nickname for a pet""" + async def cmd_swap(self, channel, nickname, args): + """Swap active/storage status of two pets (PM only)""" + # Redirect to PM for privacy if len(args) < 2: - self.send_message(channel, f"{nickname}: Usage: !nickname <pet> <new_nickname>") - self.send_message(channel, f"Example: !nickname Charmander Flamey") + self.send_pm(nickname, "Usage: !swap <pet1> <pet2>") + self.send_pm(nickname, "Example: !swap Flamey Aqua") + self.send_message(channel, f"{nickname}: Pet swap instructions sent via PM!") return player = await self.require_player(channel, nickname) if not player: return - # Split args into pet identifier and new nickname - pet_identifier = self.normalize_input(args[0]) - new_nickname = " ".join(args[1:]) + # Handle multi-word pet names by splitting on first space vs last space + if len(args) == 2: + pet1_name, pet2_name = args + else: + # For more complex parsing, assume equal split + mid_point = len(args) // 2 + pet1_name = " ".join(args[:mid_point]) + pet2_name = " ".join(args[mid_point:]) - result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname) + result = await self.database.swap_pets(player["id"], pet1_name, pet2_name) if result["success"]: - old_name = result["old_name"] - new_name = result["new_nickname"] - self.send_message(channel, f"✨ {nickname}: {old_name} is now nicknamed '{new_name}'!") + pet1 = result["pet1"] + pet2 = result["pet2"] + pet1_display = pet1["nickname"] or pet1["species_name"] + pet2_display = pet2["nickname"] or pet2["species_name"] + + self.send_pm(nickname, f"🔄 Swap complete!") + self.send_pm(nickname, f" • {pet1_display} → {result['pet1_now']}") + self.send_pm(nickname, f" • {pet2_display} → {result['pet2_now']}") + self.send_message(channel, f"{nickname}: Pet swap completed!") else: - self.send_message(channel, f"❌ {nickname}: {result['error']}") - - async def cmd_heal(self, channel, nickname): - """Heal active pets (available to all users with 1-hour cooldown)""" - try: - player = await self.require_player(channel, nickname) - if not player: - return - - # Check cooldown - from datetime import datetime, timedelta - last_heal = await self.database.get_last_heal_time(player["id"]) - if last_heal: - time_since_heal = datetime.now() - last_heal - if time_since_heal < timedelta(hours=1): - remaining = timedelta(hours=1) - time_since_heal - minutes_remaining = int(remaining.total_seconds() / 60) - self.send_message(channel, f"⏰ {nickname}: Heal command is on cooldown! {minutes_remaining} minutes remaining.") - return - - # Get active pets - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - self.send_message(channel, f"❌ {nickname}: You don't have any active pets to heal!") - return - - # Count how many pets need healing - pets_healed = 0 - for pet in active_pets: - if pet["hp"] < pet["max_hp"]: - # Heal pet to full HP - await self.database.update_pet_hp(pet["id"], pet["max_hp"]) - pets_healed += 1 - - if pets_healed == 0: - self.send_message(channel, f"✅ {nickname}: All your active pets are already at full health!") - return - - # Update cooldown - await self.database.update_last_heal_time(player["id"]) - - self.send_message(channel, f"💊 {nickname}: Healed {pets_healed} pet{'s' if pets_healed != 1 else ''} to full health! Next heal available in 1 hour.") - - except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error with heal command: {str(e)}") - - async def cmd_teamname(self, channel, nickname, args): - """Redirect player to team builder for team naming""" - player = await self.require_player(channel, nickname) - if not player: - return - - # Redirect to web interface for team management - self.send_message(channel, f"🏷️ {nickname}: Rename your teams at: http://petz.rdx4.com/teambuilder/{nickname}") - self.send_message(channel, f"💡 Click on team names to rename them with secure PIN verification!") - - async def cmd_teamswap(self, channel, nickname, args): - """Switch active team to specified slot with PIN verification""" - if not args: - self.send_message(channel, f"{nickname}: Usage: !teamswap <slot>") - self.send_message(channel, f"Example: !teamswap 2") - self.send_message(channel, f"Slots: 1-3") - return - - player = await self.require_player(channel, nickname) - if not player: - return - - try: - slot = int(args[0]) - if slot < 1 or slot > 3: - self.send_message(channel, f"❌ {nickname}: Invalid slot! Must be 1, 2, or 3") - return - except ValueError: - self.send_message(channel, f"❌ {nickname}: Slot must be a number (1-3)") - return - - try: - # Check if team slot exists and has pets - config = await self.database.load_team_configuration(player["id"], slot) - if not config: - self.send_message(channel, f"❌ {nickname}: Team slot {slot} is empty! Create a team first via the web interface") - return - - # Parse team data to check if it has pets - import json - team_data = json.loads(config["team_data"]) if config["team_data"] else [] - if not team_data: - self.send_message(channel, f"❌ {nickname}: Team slot {slot} '{config['config_name']}' has no pets!") - return - - # Check if this team is already active - current_active_slot = await self.database.get_active_team_slot(player["id"]) - if current_active_slot == slot: - self.send_message(channel, f"✅ {nickname}: Team slot {slot} '{config['config_name']}' is already active!") - return - - # Get team management service for PIN-based swap - from src.team_management import TeamManagementService - from src.pin_authentication import PinAuthenticationService - - pin_service = PinAuthenticationService(self.database, self.bot) - team_service = TeamManagementService(self.database, pin_service) - - # Request team swap with PIN verification - swap_result = await team_service.request_team_swap(player["id"], nickname, slot) - - if swap_result["success"]: - pet_count = len(team_data) - self.send_message(channel, f"🔐 {nickname}: PIN sent for swapping to '{config['config_name']}' ({pet_count} pets). Check your PM!") - else: - self.send_message(channel, f"❌ {nickname}: {swap_result.get('error', 'Failed to request team swap')}") - - except Exception as e: - self.logger.error(f"Error in teamswap command: {e}") - self.send_message(channel, f"❌ {nickname}: Error processing team swap request") - - async def cmd_teamlist(self, channel, nickname): - """Show all team slots with names and pet names""" - player = await self.require_player(channel, nickname) - if not player: - return - - try: - import json - - # Get team configurations directly from database - team_configs = await self.database.get_player_team_configurations(player["id"]) - current_active_slot = await self.database.get_active_team_slot(player["id"]) - - # Build team list display - team_lines = [f"📋 {nickname}'s Teams:"] - - for slot in range(1, 4): - # Find config for this slot - config = next((c for c in team_configs if c.get("slot") == slot), None) - - if config and config.get("team_data"): - team_name = config["name"] - team_data = config["team_data"] - - # Get pet names from team data - pet_names = [] - if isinstance(team_data, list): - # New format: list of pet objects (already fetched by get_player_team_configurations) - for pet in team_data: - if pet and isinstance(pet, dict): - display_name = pet.get("nickname") or pet.get("species_name", "Unknown") - pet_names.append(display_name) - elif isinstance(team_data, dict): - # Old format: dict with positions containing pet IDs - for pos in sorted(team_data.keys()): - pet_id = team_data[pos] - if pet_id: - pet = await self.database.get_pet_by_id(pet_id) - if pet: - display_name = pet.get("nickname") or pet.get("species_name", "Unknown") - pet_names.append(display_name) - - # Mark active team - active_marker = " 🟢" if current_active_slot == slot else "" - - if pet_names: - pets_text = " - ".join(pet_names) - team_lines.append(f" {slot}. {team_name} - {pets_text}{active_marker}") - else: - team_lines.append(f" {slot}. {team_name} - empty{active_marker}") - else: - team_lines.append(f" {slot}. Team {slot} - empty") - - team_lines.append("") - team_lines.append("Commands: !teamswap <slot> | Web: http://petz.rdx4.com/teambuilder/" + nickname) - - # Send each line separately to avoid IRC length limits - for line in team_lines: - self.send_message(channel, line) - - except Exception as e: - self.logger.error(f"Error in teamlist command: {e}") - self.send_message(channel, f"❌ {nickname}: Error loading team list") - - async def cmd_activeteam(self, channel, nickname): - """Show current active team details""" - player = await self.require_player(channel, nickname) - if not player: - return - - try: - # Get active team - active_pets = await self.database.get_active_team(player["id"]) - current_slot = await self.database.get_active_team_slot(player["id"]) - - if not active_pets: - self.send_message(channel, f"❌ {nickname}: You don't have an active team! Use !teamswap or the web interface to set one") - return - - # Get team name if it's from a saved configuration - team_name = "Active Team" - if current_slot: - config = await self.database.load_team_configuration(player["id"], current_slot) - if config: - team_name = config["config_name"] - - # Build active team display - team_lines = [f"⚔️ {nickname}'s {team_name}:"] - - for i, pet in enumerate(active_pets, 1): - display_name = pet.get("nickname") or pet.get("species_name", "Unknown") - level = pet.get("level", 1) - hp = pet.get("hp", 0) - max_hp = pet.get("max_hp", 100) - - # Health status indicator - health_pct = (hp / max_hp * 100) if max_hp > 0 else 0 - if health_pct >= 75: - health_icon = "💚" - elif health_pct >= 50: - health_icon = "💛" - elif health_pct >= 25: - health_icon = "🧡" - else: - health_icon = "❤️" - - team_lines.append(f" {i}. {display_name} (Lv.{level}) {health_icon} {hp}/{max_hp}") - - team_lines.append("") - team_lines.append(f"Use !heal to restore health (1hr cooldown)") - team_lines.append(f"Manage teams: http://petz.rdx4.com/teambuilder/{nickname}") - - # Send team info - for line in team_lines: - self.send_message(channel, line) - - except Exception as e: - self.logger.error(f"Error in activeteam command: {e}") - self.send_message(channel, f"❌ {nickname}: Error loading active team") - - async def cmd_verifyteamswap(self, channel, nickname, args): - """Verify PIN and execute team swap""" - if not args: - self.send_message(channel, f"{nickname}: Usage: !verifyteamswap <pin>") - return - - player = await self.require_player(channel, nickname) - if not player: - return - - pin_code = args[0].strip() - - try: - # Get team management service - from src.team_management import TeamManagementService - from src.pin_authentication import PinAuthenticationService - - pin_service = PinAuthenticationService(self.database, self.bot) - team_service = TeamManagementService(self.database, pin_service) - - # Verify PIN and execute team swap - result = await team_service.verify_team_swap(player["id"], pin_code) - - if result["success"]: - self.send_message(channel, f"✅ {nickname}: {result['message']}") - if 'pets_applied' in result: - self.send_message(channel, f"🔄 {result['pets_applied']} pets are now active for battle!") - else: - self.send_message(channel, f"❌ {nickname}: {result.get('error', 'Team swap failed')}") - - except Exception as e: - self.logger.error(f"Error in verifyteamswap command: {e}") - self.send_message(channel, f"❌ {nickname}: Error processing team swap verification") \ No newline at end of file + self.send_pm(nickname, f"❌ {result['error']}") + self.send_message(channel, f"{nickname}: Pet swap failed - check PM for details!") \ No newline at end of file diff --git a/modules/team_builder.py b/modules/team_builder.py deleted file mode 100644 index 03fc1ce..0000000 --- a/modules/team_builder.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -"""Team builder module for PetBot - handles PIN delivery and verification""" - -from .base_module import BaseModule - -class TeamBuilder(BaseModule): - """Handles team builder PIN system and IRC integration""" - - def get_commands(self): - return [] # No direct IRC commands, only web interface - - async def handle_command(self, channel, nickname, command, args): - # No direct commands handled by this module - pass - - async def send_team_builder_pin(self, nickname, pin_code, team_name=None): - """Send PIN to player via private message""" - team_text = f" for {team_name}" if team_name else "" - message = f"""🔐 Team Builder Verification PIN{team_text}: {pin_code} - -This PIN will expire in 10 minutes. -Enter this PIN on the team builder web page to confirm your team changes. - -⚠️ Keep this PIN private! Do not share it with anyone.""" - - self.send_pm(nickname, message) - self.logger.info(f"🔐 Sent team builder PIN to {nickname}: {pin_code}{team_text}") - - async def cleanup_expired_data(self): - """Clean up expired PINs and pending requests""" - try: - result = await self.database.cleanup_expired_pins() - if result["success"] and (result["pins_cleaned"] > 0 or result["changes_cleaned"] > 0): - self.logger.info(f"🧹 Cleaned up {result['pins_cleaned']} expired PINs and {result['changes_cleaned']} pending changes") - except Exception as e: - self.logger.error(f"Error during cleanup: {e}") \ No newline at end of file diff --git a/rate_limiting_config.json b/rate_limiting_config.json deleted file mode 100644 index f23dcc0..0000000 --- a/rate_limiting_config.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "rate_limiting": { - "enabled": true, - "description": "Rate limiting configuration for PetBot IRC commands and web interface", - "categories": { - "basic": { - "description": "Basic commands like !help, !ping, !status", - "requests_per_minute": 20, - "burst_capacity": 5, - "cooldown_seconds": 1, - "commands": ["help", "ping", "status", "uptime", "connection_stats"] - }, - "gameplay": { - "description": "Gameplay commands like !explore, !catch, !battle", - "requests_per_minute": 10, - "burst_capacity": 3, - "cooldown_seconds": 3, - "commands": ["start", "explore", "catch", "battle", "attack", "moves", "flee", "travel", "weather", "gym"] - }, - "management": { - "description": "Pet and inventory management commands", - "requests_per_minute": 5, - "burst_capacity": 2, - "cooldown_seconds": 5, - "commands": ["pets", "activate", "deactivate", "stats", "inventory", "use", "nickname"] - }, - "admin": { - "description": "Administrative commands", - "requests_per_minute": 100, - "burst_capacity": 10, - "cooldown_seconds": 0, - "commands": ["backup", "restore", "backups", "backup_stats", "backup_cleanup", "reload", "reconnect", "rate_stats", "rate_user", "rate_unban", "rate_reset"] - }, - "web": { - "description": "Web interface requests", - "requests_per_minute": 60, - "burst_capacity": 10, - "cooldown_seconds": 1 - } - }, - "admin_users": ["admin", "megaproxy", "megasconed"], - "global_limits": { - "max_requests_per_minute": 200, - "max_concurrent_users": 100, - "description": "Global limits across all users and commands" - }, - "violation_penalties": { - "warning_threshold": 3, - "temporary_ban_threshold": 10, - "temporary_ban_duration": 300, - "description": "Penalties for rate limit violations (ban duration in seconds)" - }, - "monitoring": { - "log_violations": true, - "log_bans": true, - "stats_reporting_interval": 300, - "cleanup_interval": 60, - "description": "Monitoring and logging configuration" - } - } -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0ca668a..cdaf3fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,44 +1,4 @@ -# PetBot Requirements -# Core dependencies for IRC bot functionality - -# IRC client library for connecting to IRC servers irc>=20.3.0 - -# Async SQLite database interface for database operations aiosqlite>=0.19.0 - -# Environment variable loading (for configuration) python-dotenv>=1.0.0 - -# HTTP client for web interface testing and rate limiting tests -aiohttp>=3.8.0 - -# Note: The following are part of Python's standard library and don't need installation: -# - asyncio (async programming) -# - sqlite3 (synchronous SQLite operations) -# - json (JSON data handling) -# - socket (network communication) -# - threading (thread management) -# - time (time operations) -# - os (operating system interface) -# - sys (system parameters) -# - logging (logging framework) -# - pathlib (path handling) -# - urllib.parse (URL parsing) -# - datetime (date/time handling) -# - typing (type annotations) -# - enum (enumerations) -# - abc (abstract base classes) -# - importlib (import utilities) -# - signal (signal handling) -# - shutil (file operations) -# - gzip (compression) -# - tempfile (temporary files) -# - http.server (HTTP server) -# - random (random numbers) - -# Development and testing dependencies (optional) -# These are not required for basic bot operation but useful for development: -# pytest>=7.0.0 -# black>=22.0.0 -# flake8>=4.0.0 \ No newline at end of file +asyncio \ No newline at end of file diff --git a/backup_bots/run_bot.py b/run_bot.py similarity index 98% rename from backup_bots/run_bot.py rename to run_bot.py index 29fd63b..10d713c 100644 --- a/backup_bots/run_bot.py +++ b/run_bot.py @@ -10,7 +10,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.database import Database from src.game_engine import GameEngine -from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles +from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory class PetBot: def __init__(self): @@ -54,8 +54,7 @@ class PetBot: PetManagement, Achievements, Admin, - Inventory, - GymBattles + Inventory ] self.modules = {} diff --git a/run_bot_debug.py b/run_bot_debug.py index 15d14e2..825b92c 100644 --- a/run_bot_debug.py +++ b/run_bot_debug.py @@ -10,7 +10,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.database import Database from src.game_engine import GameEngine -from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder +from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory from webserver import PetBotWebServer class PetBotDebug: @@ -53,20 +53,8 @@ class PetBotDebug: self.load_modules() print("✅ Modules loaded") - print("🔄 Validating player data and achievements...") - loop.run_until_complete(self.validate_all_player_data()) - print("✅ Player data validation complete") - - print("🔄 Validating database integrity...") - loop.run_until_complete(self.validate_database_integrity()) - print("✅ Database integrity validation complete") - - print("🔄 Starting background validation task...") - self.start_background_validation(loop) - print("✅ Background validation started") - print("🔄 Starting web server...") - self.web_server = PetBotWebServer(self.database, port=8080, bot=self) + self.web_server = PetBotWebServer(self.database, port=8080) self.web_server.start_in_thread() print("✅ Web server started") @@ -82,9 +70,7 @@ class PetBotDebug: PetManagement, Achievements, Admin, - Inventory, - GymBattles, - TeamBuilder + Inventory ] self.modules = {} @@ -104,186 +90,23 @@ class PetBotDebug: print(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands") - async def validate_all_player_data(self): - """Validate and refresh all player data on startup to prevent state loss""" - try: - # Get all players from database - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute("SELECT id, nickname FROM players") - players = await cursor.fetchall() - - print(f"🔄 Found {len(players)} players to validate...") - - for player_id, nickname in players: - try: - # Check and award any missing achievements for each player - new_achievements = await self.game_engine.check_all_achievements(player_id) - - if new_achievements: - print(f" 🏆 {nickname}: Restored {len(new_achievements)} missing achievements") - for achievement in new_achievements: - print(f" - {achievement['name']}") - else: - print(f" ✅ {nickname}: All achievements up to date") - - # Validate team composition - team_composition = await self.database.get_team_composition(player_id) - if team_composition["active_pets"] == 0 and team_composition["total_pets"] > 0: - # Player has pets but none active - activate the first one - pets = await self.database.get_player_pets(player_id) - if pets: - first_pet = pets[0] - await self.database.activate_pet(player_id, str(first_pet["id"])) - print(f" 🔧 {nickname}: Auto-activated pet {first_pet['nickname'] or first_pet['species_name']} (no active pets)") - - except Exception as e: - print(f" ❌ Error validating {nickname}: {e}") - - print("✅ All player data validated and updated") - - except Exception as e: - print(f"❌ Error during player data validation: {e}") - # Don't fail startup if validation fails - - async def validate_database_integrity(self): - """Validate database integrity and fix common issues""" - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - # Check for orphaned pets - cursor = await db.execute(""" - SELECT COUNT(*) FROM pets - WHERE species_id NOT IN (SELECT id FROM pet_species) - """) - orphaned_pets = (await cursor.fetchone())[0] - - if orphaned_pets > 0: - print(f"⚠️ Found {orphaned_pets} orphaned pets - fixing references...") - # This should not happen with the new startup logic, but just in case - await self.fix_orphaned_pets(db) - - # Check player data accessibility - cursor = await db.execute("SELECT id, nickname FROM players") - players = await cursor.fetchall() - - total_accessible_pets = 0 - for player_id, nickname in players: - cursor = await db.execute(""" - SELECT COUNT(*) FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - """, (player_id,)) - accessible_pets = (await cursor.fetchone())[0] - total_accessible_pets += accessible_pets - - if accessible_pets > 0: - print(f" ✅ {nickname}: {accessible_pets} pets accessible") - else: - # Get total pets for this player - cursor = await db.execute("SELECT COUNT(*) FROM pets WHERE player_id = ?", (player_id,)) - total_pets = (await cursor.fetchone())[0] - if total_pets > 0: - print(f" ⚠️ {nickname}: {total_pets} pets but 0 accessible (orphaned)") - - print(f"✅ Database integrity check: {total_accessible_pets} total accessible pets") - - except Exception as e: - print(f"❌ Database integrity validation failed: {e}") - - async def fix_orphaned_pets(self, db): - """Fix orphaned pet references (emergency fallback)""" - try: - # This is a simplified fix - map common species names to current IDs - common_species = ['Flamey', 'Aqua', 'Leafy', 'Vinewrap', 'Bloomtail', 'Furry'] - - for species_name in common_species: - cursor = await db.execute("SELECT id FROM pet_species WHERE name = ?", (species_name,)) - species_row = await cursor.fetchone() - if species_row: - current_id = species_row[0] - # Update any pets that might be referencing old IDs for this species - await db.execute(""" - UPDATE pets SET species_id = ? - WHERE species_id NOT IN (SELECT id FROM pet_species) - AND species_id IN ( - SELECT DISTINCT p.species_id FROM pets p - WHERE p.species_id NOT IN (SELECT id FROM pet_species) - LIMIT 1 - ) - """, (current_id,)) - - await db.commit() - print(" ✅ Orphaned pets fixed") - - except Exception as e: - print(f" ❌ Failed to fix orphaned pets: {e}") - - def start_background_validation(self, loop): - """Start background task to periodically validate player data""" - import asyncio - - async def periodic_validation(): - while True: - try: - await asyncio.sleep(1800) # Run every 30 minutes - print("🔄 Running periodic player data validation...") - await self.validate_all_player_data() - except Exception as e: - print(f"❌ Error in background validation: {e}") - - # Create background task - loop.create_task(periodic_validation()) - async def reload_modules(self): """Reload all modules (for admin use)""" try: - 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 + # 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) - # 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 = {} + # Reinitialize modules + print("🔄 Reloading modules...") self.load_modules() - - print("✅ All modules reloaded successfully") + print("✅ Modules reloaded successfully") return True except Exception as e: print(f"❌ Module reload failed: {e}") @@ -412,14 +235,12 @@ class PetBotDebug: self.handle_command(channel, nickname, message) def handle_command(self, channel, nickname, message): - from modules.base_module import BaseModule - command_parts = message[1:].split() if not command_parts: return - command = BaseModule.normalize_input(command_parts[0]) - args = BaseModule.normalize_input(command_parts[1:]) + command = command_parts[0].lower() + args = command_parts[1:] try: if command in self.command_map: @@ -441,25 +262,12 @@ class PetBotDebug: self.send(f"PRIVMSG {target} :{message}") time.sleep(0.5) - async def send_team_builder_pin(self, nickname, pin_code): - """Send team builder PIN via private message""" - if hasattr(self.modules.get('TeamBuilder'), 'send_team_builder_pin'): - await self.modules['TeamBuilder'].send_team_builder_pin(nickname, pin_code) - else: - # Fallback direct PM - message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)" - self.send_message(nickname, message) - def run_async_command(self, coro): return self.loop.run_until_complete(coro) if __name__ == "__main__": print("🐾 Starting Pet Bot for IRC (Debug Mode)...") bot = PetBotDebug() - - # Make bot instance globally accessible for webserver - import sys - sys.modules[__name__].bot_instance = bot try: bot.connect() except KeyboardInterrupt: diff --git a/backup_bots/run_bot_original.py b/run_bot_original.py similarity index 100% rename from backup_bots/run_bot_original.py rename to run_bot_original.py diff --git a/run_bot_with_reconnect.py b/run_bot_with_reconnect.py deleted file mode 100644 index fdea080..0000000 --- a/run_bot_with_reconnect.py +++ /dev/null @@ -1,512 +0,0 @@ -#!/usr/bin/env python3 -""" -PetBot with Advanced IRC Connection Management -Includes automatic reconnection, health monitoring, and graceful error handling. -""" - -import asyncio -import sys -import os -import importlib -import logging -import signal -from datetime import datetime - -# Add the project directory to the path -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from src.database import Database -from src.game_engine import GameEngine -from src.irc_connection_manager import IRCConnectionManager, ConnectionState -from src.rate_limiter import RateLimiter, get_command_category -from src.npc_events import NPCEventsManager -from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder, NPCEventsModule, BackupCommands -from webserver import PetBotWebServer -from config import IRC_CONFIG, RATE_LIMIT_CONFIG - - -class PetBotWithReconnect: - """ - Enhanced PetBot with robust IRC connection management. - """ - - def __init__(self): - # Setup logging - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - self.logger = logging.getLogger(__name__) - - self.logger.info("🤖 PetBot with Auto-Reconnect - Initializing...") - - # Core components - self.database = Database() - self.game_engine = GameEngine(self.database) - self.npc_events = None - self.config = IRC_CONFIG - - # Connection and state management - self.connection_manager = None - self.running = False - self.shutdown_requested = False - - # Module management - self.modules = {} - self.command_map = {} - self.active_encounters = {} - - # Web server - self.web_server = None - - # Rate limiting - self.rate_limiter = None - - # Message queue for thread-safe IRC messaging - import queue - self.message_queue = queue.Queue() - - # Statistics - self.startup_time = datetime.now() - self.command_count = 0 - self.connection_events = [] - - self.logger.info("✅ Basic initialization complete") - - async def initialize(self): - """Initialize all async components.""" - try: - self.logger.info("🔄 Initializing async components...") - - # Initialize database - self.logger.info("🔄 Initializing database...") - await self.database.init_database() - self.logger.info("✅ Database initialized") - - # Load game data - self.logger.info("🔄 Loading game data...") - await self.game_engine.load_game_data() - - # Set bot reference for weather announcements - self.game_engine.bot = self - self.logger.info("✅ Game data loaded") - - # Initialize NPC events manager - self.logger.info("🔄 Initializing NPC events manager...") - self.npc_events = NPCEventsManager(self.database) - self.logger.info("✅ NPC events manager initialized") - - # Load modules - self.logger.info("🔄 Loading command modules...") - await self.load_modules() - self.logger.info("✅ Modules loaded") - - # Validate player data - self.logger.info("🔄 Validating player data...") - await self.validate_all_player_data() - self.logger.info("✅ Player data validation complete") - - # Initialize rate limiter with config - self.logger.info("🔄 Initializing rate limiter...") - self.rate_limiter = RateLimiter(RATE_LIMIT_CONFIG) - self.logger.info("✅ Rate limiter initialized") - - # Start web server - self.logger.info("🔄 Starting web server...") - self.web_server = PetBotWebServer(self.database, port=8080, bot=self) - self.web_server.start_in_thread() - self.logger.info("✅ Web server started on port 8080") - - # Initialize connection manager - self.logger.info("🔄 Initializing IRC connection manager...") - self.connection_manager = IRCConnectionManager(self.config, self) - self.connection_manager.set_callbacks( - on_connect=self.on_irc_connect, - on_disconnect=self.on_irc_disconnect, - on_message=self.on_irc_message, - on_connection_lost=self.on_connection_lost - ) - self.logger.info("✅ IRC connection manager initialized") - - # Start background tasks - self.logger.info("🔄 Starting background tasks...") - asyncio.create_task(self.background_validation_task()) - asyncio.create_task(self.connection_stats_task()) - asyncio.create_task(self.message_queue_processor()) - asyncio.create_task(self.npc_events.start_background_task()) - self.logger.info("✅ Background tasks started") - - self.logger.info("🎉 All components initialized successfully!") - - except Exception as e: - self.logger.error(f"❌ Initialization failed: {e}") - raise - - async def load_modules(self): - """Load all command modules.""" - module_classes = [ - CoreCommands, - Exploration, - BattleSystem, - PetManagement, - Achievements, - Admin, - Inventory, - GymBattles, - TeamBuilder, - NPCEventsModule, - BackupCommands - ] - - self.modules = {} - self.command_map = {} - - for module_class in module_classes: - try: - module_name = module_class.__name__ - self.logger.info(f" Loading {module_name}...") - - module_instance = module_class(self, self.database, self.game_engine) - self.modules[module_name] = module_instance - - # Map commands to modules - commands = module_instance.get_commands() - for command in commands: - self.command_map[command] = module_instance - - self.logger.info(f" ✅ {module_name}: {len(commands)} commands") - - except Exception as e: - self.logger.error(f" ❌ Failed to load {module_name}: {e}") - raise - - self.logger.info(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands") - - async def reload_modules(self): - """Reload all modules (for admin use).""" - try: - self.logger.info("🔄 Reloading modules...") - - # 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.inventory) - importlib.reload(modules.gym_battles) - importlib.reload(modules.team_builder) - importlib.reload(modules.backup_commands) - importlib.reload(modules) - - # Reinitialize modules - await self.load_modules() - - self.logger.info("✅ Modules reloaded successfully") - return True - - except Exception as e: - self.logger.error(f"❌ Module reload failed: {e}") - return False - - async def validate_all_player_data(self): - """Validate and refresh all player data.""" - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute("SELECT id, nickname FROM players") - players = await cursor.fetchall() - - self.logger.info(f"🔄 Validating {len(players)} players...") - - for player_id, nickname in players: - try: - # Check and award missing achievements - new_achievements = await self.game_engine.check_all_achievements(player_id) - - if new_achievements: - self.logger.info(f" 🏆 {nickname}: Restored {len(new_achievements)} achievements") - - # Validate team composition - team_composition = await self.database.get_team_composition(player_id) - if team_composition["active_pets"] == 0 and team_composition["total_pets"] > 0: - pets = await self.database.get_player_pets(player_id) - if pets: - first_pet = pets[0] - await self.database.activate_pet(player_id, str(first_pet["id"])) - self.logger.info(f" 🔧 {nickname}: Auto-activated pet {first_pet['nickname'] or first_pet['species_name']}") - - except Exception as e: - self.logger.error(f" ❌ Error validating {nickname}: {e}") - - self.logger.info("✅ Player data validation complete") - - except Exception as e: - self.logger.error(f"❌ Player data validation failed: {e}") - - async def background_validation_task(self): - """Background task for periodic validation.""" - while self.running: - try: - await asyncio.sleep(1800) # Run every 30 minutes - if self.running: - self.logger.info("🔄 Running periodic validation...") - await self.validate_all_player_data() - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"❌ Background validation error: {e}") - - async def connection_stats_task(self): - """Background task for connection statistics.""" - while self.running: - try: - await asyncio.sleep(300) # Log stats every 5 minutes - if self.running and self.connection_manager: - stats = self.connection_manager.get_connection_stats() - if stats["connected"]: - self.logger.info(f"📊 Connection stats: {stats['message_count']} messages, {stats['total_reconnections']} reconnections") - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"❌ Connection stats error: {e}") - - async def message_queue_processor(self): - """Background task to process queued messages from other threads.""" - while self.running: - try: - # Check for messages in queue (non-blocking) - try: - target, message = self.message_queue.get_nowait() - await self.send_message(target, message) - self.message_queue.task_done() - except: - # No messages in queue, sleep a bit - await asyncio.sleep(0.1) - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"❌ Message queue processor error: {e}") - - async def on_irc_connect(self): - """Called when IRC connection is established.""" - self.logger.info("🎉 IRC connection established successfully!") - self.connection_events.append({"type": "connect", "time": datetime.now()}) - - # Send welcome message to channel - if self.connection_manager: - await self.connection_manager.send_message( - self.config["channel"], - "🤖 PetBot is online and ready for commands! Use !help to get started." - ) - - async def on_irc_disconnect(self, error): - """Called when IRC connection is lost.""" - self.logger.warning(f"💔 IRC connection lost: {error}") - self.connection_events.append({"type": "disconnect", "time": datetime.now(), "error": str(error)}) - - async def on_connection_lost(self, error): - """Called when connection is lost and reconnection will be attempted.""" - self.logger.warning(f"🔄 Connection lost, will attempt reconnection: {error}") - self.connection_events.append({"type": "reconnect_attempt", "time": datetime.now(), "error": str(error)}) - - async def on_irc_message(self, line): - """Called when IRC message is received.""" - # Handle private messages and channel messages - parts = line.split() - if len(parts) < 4: - return - - if parts[1] == "PRIVMSG": - channel = parts[2] - message = " ".join(parts[3:])[1:] # Remove leading ':' - - # Extract nickname from hostmask - hostmask = parts[0][1:] # Remove leading ':' - nickname = hostmask.split('!')[0] - - # Handle commands - if message.startswith(self.config["command_prefix"]): - self.logger.info(f"🎮 Command from {nickname}: {message}") - await self.handle_command(channel, nickname, message) - - async def handle_command(self, channel, nickname, message): - """Handle IRC commands with rate limiting.""" - from modules.base_module import BaseModule - - command_parts = message[1:].split() - if not command_parts: - return - - command = BaseModule.normalize_input(command_parts[0]) - args = BaseModule.normalize_input(command_parts[1:]) - - try: - # Check rate limit first - if self.rate_limiter: - category = get_command_category(command) - allowed, rate_limit_message = await self.rate_limiter.check_rate_limit( - nickname, category, command - ) - - if not allowed: - await self.send_message(channel, f"{nickname}: {rate_limit_message}") - return - - self.command_count += 1 - - if command in self.command_map: - module = self.command_map[command] - self.logger.info(f"🔧 Executing {command} via {module.__class__.__name__}") - await module.handle_command(channel, nickname, command, args) - else: - await self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.") - - except Exception as e: - self.logger.error(f"❌ Command error: {e}") - await self.send_message(channel, f"{nickname}: Error processing command: {str(e)}") - - async def send_message(self, target, message): - """Send message via connection manager.""" - if self.connection_manager: - success = await self.connection_manager.send_message(target, message) - if not success: - self.logger.warning(f"Failed to send message to {target}: {message}") - else: - self.logger.warning(f"No connection manager available to send message to {target}") - - def send_message_sync(self, target, message): - """Synchronous wrapper for send_message (for compatibility with web server).""" - try: - # Add message to queue for processing by background task - self.message_queue.put((target, message)) - self.logger.info(f"Queued message for {target}: {message}") - except Exception as e: - self.logger.error(f"Failed to queue message: {e}") - - async def send_team_builder_pin(self, nickname, pin_code): - """Send team builder PIN via private message.""" - message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)" - await self.send_message(nickname, message) - - def get_bot_stats(self): - """Get comprehensive bot statistics.""" - uptime = datetime.now() - self.startup_time - - connection_stats = {} - if self.connection_manager: - connection_stats = self.connection_manager.get_connection_stats() - - return { - "uptime": str(uptime), - "startup_time": self.startup_time, - "command_count": self.command_count, - "loaded_modules": len(self.modules), - "available_commands": len(self.command_map), - "connection_events": len(self.connection_events), - "connection_stats": connection_stats, - "running": self.running - } - - async def start(self): - """Start the bot.""" - self.running = True - - try: - # Initialize all components - await self.initialize() - - # Start connection manager - self.logger.info("🚀 Starting IRC connection manager...") - await self.connection_manager.start() - - except Exception as e: - self.logger.error(f"❌ Bot startup failed: {e}") - raise - finally: - await self.shutdown() - - async def shutdown(self): - """Gracefully shutdown the bot.""" - if self.shutdown_requested: - return - - self.shutdown_requested = True - self.logger.info("🔄 Shutting down bot...") - - # Stop main loop - self.running = False - - # Stop connection manager - if self.connection_manager: - await self.connection_manager.stop() - - # Shutdown rate limiter - if self.rate_limiter: - try: - await self.rate_limiter.shutdown() - except Exception as e: - self.logger.error(f"Error shutting down rate limiter: {e}") - - # Shutdown game engine - if self.game_engine: - try: - await self.game_engine.shutdown() - except Exception as e: - self.logger.error(f"Error shutting down game engine: {e}") - - # Stop web server - if self.web_server: - try: - # Web server doesn't have async shutdown, so we'll just log it - self.logger.info("🔄 Web server shutdown (handled by thread)") - except Exception as e: - self.logger.error(f"Error shutting down web server: {e}") - - self.logger.info("✅ Bot shutdown complete") - - -async def main(): - """Main entry point.""" - bot = PetBotWithReconnect() - - # Make bot instance globally accessible for webserver - sys.modules[__name__].bot_instance = bot - - # Setup signal handlers for graceful shutdown - def signal_handler(signum, frame): - bot.logger.info(f"Received signal {signum}, initiating shutdown...") - asyncio.create_task(bot.shutdown()) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - await bot.start() - except KeyboardInterrupt: - bot.logger.info("🛑 Keyboard interrupt received") - except Exception as e: - bot.logger.error(f"❌ Bot crashed: {e}") - import traceback - traceback.print_exc() - finally: - await bot.shutdown() - - -if __name__ == "__main__": - print("🐾 Starting PetBot with Auto-Reconnect...") - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("\n🔄 Bot stopping...") - except Exception as e: - print(f"❌ Fatal error: {e}") - import traceback - traceback.print_exc() - finally: - print("✅ Bot stopped") \ No newline at end of file diff --git a/setup-github.sh b/setup-github.sh new file mode 100755 index 0000000..1c2d717 --- /dev/null +++ b/setup-github.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# GitHub Setup Script for PetBot +# Run this script after creating your GitHub repository and setting up authentication + +echo "🚀 PetBot GitHub Setup" +echo "======================" + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "❌ Error: Not in a git repository" + exit 1 +fi + +echo "🔐 GitHub Authentication Setup Required" +echo "=======================================" +echo "" +echo "GitHub requires secure authentication. Choose one option:" +echo "" +echo "Option 1: SSH Key (Recommended)" +echo " • More secure and convenient" +echo " • No password prompts after setup" +echo " • Repository URL format: git@github.com:username/repo.git" +echo "" +echo "Option 2: Personal Access Token" +echo " • Use HTTPS with token instead of password" +echo " • Repository URL format: https://github.com/username/repo.git" +echo "" +echo "📖 Setup guides:" +echo " SSH Keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh" +echo " Access Tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" +echo "" + +# Ask user which method they want to use +echo "Which authentication method did you set up?" +echo "1) SSH Key" +echo "2) Personal Access Token (HTTPS)" +read -p "Enter choice (1 or 2): " AUTH_CHOICE + +case $AUTH_CHOICE in + 1) + echo "" + echo "📝 Enter your SSH repository URL:" + echo " Format: git@github.com:yourusername/petbot-irc-game.git" + read -p "SSH URL: " REPO_URL + + if [[ ! "$REPO_URL" =~ ^git@github\.com: ]]; then + echo "❌ Error: Please use SSH URL format (git@github.com:username/repo.git)" + exit 1 + fi + ;; + 2) + echo "" + echo "📝 Enter your HTTPS repository URL:" + echo " Format: https://github.com/yourusername/petbot-irc-game.git" + read -p "HTTPS URL: " REPO_URL + + if [[ ! "$REPO_URL" =~ ^https://github\.com/ ]]; then + echo "❌ Error: Please use HTTPS URL format (https://github.com/username/repo.git)" + exit 1 + fi + + echo "" + echo "⚠️ Important: When prompted for password, use your Personal Access Token" + echo " Do NOT use your GitHub account password" + ;; + *) + echo "❌ Invalid choice" + exit 1 + ;; +esac + +if [ -z "$REPO_URL" ]; then + echo "❌ Error: No URL provided" + exit 1 +fi + +# Test GitHub connection +echo "" +echo "🔍 Testing GitHub connection..." +if [ "$AUTH_CHOICE" = "1" ]; then + ssh -T git@github.com 2>/dev/null + if [ $? -ne 1 ]; then + echo "❌ SSH connection test failed. Please check your SSH key setup." + echo " Guide: https://docs.github.com/en/authentication/connecting-to-github-with-ssh" + exit 1 + fi + echo "✅ SSH connection successful" +else + echo "⚠️ HTTPS connection will be tested during push" +fi + +# Add remote origin +echo "" +echo "🔗 Adding GitHub remote..." +git remote add origin "$REPO_URL" 2>/dev/null || { + echo "🔄 Remote already exists, updating..." + git remote set-url origin "$REPO_URL" +} + +# Push to GitHub +echo "⬆️ Pushing to GitHub..." +if ! git push -u origin main; then + echo "" + echo "❌ Push failed. Common solutions:" + if [ "$AUTH_CHOICE" = "1" ]; then + echo " • Check SSH key is added to GitHub: https://github.com/settings/keys" + echo " • Verify SSH agent is running: ssh-add -l" + else + echo " • Use Personal Access Token as password (not account password)" + echo " • Create token at: https://github.com/settings/tokens" + echo " • Token needs 'repo' scope for private repos" + fi + echo " • Verify repository exists and you have write access" + exit 1 +fi + +# Push tags +echo "🏷️ Pushing tags..." +git push --tags + +echo "" +echo "✅ Setup complete!" +echo "" +echo "🎯 Your repository is now on GitHub:" +echo " Repository: $REPO_URL" +echo " Current version: v0.1.0" +echo " Authentication: $([ "$AUTH_CHOICE" = "1" ] && echo "SSH Key" || echo "Personal Access Token")" +echo "" +echo "🔄 Future updates will be automatic:" +echo " - Claude will commit changes with descriptive messages" +echo " - Changelog will be updated automatically" +echo " - Version tags will be created for releases" +echo " - All changes will be pushed to GitHub" +echo "" +echo "📚 Useful commands:" +echo " git status - Check repository status" +echo " git log --oneline - View commit history" +echo " git tag -l - List all version tags" \ No newline at end of file diff --git a/src/backup_manager.py b/src/backup_manager.py deleted file mode 100644 index 6a71f7a..0000000 --- a/src/backup_manager.py +++ /dev/null @@ -1,458 +0,0 @@ -import os -import shutil -import sqlite3 -import gzip -import json -from datetime import datetime, timedelta -from typing import Dict, List, Optional -import asyncio -import aiosqlite -from pathlib import Path -import logging - - -class BackupManager: - def __init__(self, db_path: str = "data/petbot.db", backup_dir: str = "backups"): - self.db_path = db_path - self.backup_dir = Path(backup_dir) - self.backup_dir.mkdir(exist_ok=True) - - # Backup configuration - self.max_daily_backups = 7 # Keep 7 daily backups - self.max_weekly_backups = 4 # Keep 4 weekly backups - self.max_monthly_backups = 12 # Keep 12 monthly backups - - # Setup logging - self.logger = logging.getLogger(__name__) - - async def create_backup(self, backup_type: str = "manual", compress: bool = True) -> Dict: - """Create a database backup with optional compression.""" - try: - # Check if database exists - if not os.path.exists(self.db_path): - return {"success": False, "error": "Database file not found"} - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backup_filename = f"petbot_backup_{backup_type}_{timestamp}.db" - - if compress: - backup_filename += ".gz" - - backup_path = self.backup_dir / backup_filename - - # Create the backup - if compress: - await self._create_compressed_backup(backup_path) - else: - await self._create_regular_backup(backup_path) - - # Get backup info - backup_info = await self._get_backup_info(backup_path) - - # Log the backup - self.logger.info(f"Backup created: {backup_filename} ({backup_info['size_mb']:.1f}MB)") - - return { - "success": True, - "backup_path": str(backup_path), - "backup_filename": backup_filename, - "backup_type": backup_type, - "timestamp": timestamp, - "compressed": compress, - "size_mb": backup_info["size_mb"] - } - - except Exception as e: - self.logger.error(f"Backup creation failed: {str(e)}") - return {"success": False, "error": str(e)} - - async def _create_regular_backup(self, backup_path: Path): - """Create a regular SQLite backup using the backup API.""" - def backup_db(): - # Use SQLite backup API for consistent backup - source_conn = sqlite3.connect(self.db_path) - backup_conn = sqlite3.connect(str(backup_path)) - - # Perform the backup - source_conn.backup(backup_conn) - - # Close connections - source_conn.close() - backup_conn.close() - - # Run the backup in a thread to avoid blocking - await asyncio.get_event_loop().run_in_executor(None, backup_db) - - async def _create_compressed_backup(self, backup_path: Path): - """Create a compressed backup.""" - def backup_and_compress(): - # First create temporary uncompressed backup - temp_backup = backup_path.with_suffix('.tmp') - - # Use SQLite backup API - source_conn = sqlite3.connect(self.db_path) - backup_conn = sqlite3.connect(str(temp_backup)) - source_conn.backup(backup_conn) - source_conn.close() - backup_conn.close() - - # Compress the backup - with open(temp_backup, 'rb') as f_in: - with gzip.open(backup_path, 'wb') as f_out: - shutil.copyfileobj(f_in, f_out) - - # Remove temporary file - temp_backup.unlink() - - await asyncio.get_event_loop().run_in_executor(None, backup_and_compress) - - async def _get_backup_info(self, backup_path: Path) -> Dict: - """Get information about a backup file.""" - stat = backup_path.stat() - - return { - "size_bytes": stat.st_size, - "size_mb": stat.st_size / (1024 * 1024), - "created_at": datetime.fromtimestamp(stat.st_mtime), - "compressed": backup_path.suffix == '.gz' - } - - async def list_backups(self) -> List[Dict]: - """List all available backups with metadata.""" - backups = [] - - for backup_file in self.backup_dir.glob("petbot_backup_*.db*"): - try: - info = await self._get_backup_info(backup_file) - - # Parse backup filename for metadata - filename = backup_file.name - parts = filename.replace('.gz', '').replace('.db', '').split('_') - - if len(parts) >= 4: - backup_type = parts[2] - timestamp = parts[3] - - backups.append({ - "filename": filename, - "path": str(backup_file), - "type": backup_type, - "timestamp": timestamp, - "created_at": info["created_at"], - "size_mb": info["size_mb"], - "compressed": info["compressed"] - }) - - except Exception as e: - self.logger.warning(f"Error reading backup {backup_file}: {e}") - continue - - # Sort by creation time (newest first) - backups.sort(key=lambda x: x["created_at"], reverse=True) - - return backups - - async def restore_backup(self, backup_filename: str, target_path: str = None) -> Dict: - """Restore a database from backup.""" - try: - backup_path = self.backup_dir / backup_filename - - if not backup_path.exists(): - return {"success": False, "error": "Backup file not found"} - - target_path = target_path or self.db_path - - # Create backup of current database before restore - current_backup = await self.create_backup("pre_restore", compress=True) - if not current_backup["success"]: - return {"success": False, "error": "Failed to backup current database"} - - # Restore the backup - if backup_path.suffix == '.gz': - await self._restore_compressed_backup(backup_path, target_path) - else: - await self._restore_regular_backup(backup_path, target_path) - - # Verify the restored database - verification = await self._verify_database(target_path) - if not verification["success"]: - return {"success": False, "error": f"Restored database verification failed: {verification['error']}"} - - self.logger.info(f"Database restored from backup: {backup_filename}") - - return { - "success": True, - "backup_filename": backup_filename, - "target_path": target_path, - "current_backup": current_backup["backup_filename"], - "tables_verified": verification["table_count"] - } - - except Exception as e: - self.logger.error(f"Restore failed: {str(e)}") - return {"success": False, "error": str(e)} - - async def _restore_regular_backup(self, backup_path: Path, target_path: str): - """Restore from regular backup.""" - def restore(): - shutil.copy2(backup_path, target_path) - - await asyncio.get_event_loop().run_in_executor(None, restore) - - async def _restore_compressed_backup(self, backup_path: Path, target_path: str): - """Restore from compressed backup.""" - def restore(): - with gzip.open(backup_path, 'rb') as f_in: - with open(target_path, 'wb') as f_out: - shutil.copyfileobj(f_in, f_out) - - await asyncio.get_event_loop().run_in_executor(None, restore) - - async def _verify_database(self, db_path: str) -> Dict: - """Verify database integrity and structure.""" - try: - async with aiosqlite.connect(db_path) as db: - # Check database integrity - cursor = await db.execute("PRAGMA integrity_check") - integrity_result = await cursor.fetchone() - - if integrity_result[0] != "ok": - return {"success": False, "error": f"Database integrity check failed: {integrity_result[0]}"} - - # Count tables - cursor = await db.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'") - table_count = (await cursor.fetchone())[0] - - # Basic table existence check - required_tables = ["players", "pets", "pet_species", "moves", "items"] - for table in required_tables: - cursor = await db.execute(f"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", (table,)) - exists = (await cursor.fetchone())[0] - if not exists: - return {"success": False, "error": f"Required table '{table}' not found"} - - return {"success": True, "table_count": table_count} - - except Exception as e: - return {"success": False, "error": str(e)} - - async def cleanup_old_backups(self) -> Dict: - """Remove old backups based on retention policy.""" - try: - backups = await self.list_backups() - - # Group backups by type - daily_backups = [b for b in backups if b["type"] in ["daily", "manual"]] - weekly_backups = [b for b in backups if b["type"] == "weekly"] - monthly_backups = [b for b in backups if b["type"] == "monthly"] - - cleaned_count = 0 - - # Clean daily backups (keep most recent) - if len(daily_backups) > self.max_daily_backups: - old_daily = daily_backups[self.max_daily_backups:] - for backup in old_daily: - await self._remove_backup(backup["path"]) - cleaned_count += 1 - - # Clean weekly backups - if len(weekly_backups) > self.max_weekly_backups: - old_weekly = weekly_backups[self.max_weekly_backups:] - for backup in old_weekly: - await self._remove_backup(backup["path"]) - cleaned_count += 1 - - # Clean monthly backups - if len(monthly_backups) > self.max_monthly_backups: - old_monthly = monthly_backups[self.max_monthly_backups:] - for backup in old_monthly: - await self._remove_backup(backup["path"]) - cleaned_count += 1 - - self.logger.info(f"Cleaned up {cleaned_count} old backups") - - return { - "success": True, - "cleaned_count": cleaned_count, - "remaining_backups": len(backups) - cleaned_count - } - - except Exception as e: - self.logger.error(f"Cleanup failed: {str(e)}") - return {"success": False, "error": str(e)} - - async def _remove_backup(self, backup_path: str): - """Remove a backup file.""" - try: - Path(backup_path).unlink() - self.logger.debug(f"Removed backup: {backup_path}") - except Exception as e: - self.logger.warning(f"Failed to remove backup {backup_path}: {e}") - - async def get_backup_stats(self) -> Dict: - """Get statistics about backups.""" - try: - backups = await self.list_backups() - - if not backups: - return { - "success": True, - "total_backups": 0, - "total_size_mb": 0, - "oldest_backup": None, - "newest_backup": None, - "by_type": {} - } - - total_size = sum(b["size_mb"] for b in backups) - oldest = min(backups, key=lambda x: x["created_at"]) - newest = max(backups, key=lambda x: x["created_at"]) - - # Group by type - by_type = {} - for backup in backups: - backup_type = backup["type"] - if backup_type not in by_type: - by_type[backup_type] = {"count": 0, "size_mb": 0} - by_type[backup_type]["count"] += 1 - by_type[backup_type]["size_mb"] += backup["size_mb"] - - return { - "success": True, - "total_backups": len(backups), - "total_size_mb": round(total_size, 1), - "oldest_backup": oldest["created_at"], - "newest_backup": newest["created_at"], - "by_type": by_type - } - - except Exception as e: - self.logger.error(f"Failed to get backup stats: {str(e)}") - return {"success": False, "error": str(e)} - - async def export_database_structure(self) -> Dict: - """Export database schema for documentation/analysis.""" - try: - structure = { - "export_time": datetime.now().isoformat(), - "database_path": self.db_path, - "tables": {} - } - - async with aiosqlite.connect(self.db_path) as db: - # Get all tables - cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table'") - tables = await cursor.fetchall() - - for table_name in [t[0] for t in tables]: - # Get table info - cursor = await db.execute(f"PRAGMA table_info({table_name})") - columns = await cursor.fetchall() - - # Get row count - cursor = await db.execute(f"SELECT COUNT(*) FROM {table_name}") - row_count = (await cursor.fetchone())[0] - - structure["tables"][table_name] = { - "columns": [ - { - "name": col[1], - "type": col[2], - "not_null": bool(col[3]), - "default": col[4], - "primary_key": bool(col[5]) - } - for col in columns - ], - "row_count": row_count - } - - # Save structure to file - structure_path = self.backup_dir / f"database_structure_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - with open(structure_path, 'w') as f: - json.dump(structure, f, indent=2, default=str) - - return { - "success": True, - "structure_path": str(structure_path), - "table_count": len(structure["tables"]), - "total_rows": sum(table["row_count"] for table in structure["tables"].values()) - } - - except Exception as e: - self.logger.error(f"Failed to export database structure: {str(e)}") - return {"success": False, "error": str(e)} - - -# Scheduler for automated backups -class BackupScheduler: - def __init__(self, backup_manager: BackupManager): - self.backup_manager = backup_manager - self.logger = logging.getLogger(__name__) - self.running = False - - async def start_scheduler(self): - """Start the backup scheduler.""" - self.running = True - self.logger.info("Backup scheduler started") - - while self.running: - try: - await self._check_and_create_backups() - await asyncio.sleep(3600) # Check every hour - - except Exception as e: - self.logger.error(f"Scheduler error: {str(e)}") - await asyncio.sleep(3600) # Wait before retrying - - def stop_scheduler(self): - """Stop the backup scheduler.""" - self.running = False - self.logger.info("Backup scheduler stopped") - - async def _check_and_create_backups(self): - """Check if backups are needed and create them.""" - now = datetime.now() - - # Check daily backup (every 24 hours) - if await self._should_create_backup("daily", hours=24): - result = await self.backup_manager.create_backup("daily", compress=True) - if result["success"]: - self.logger.info(f"Daily backup created: {result['backup_filename']}") - - # Check weekly backup (every 7 days) - if await self._should_create_backup("weekly", days=7): - result = await self.backup_manager.create_backup("weekly", compress=True) - if result["success"]: - self.logger.info(f"Weekly backup created: {result['backup_filename']}") - - # Check monthly backup (every 30 days) - if await self._should_create_backup("monthly", days=30): - result = await self.backup_manager.create_backup("monthly", compress=True) - if result["success"]: - self.logger.info(f"Monthly backup created: {result['backup_filename']}") - - # Cleanup old backups - await self.backup_manager.cleanup_old_backups() - - async def _should_create_backup(self, backup_type: str, hours: int = 0, days: int = 0) -> bool: - """Check if a backup of the specified type should be created.""" - try: - backups = await self.backup_manager.list_backups() - - # Find most recent backup of this type - type_backups = [b for b in backups if b["type"] == backup_type] - - if not type_backups: - return True # No backups of this type exist - - most_recent = max(type_backups, key=lambda x: x["created_at"]) - time_since = datetime.now() - most_recent["created_at"] - - required_delta = timedelta(hours=hours, days=days) - - return time_since >= required_delta - - except Exception as e: - self.logger.error(f"Error checking backup schedule: {str(e)}") - return False \ No newline at end of file diff --git a/src/bot.py b/src/bot.py index c2575c9..f69e25e 100644 --- a/src/bot.py +++ b/src/bot.py @@ -72,14 +72,119 @@ class PetBot: await self.handle_command(connection, nickname, nickname, message) async def handle_command(self, connection, target, nickname, message): - # Command handling is now done by the modular system in run_bot_with_reconnect.py - # This method is kept for backward compatibility but does nothing - pass + command_parts = message[1:].split() + if not command_parts: + return + + command = command_parts[0].lower() + args = command_parts[1:] + + try: + if command == "help": + await self.cmd_help(connection, target, nickname) + elif command == "start": + await self.cmd_start(connection, target, nickname) + elif command == "catch": + await self.cmd_catch(connection, target, nickname, args) + elif command == "team": + await self.cmd_team(connection, target, nickname) + elif command == "wild": + await self.cmd_wild(connection, target, nickname, args) + elif command == "battle": + await self.cmd_battle(connection, target, nickname, args) + elif command == "stats": + await self.cmd_stats(connection, target, nickname, args) + else: + await self.send_message(connection, target, f"{nickname}: Unknown command. Use !help for available commands.") + except Exception as e: + await self.send_message(connection, target, f"{nickname}: Error processing command: {str(e)}") async def send_message(self, connection, target, message): connection.privmsg(target, message) await asyncio.sleep(0.5) + async def cmd_help(self, connection, target, nickname): + help_text = [ + "Available commands:", + "!start - Begin your pet journey", + "!catch <location> - Try to catch a pet in a location", + "!team - View your active pets", + "!wild <location> - See what pets are in an area", + "!battle <player> - Challenge another player", + "!stats [pet_name] - View pet or player stats" + ] + for line in help_text: + await self.send_message(connection, target, line) + + async def cmd_start(self, connection, target, nickname): + player = await self.database.get_player(nickname) + if player: + await self.send_message(connection, target, f"{nickname}: You already have an account! Use !team to see your pets.") + return + + player_id = await self.database.create_player(nickname) + starter_pet = await self.game_engine.give_starter_pet(player_id) + + await self.send_message(connection, target, + f"{nickname}: Welcome to the world of pets! You received a {starter_pet['species_name']}!") + + async def cmd_catch(self, connection, target, nickname, args): + if not args: + await self.send_message(connection, target, f"{nickname}: Specify a location to catch pets in!") + return + + location_name = " ".join(args) + player = await self.database.get_player(nickname) + if not player: + await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!") + return + + result = await self.game_engine.attempt_catch(player["id"], location_name) + await self.send_message(connection, target, f"{nickname}: {result}") + + async def cmd_team(self, connection, target, nickname): + player = await self.database.get_player(nickname) + if not player: + await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!") + return + + pets = await self.database.get_player_pets(player["id"], active_only=True) + if not pets: + await self.send_message(connection, target, f"{nickname}: You don't have any active pets! Use !catch to find some.") + return + + team_info = [] + for pet in pets: + name = pet["nickname"] or pet["species_name"] + team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP") + + await self.send_message(connection, target, f"{nickname}'s team: " + " | ".join(team_info)) + + async def cmd_wild(self, connection, target, nickname, args): + if not args: + await self.send_message(connection, target, f"{nickname}: Specify a location to explore!") + return + + location_name = " ".join(args) + wild_pets = await self.game_engine.get_location_spawns(location_name) + + if wild_pets: + pet_list = ", ".join([pet["name"] for pet in wild_pets]) + await self.send_message(connection, target, f"Wild pets in {location_name}: {pet_list}") + else: + await self.send_message(connection, target, f"{nickname}: No location found called '{location_name}'") + + async def cmd_battle(self, connection, target, nickname, args): + await self.send_message(connection, target, f"{nickname}: Battle system coming soon!") + + async def cmd_stats(self, connection, target, nickname, args): + player = await self.database.get_player(nickname) + if not player: + await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!") + return + + await self.send_message(connection, target, + f"{nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}") if __name__ == "__main__": bot = PetBot() diff --git a/src/database.py b/src/database.py index ed65c28..aaee089 100644 --- a/src/database.py +++ b/src/database.py @@ -1,13 +1,11 @@ import aiosqlite import json -import logging from typing import Dict, List, Optional, Tuple from datetime import datetime class Database: def __init__(self, db_path: str = "data/petbot.db"): self.db_path = db_path - self.logger = logging.getLogger(__name__) async def init_database(self): async with aiosqlite.connect(self.db_path) as db: @@ -38,56 +36,10 @@ class Database: evolution_level INTEGER, evolution_species_id INTEGER, rarity INTEGER DEFAULT 1, - emoji TEXT, FOREIGN KEY (evolution_species_id) REFERENCES pet_species (id) ) """) - # Add emoji column if it doesn't exist (migration) - try: - await db.execute("ALTER TABLE pet_species ADD COLUMN emoji TEXT") - await db.commit() - except Exception: - # Column already exists or other error, which is fine - pass - - # Add IV columns to pets table (migration) - iv_columns = [ - "ALTER TABLE pets ADD COLUMN iv_hp INTEGER DEFAULT 15", - "ALTER TABLE pets ADD COLUMN iv_attack INTEGER DEFAULT 15", - "ALTER TABLE pets ADD COLUMN iv_defense INTEGER DEFAULT 15", - "ALTER TABLE pets ADD COLUMN iv_speed INTEGER DEFAULT 15" - ] - - for column_sql in iv_columns: - try: - await db.execute(column_sql) - await db.commit() - except Exception: - # Column already exists or other error, which is fine - pass - - # Add future-ready columns for breeding and personality system (migration) - future_columns = [ - "ALTER TABLE pets ADD COLUMN nature TEXT DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN personality_value INTEGER DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN original_trainer_id INTEGER DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN parent1_id INTEGER DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN parent2_id INTEGER DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN generation INTEGER DEFAULT 1", - "ALTER TABLE pets ADD COLUMN is_shiny BOOLEAN DEFAULT FALSE", - "ALTER TABLE pets ADD COLUMN gender TEXT DEFAULT NULL", - "ALTER TABLE pets ADD COLUMN ability TEXT DEFAULT NULL" - ] - - for column_sql in future_columns: - try: - await db.execute(column_sql) - await db.commit() - except Exception: - # Column already exists or other error, which is fine - pass - await db.execute(""" CREATE TABLE IF NOT EXISTS pets ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -104,26 +56,8 @@ class Database: happiness INTEGER DEFAULT 50, caught_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT FALSE, - -- Individual Values (IVs) for each stat (0-31) - iv_hp INTEGER DEFAULT 15, - iv_attack INTEGER DEFAULT 15, - iv_defense INTEGER DEFAULT 15, - iv_speed INTEGER DEFAULT 15, - -- Future-ready columns for breeding and personality system - nature TEXT DEFAULT NULL, - personality_value INTEGER DEFAULT NULL, - original_trainer_id INTEGER DEFAULT NULL, - parent1_id INTEGER DEFAULT NULL, - parent2_id INTEGER DEFAULT NULL, - generation INTEGER DEFAULT 1, - is_shiny BOOLEAN DEFAULT FALSE, - gender TEXT DEFAULT NULL, - ability TEXT DEFAULT NULL, FOREIGN KEY (player_id) REFERENCES players (id), - FOREIGN KEY (species_id) REFERENCES pet_species (id), - FOREIGN KEY (original_trainer_id) REFERENCES players (id), - FOREIGN KEY (parent1_id) REFERENCES pets (id), - FOREIGN KEY (parent2_id) REFERENCES pets (id) + FOREIGN KEY (species_id) REFERENCES pet_species (id) ) """) @@ -182,63 +116,7 @@ class Database: try: await db.execute("ALTER TABLE players ADD COLUMN current_location_id INTEGER DEFAULT 1") await db.commit() - self.logger.info("Added current_location_id column to players table") - except: - pass # Column already exists - - # Add team_order column if it doesn't exist - try: - await db.execute("ALTER TABLE pets ADD COLUMN team_order INTEGER DEFAULT NULL") - await db.commit() - self.logger.info("Added team_order column to pets table") - except: - pass # Column already exists - - # Migrate existing active pets to have team_order values - try: - # Find active pets without team_order - cursor = await db.execute(""" - SELECT id, player_id FROM pets - WHERE is_active = TRUE AND team_order IS NULL - ORDER BY player_id, id - """) - pets_to_migrate = await cursor.fetchall() - - if pets_to_migrate: - self.logger.info(f"Migrating {len(pets_to_migrate)} active pets to have team_order values...") - - # Group pets by player - from collections import defaultdict - pets_by_player = defaultdict(list) - for pet in pets_to_migrate: - pets_by_player[pet[1]].append(pet[0]) - - # Assign team_order values for each player - for player_id, pet_ids in pets_by_player.items(): - for i, pet_id in enumerate(pet_ids[:6]): # Max 6 pets per team - await db.execute(""" - UPDATE pets SET team_order = ? WHERE id = ? - """, (i + 1, pet_id)) - - await db.commit() - self.logger.info("Migration completed successfully") - except Exception as e: - self.logger.warning(f"Migration warning: {e}") - pass # Don't fail if migration has issues - - # Add fainted_at column for tracking when pets faint - try: - await db.execute("ALTER TABLE pets ADD COLUMN fainted_at TIMESTAMP DEFAULT NULL") - await db.commit() - print("Added fainted_at column to pets table") - except: - pass # Column already exists - - # Add last_heal_time column for heal command cooldown - try: - await db.execute("ALTER TABLE players ADD COLUMN last_heal_time TIMESTAMP DEFAULT NULL") - await db.commit() - print("Added last_heal_time column to players table") + print("Added current_location_id column to players table") except: pass # Column already exists @@ -335,245 +213,6 @@ class Database: ) """) - await db.execute(""" - CREATE TABLE IF NOT EXISTS gyms ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - location_id INTEGER NOT NULL, - name TEXT UNIQUE NOT NULL, - leader_name TEXT NOT NULL, - description TEXT, - theme TEXT NOT NULL, - badge_name TEXT NOT NULL, - badge_icon TEXT NOT NULL, - badge_description TEXT, - FOREIGN KEY (location_id) REFERENCES locations (id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS gym_teams ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - gym_id INTEGER NOT NULL, - species_id INTEGER NOT NULL, - base_level INTEGER NOT NULL, - move_set TEXT, - team_position INTEGER NOT NULL, - FOREIGN KEY (gym_id) REFERENCES gyms (id), - FOREIGN KEY (species_id) REFERENCES pet_species (id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS player_gym_battles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL, - gym_id INTEGER NOT NULL, - victories INTEGER DEFAULT 0, - highest_difficulty INTEGER DEFAULT 0, - first_victory_date TIMESTAMP, - last_battle_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (player_id) REFERENCES players (id), - FOREIGN KEY (gym_id) REFERENCES gyms (id), - UNIQUE(player_id, gym_id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS active_gym_battles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL, - gym_id INTEGER NOT NULL, - difficulty_level INTEGER NOT NULL, - current_pet_index INTEGER DEFAULT 0, - gym_team_data TEXT NOT NULL, - battle_status TEXT DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (player_id) REFERENCES players (id), - FOREIGN KEY (gym_id) REFERENCES gyms (id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS player_encounters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL, - species_id INTEGER NOT NULL, - first_encounter_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - total_encounters INTEGER DEFAULT 1, - last_encounter_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - caught_count INTEGER DEFAULT 0, - FOREIGN KEY (player_id) REFERENCES players (id), - FOREIGN KEY (species_id) REFERENCES pet_species (id), - UNIQUE(player_id, species_id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS pending_team_changes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL, - new_team_data TEXT NOT NULL, - verification_pin TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - verified_at TIMESTAMP, - FOREIGN KEY (player_id) REFERENCES players (id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS pet_moves ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pet_id INTEGER NOT NULL, - move_name TEXT NOT NULL, - power_iv INTEGER DEFAULT 0, - accuracy_iv INTEGER DEFAULT 0, - pp_iv INTEGER DEFAULT 0, - learned_at_level INTEGER DEFAULT 1, - is_signature BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (pet_id) REFERENCES pets (id) ON DELETE CASCADE - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS npc_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_type TEXT NOT NULL, - title TEXT NOT NULL, - description TEXT NOT NULL, - difficulty INTEGER DEFAULT 1, - target_contributions INTEGER NOT NULL, - current_contributions INTEGER DEFAULT 0, - reward_experience INTEGER DEFAULT 0, - reward_money INTEGER DEFAULT 0, - reward_items TEXT, - status TEXT DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - completion_message TEXT - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS npc_event_contributions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_id INTEGER NOT NULL, - player_id INTEGER NOT NULL, - contributions INTEGER DEFAULT 0, - last_contribution_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (event_id) REFERENCES npc_events (id), - FOREIGN KEY (player_id) REFERENCES players (id), - UNIQUE(event_id, player_id) - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS verification_pins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL, - pin_code TEXT NOT NULL, - request_type TEXT NOT NULL, - request_data TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - is_used BOOLEAN DEFAULT FALSE, - used_at TIMESTAMP, - FOREIGN KEY (player_id) REFERENCES players (id) - ) - """) - - 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 active_teams table for tracking which team configuration is active - await db.execute(""" - CREATE TABLE IF NOT EXISTS active_teams ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - player_id INTEGER NOT NULL UNIQUE, - active_slot INTEGER NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (player_id) REFERENCES players (id), - FOREIGN KEY (active_slot) REFERENCES team_configurations (slot_number) - ) - """) - - # Create species_moves table for move learning system - await db.execute(""" - CREATE TABLE IF NOT EXISTS species_moves ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - species_id INTEGER NOT NULL, - move_id INTEGER NOT NULL, - learn_level INTEGER NOT NULL, - learn_method TEXT DEFAULT 'level', - FOREIGN KEY (species_id) REFERENCES pet_species (id), - FOREIGN KEY (move_id) REFERENCES moves (id), - UNIQUE(species_id, move_id, learn_level) - ) - """) - - # Create location type compatibility table - await db.execute(""" - CREATE TABLE IF NOT EXISTS location_type_compatibility ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - location_id INTEGER NOT NULL, - pet_type TEXT NOT NULL, - compatibility_modifier REAL DEFAULT 1.0, - FOREIGN KEY (location_id) REFERENCES locations (id), - UNIQUE(location_id, pet_type) - ) - """) - - # Migration: Add columns for pet rename functionality - try: - await db.execute("ALTER TABLE pending_team_changes ADD COLUMN action_type TEXT DEFAULT 'team_change'") - await db.execute("ALTER TABLE pending_team_changes ADD COLUMN pet_id INTEGER") - await db.execute("ALTER TABLE pending_team_changes ADD COLUMN new_nickname TEXT") - await db.commit() - print("Added pet rename columns to pending_team_changes table") - except Exception: - # Columns already exist or other error, which is fine - pass - - # Create indexes for performance optimization - await db.execute("CREATE INDEX IF NOT EXISTS idx_pets_player_active ON pets (player_id, is_active)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_pets_species ON pets (species_id)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_player_encounters_player ON player_encounters (player_id)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_player_encounters_species ON player_encounters (species_id)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_location_spawns_location ON location_spawns (location_id)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_species_moves_species ON species_moves (species_id)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_species_moves_level ON species_moves (learn_level)") - await db.execute("CREATE INDEX IF NOT EXISTS idx_active_teams_player ON active_teams (player_id)") - - # Add team size validation trigger - await db.execute(""" - CREATE TRIGGER IF NOT EXISTS validate_team_size_on_update - AFTER UPDATE OF is_active ON pets - WHEN NEW.is_active != OLD.is_active - BEGIN - UPDATE pets SET is_active = OLD.is_active - WHERE id = NEW.id AND ( - SELECT COUNT(*) FROM pets - WHERE player_id = NEW.player_id AND is_active = TRUE - ) < 1; - END - """) - - # Enable foreign key constraints - await db.execute("PRAGMA foreign_keys = ON") - await db.commit() async def get_player(self, nickname: str) -> Optional[Dict]: @@ -585,33 +224,6 @@ class Database: row = await cursor.fetchone() return dict(row) if row else None - async def update_player_admin(self, player_id: int, updates: Dict) -> bool: - """Update player data for admin purposes""" - try: - if not updates: - return False - - # Build dynamic SQL query for provided fields - set_clauses = [] - values = [] - - for field, value in updates.items(): - set_clauses.append(f"{field} = ?") - values.append(value) - - values.append(player_id) # Add player_id for WHERE clause - - sql = f"UPDATE players SET {', '.join(set_clauses)} WHERE id = ?" - - async with aiosqlite.connect(self.db_path) as db: - await db.execute(sql, values) - await db.commit() - return True - - except Exception as e: - self.logger.error(f"Error updating player admin data: {e}") - return False - async def create_player(self, nickname: str) -> int: async with aiosqlite.connect(self.db_path) as db: # Get Starter Town ID @@ -631,35 +243,20 @@ class Database: async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row - - if active_only: - # Use the new active team system - return await self.get_active_pets(player_id) - else: - # Return all pets (existing behavior for storage pets) - query = """ - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - ORDER BY p.id ASC - """ - cursor = await db.execute(query, (player_id,)) - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_pet_by_id(self, pet_id: int) -> Optional[Dict]: - """Get a specific pet by ID with full details""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji + query = """ + SELECT p.*, ps.name as species_name, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? - """, (pet_id,)) - row = await cursor.fetchone() - return dict(row) if row else None + WHERE p.player_id = ? + """ + params = [player_id] + + if active_only: + query += " AND p.is_active = TRUE" + + cursor = await db.execute(query, params) + rows = await cursor.fetchall() + return [dict(row) for row in rows] async def get_player_location(self, player_id: int) -> Optional[Dict]: async with aiosqlite.connect(self.db_path) as db: @@ -689,24 +286,6 @@ class Database: row = await cursor.fetchone() return dict(row) if row else None - async def get_location_by_id(self, location_id: int) -> Optional[Dict]: - """Get location by ID""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute( - "SELECT * FROM locations WHERE id = ?", (location_id,) - ) - row = await cursor.fetchone() - return dict(row) if row else None - - async def get_all_locations(self) -> List[Dict]: - """Get all locations""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute("SELECT * FROM locations ORDER BY id") - rows = await cursor.fetchall() - return [dict(row) for row in rows] - async def check_player_achievements(self, player_id: int, achievement_type: str, data: str): """Check and award achievements based on player actions""" async with aiosqlite.connect(self.db_path) as db: @@ -774,19 +353,6 @@ class Database: return False - async def check_all_achievements(self, player_id: int) -> List[Dict]: - """Check and award ALL possible achievements for a player""" - all_new_achievements = [] - - # Check all achievement types that might be available - achievement_types = ["catch_type", "catch_total", "explore_count"] - - for achievement_type in achievement_types: - new_achievements = await self.check_player_achievements(player_id, achievement_type, "") - all_new_achievements.extend(new_achievements) - - return all_new_achievements - async def get_player_achievements(self, player_id: int) -> List[Dict]: """Get all achievements earned by player""" async with aiosqlite.connect(self.db_path) as db: @@ -897,16 +463,11 @@ class Database: if not pet: return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"} - # Get next available team slot - next_slot = await self.get_next_available_team_slot(player_id) - if next_slot is None: - return {"success": False, "error": "Team is full (maximum 6 pets)"} - - # Activate the pet and assign team position - await db.execute("UPDATE pets SET is_active = TRUE, team_order = ? WHERE id = ?", (next_slot, pet["id"])) + # Activate the pet + await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],)) await db.commit() - return {"success": True, "pet": dict(pet), "team_position": next_slot} + return {"success": True, "pet": dict(pet)} async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict: """Deactivate a pet by name or species name. Returns result dict.""" @@ -934,129 +495,65 @@ class Database: if active_count["count"] <= 1: return {"success": False, "error": "You must have at least one active pet!"} - # Deactivate the pet and clear team order - await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],)) + # Deactivate the pet + await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],)) await db.commit() return {"success": True, "pet": dict(pet)} - # Team Order Methods - async def get_next_available_team_slot(self, player_id: int) -> int: - """Get the next available team slot (1-6)""" + async def swap_pets(self, player_id: int, pet1_identifier: str, pet2_identifier: str) -> Dict: + """Swap the active status of two pets. Returns result dict.""" async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + # Find both pets cursor = await db.execute(""" - SELECT team_order FROM pets - WHERE player_id = ? AND is_active = TRUE AND team_order IS NOT NULL - ORDER BY team_order ASC - """, (player_id,)) - used_slots = [row[0] for row in await cursor.fetchall()] + SELECT p.*, ps.name as species_name + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + AND (p.nickname = ? OR ps.name = ?) + LIMIT 1 + """, (player_id, pet1_identifier, pet1_identifier)) + pet1 = await cursor.fetchone() - # Find first available slot (1-6) - for slot in range(1, 7): - if slot not in used_slots: - return slot - return None # Team is full - - async def set_pet_team_order(self, player_id: int, pet_id: int, position: int) -> Dict: - """Set a pet's team order position (1-6)""" - if position < 1 or position > 6: - return {"success": False, "error": "Team position must be between 1-6"} - - async with aiosqlite.connect(self.db_path) as db: - # Check if pet belongs to player - cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": "Pet not found"} - - # Check if position is already taken cursor = await db.execute(""" - SELECT id FROM pets - WHERE player_id = ? AND team_order = ? AND is_active = TRUE AND id != ? - """, (player_id, position, pet_id)) - existing_pet = await cursor.fetchone() + SELECT p.*, ps.name as species_name + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + AND (p.nickname = ? OR ps.name = ?) + LIMIT 1 + """, (player_id, pet2_identifier, pet2_identifier)) + pet2 = await cursor.fetchone() - if existing_pet: - return {"success": False, "error": f"Position {position} is already taken"} + if not pet1: + return {"success": False, "error": f"Pet '{pet1_identifier}' not found"} + if not pet2: + return {"success": False, "error": f"Pet '{pet2_identifier}' not found"} - # Update pet's team order and make it active - await db.execute(""" - UPDATE pets SET team_order = ?, is_active = TRUE - WHERE id = ? AND player_id = ? - """, (position, pet_id, player_id)) + if pet1["id"] == pet2["id"]: + return {"success": False, "error": "Cannot swap a pet with itself"} + # Swap their active status + await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet1["is_active"], pet1["id"])) + await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet2["is_active"], pet2["id"])) await db.commit() - return {"success": True, "position": position} - - async def reorder_team_positions(self, player_id: int, new_positions: List[Dict]) -> Dict: - """Reorder team positions based on new arrangement""" - async with aiosqlite.connect(self.db_path) as db: - try: - # Validate all positions are 1-6 and no duplicates - positions = [pos["position"] for pos in new_positions] - if len(set(positions)) != len(positions): - return {"success": False, "error": "Duplicate positions detected"} - - for pos_data in new_positions: - position = pos_data["position"] - pet_id = pos_data["pet_id"] - - if position < 1 or position > 6: - return {"success": False, "error": f"Invalid position {position}"} - - # Verify pet belongs to player - cursor = await db.execute("SELECT id FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - if not await cursor.fetchone(): - return {"success": False, "error": f"Pet {pet_id} not found"} - - # Clear all team orders first - await db.execute("UPDATE pets SET team_order = NULL WHERE player_id = ?", (player_id,)) - - # Set new positions - for pos_data in new_positions: - await db.execute(""" - UPDATE pets SET team_order = ?, is_active = TRUE - WHERE id = ? AND player_id = ? - """, (pos_data["position"], pos_data["pet_id"], player_id)) - - await db.commit() - return {"success": True, "message": "Team order updated successfully"} - - except Exception as e: - await db.rollback() - return {"success": False, "error": str(e)} - - async def remove_from_team_position(self, player_id: int, pet_id: int) -> Dict: - """Remove a pet from team (set to inactive and clear team_order)""" - async with aiosqlite.connect(self.db_path) as db: - # Check if pet belongs to player - cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id)) - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": "Pet not found"} - # Check if this is the only active pet - cursor = await db.execute("SELECT COUNT(*) as count FROM pets WHERE player_id = ? AND is_active = TRUE", (player_id,)) - active_count = await cursor.fetchone() - - if active_count["count"] <= 1: - return {"success": False, "error": "Cannot deactivate your only active pet"} - - # Remove from team - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE id = ? AND player_id = ? - """, (pet_id, player_id)) - - await db.commit() - return {"success": True, "message": "Pet removed from team"} + return { + "success": True, + "pet1": dict(pet1), + "pet2": dict(pet2), + "pet1_now": "active" if not pet1["is_active"] else "storage", + "pet2_now": "active" if not pet2["is_active"] else "storage" + } # Item and Inventory Methods async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool: """Add an item to player's inventory""" async with aiosqlite.connect(self.db_path) as db: # Get item ID - cursor = await db.execute("SELECT id FROM items WHERE LOWER(name) = LOWER(?)", (item_name,)) + cursor = await db.execute("SELECT id FROM items WHERE name = ?", (item_name,)) item = await cursor.fetchone() if not item: return False @@ -1111,7 +608,7 @@ class Database: SELECT i.*, pi.quantity FROM items i JOIN player_inventory pi ON i.id = pi.item_id - WHERE pi.player_id = ? AND LOWER(i.name) = LOWER(?) + WHERE pi.player_id = ? AND i.name = ? """, (player_id, item_name)) item = await cursor.fetchone() @@ -1189,2294 +686,4 @@ class Database: async with aiosqlite.connect(self.db_path) as db: await db.execute("UPDATE pets SET hp = ? WHERE id = ?", (new_hp, pet_id)) await db.commit() - return True - - async def get_active_pets(self, player_id: int) -> List[Dict]: - """Get all active pets for a player using new active_teams system""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Get the current active team slot - cursor = await db.execute(""" - SELECT active_slot FROM active_teams WHERE player_id = ? - """, (player_id,)) - active_team_row = await cursor.fetchone() - - if not active_team_row: - return [] # No active team set - - active_slot = active_team_row[0] - - # Get the team configuration for the active slot - cursor = await db.execute(""" - SELECT team_data FROM team_configurations - WHERE player_id = ? AND slot_number = ? - """, (player_id, active_slot)) - config_row = await cursor.fetchone() - - if not config_row or not config_row[0]: - return [] # No team configuration or empty team - - # Parse the team data - import json - try: - team_data = json.loads(config_row[0]) if isinstance(config_row[0], str) else config_row[0] - except (json.JSONDecodeError, TypeError): - return [] - - # Get full pet details for each pet in the team - active_pets = [] - for pet_data in team_data: - if pet_data and 'id' in pet_data: - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, - ps.base_hp, ps.base_attack, ps.base_defense, ps.base_speed - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? - """, (pet_data['id'], player_id)) - pet_row = await cursor.fetchone() - if pet_row: - pet_dict = dict(pet_row) - # Add team_order from the saved configuration - pet_dict['team_order'] = pet_data.get('team_order', len(active_pets) + 1) - active_pets.append(pet_dict) - - # Sort by team_order - active_pets.sort(key=lambda x: x.get('team_order', 999)) - return active_pets - - def calculate_exp_for_level(self, level: int) -> int: - """Calculate total experience needed to reach a level""" - # Input validation - if not isinstance(level, int) or level < 1: - return 0 - - # Cap at level 100 to prevent overflow - level = min(level, 100) - - # Using a cubic growth formula: level^3 * 4 - 12 - return max(0, (level ** 3) * 4 - 12) - - def calculate_level_from_exp(self, exp: int) -> int: - """Calculate what level a pet should be based on experience""" - # Input validation - if not isinstance(exp, int) or exp < 0: - return 1 - - level = 1 - # Prevent infinite loop with reasonable upper bound - max_iterations = 100 - iterations = 0 - - while iterations < max_iterations and self.calculate_exp_for_level(level + 1) <= exp: - level += 1 - iterations += 1 - - return min(level, 100) # Cap at level 100 - - async def award_experience(self, pet_id: int, exp_amount: int) -> Dict: - """Award experience to a pet and handle leveling up with proper transaction isolation""" - # Input validation - if not isinstance(pet_id, int) or pet_id <= 0: - return {"success": False, "error": "Invalid pet ID"} - - if not isinstance(exp_amount, int): - return {"success": False, "error": "Experience amount must be an integer"} - - if exp_amount < 0: - return {"success": False, "error": "Cannot award negative experience"} - - if exp_amount > 10000: # Reasonable cap to prevent abuse - return {"success": False, "error": "Experience amount too large"} - - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - try: - # Start immediate transaction for isolation - await db.execute("BEGIN IMMEDIATE") - - # Get current pet data with row-level lock - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.base_hp, ps.base_attack, - ps.base_defense, ps.base_speed - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? - """, (pet_id,)) - - pet = await cursor.fetchone() - if not pet: - await db.execute("ROLLBACK") - return {"success": False, "error": "Pet not found"} - - pet_dict = dict(pet) - old_level = pet_dict["level"] - old_exp = pet_dict["experience"] - old_hp = pet_dict["hp"] - old_max_hp = pet_dict["max_hp"] - - # Calculate new experience and level - new_exp = old_exp + exp_amount - new_level = self.calculate_level_from_exp(new_exp) - - # Prepare result data - result = { - "success": True, - "pet_id": pet_id, - "pet_name": pet_dict["nickname"] or pet_dict["species_name"], - "species_name": pet_dict["species_name"], - "old_level": old_level, - "new_level": new_level, - "old_exp": old_exp, - "new_exp": new_exp, - "exp_gained": exp_amount, - "leveled_up": new_level > old_level, - "levels_gained": new_level - old_level - } - - # Single atomic update - no race conditions - if new_level > old_level: - # Pet leveled up - calculate new stats and update everything at once - new_stats = self._calculate_pet_stats(pet_dict, new_level) - - # Calculate stat increases for return data - old_stats = self._calculate_pet_stats(pet_dict, old_level) - stat_increases = { - "hp": new_stats["hp"] - old_stats["hp"], - "attack": new_stats["attack"] - old_stats["attack"], - "defense": new_stats["defense"] - old_stats["defense"], - "speed": new_stats["speed"] - old_stats["speed"] - } - result["stat_increases"] = stat_increases - - # Calculate new current HP (preserve HP percentage but ensure not below 1) - if old_max_hp > 0: - hp_percentage = old_hp / old_max_hp - new_hp = max(1, int(new_stats["hp"] * hp_percentage)) - # Give full HP bonus for leveling up - new_hp = new_stats["hp"] - else: - new_hp = new_stats["hp"] - - # Single atomic update for level up - await db.execute(""" - UPDATE pets - SET experience = ?, level = ?, max_hp = ?, hp = ?, - attack = ?, defense = ?, speed = ? - WHERE id = ? - """, ( - new_exp, new_level, new_stats["hp"], new_hp, - new_stats["attack"], new_stats["defense"], new_stats["speed"], - pet_id - )) - else: - # No level up - just update experience - await db.execute(""" - UPDATE pets SET experience = ? WHERE id = ? - """, (new_exp, pet_id)) - - # Commit the transaction - await db.commit() - return result - - except Exception as e: - # Rollback on any error - await db.execute("ROLLBACK") - self.logger.error(f"Error in award_experience: {e}") - return {"success": False, "error": f"Database error: {str(e)}"} - - # NOTE: _handle_level_up function removed - now handled atomically in award_experience() - - def _calculate_pet_stats(self, pet_dict: Dict, level: int) -> Dict: - """Calculate pet stats for a given level using stored IVs""" - # Use stored IVs, with fallback to defaults for existing pets - iv_hp = pet_dict.get("iv_hp", 15) - iv_attack = pet_dict.get("iv_attack", 15) - iv_defense = pet_dict.get("iv_defense", 15) - iv_speed = pet_dict.get("iv_speed", 15) - - # Pokemon-style stat calculation with individual IVs - hp = int((2 * pet_dict["base_hp"] + iv_hp) * level / 100) + level + 10 - attack = int((2 * pet_dict["base_attack"] + iv_attack) * level / 100) + 5 - defense = int((2 * pet_dict["base_defense"] + iv_defense) * level / 100) + 5 - speed = int((2 * pet_dict["base_speed"] + iv_speed) * level / 100) + 5 - - return { - "hp": hp, - "attack": attack, - "defense": defense, - "speed": speed - } - - async def _calculate_stat_increases(self, pet_dict: Dict, old_level: int, new_level: int) -> Dict: - """Calculate stat increases from leveling up""" - old_stats = self._calculate_pet_stats(pet_dict, old_level) - new_stats = self._calculate_pet_stats(pet_dict, new_level) - - return { - "hp": new_stats["hp"] - old_stats["hp"], - "attack": new_stats["attack"] - old_stats["attack"], - "defense": new_stats["defense"] - old_stats["defense"], - "speed": new_stats["speed"] - old_stats["speed"] - } - - # Gym Battle System Methods - async def get_gyms_in_location(self, location_id: int, player_id: int = None) -> List[Dict]: - """Get all gyms in a specific location with optional player progress""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - if player_id: - cursor = await db.execute(""" - SELECT g.*, l.name as location_name, - COALESCE(pgb.victories, 0) as victories, - COALESCE(pgb.highest_difficulty, 0) as highest_difficulty, - pgb.first_victory_date - FROM gyms g - JOIN locations l ON g.location_id = l.id - LEFT JOIN player_gym_battles pgb ON g.id = pgb.gym_id AND pgb.player_id = ? - WHERE g.location_id = ? - ORDER BY g.id - """, (player_id, location_id)) - else: - cursor = await db.execute(""" - SELECT g.*, l.name as location_name - FROM gyms g - JOIN locations l ON g.location_id = l.id - WHERE g.location_id = ? - ORDER BY g.id - """, (location_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_gym_by_name(self, gym_name: str) -> Optional[Dict]: - """Get gym details by name (case-insensitive)""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT g.*, l.name as location_name - FROM gyms g - JOIN locations l ON g.location_id = l.id - WHERE LOWER(g.name) = LOWER(?) - """, (gym_name,)) - row = await cursor.fetchone() - return dict(row) if row else None - - async def get_gym_by_name_in_location(self, gym_name: str, location_id: int) -> Optional[Dict]: - """Get gym details by name in a specific location (case-insensitive)""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT g.*, l.name as location_name - FROM gyms g - JOIN locations l ON g.location_id = l.id - WHERE g.location_id = ? AND LOWER(g.name) = LOWER(?) - """, (location_id, gym_name)) - row = await cursor.fetchone() - return dict(row) if row else None - - async def get_gym_team(self, gym_id: int, difficulty_multiplier: float = 1.0) -> List[Dict]: - """Get gym team with difficulty scaling applied""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT gt.*, ps.name as species_name, ps.type1, ps.type2, - ps.base_hp, ps.base_attack, ps.base_defense, ps.base_speed - FROM gym_teams gt - JOIN pet_species ps ON gt.species_id = ps.id - WHERE gt.gym_id = ? - ORDER BY gt.team_position - """, (gym_id,)) - rows = await cursor.fetchall() - - team = [] - for row in rows: - # Apply difficulty scaling - scaled_level = int(row["base_level"] * difficulty_multiplier) - stat_multiplier = 1.0 + (difficulty_multiplier - 1.0) * 0.5 # Less aggressive stat scaling - - # Calculate scaled stats - hp = int((2 * row["base_hp"] + 31) * scaled_level / 100) + scaled_level + 10 - attack = int(((2 * row["base_attack"] + 31) * scaled_level / 100) + 5) * stat_multiplier - defense = int(((2 * row["base_defense"] + 31) * scaled_level / 100) + 5) * stat_multiplier - speed = int(((2 * row["base_speed"] + 31) * scaled_level / 100) + 5) * stat_multiplier - - pet_data = { - "species_id": row["species_id"], - "species_name": row["species_name"], - "level": scaled_level, - "type1": row["type1"], - "type2": row["type2"], - "hp": int(hp), - "max_hp": int(hp), - "attack": int(attack), - "defense": int(defense), - "speed": int(speed), - "moves": row["move_set"].split(",") if row["move_set"] else ["Tackle", "Growl"], - "position": row["team_position"] - } - team.append(pet_data) - - return team - - async def get_player_gym_progress(self, player_id: int, gym_id: int) -> Optional[Dict]: - """Get player's progress for a specific gym""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT * FROM player_gym_battles - WHERE player_id = ? AND gym_id = ? - """, (player_id, gym_id)) - row = await cursor.fetchone() - return dict(row) if row else None - - async def record_gym_victory(self, player_id: int, gym_id: int) -> Dict: - """Record a gym victory and update player progress""" - async with aiosqlite.connect(self.db_path) as db: - # Get current progress - cursor = await db.execute(""" - SELECT victories, first_victory_date FROM player_gym_battles - WHERE player_id = ? AND gym_id = ? - """, (player_id, gym_id)) - current = await cursor.fetchone() - - if current: - new_victories = current[0] + 1 - await db.execute(""" - UPDATE player_gym_battles - SET victories = ?, highest_difficulty = ?, last_battle_date = CURRENT_TIMESTAMP - WHERE player_id = ? AND gym_id = ? - """, (new_victories, new_victories, player_id, gym_id)) - else: - new_victories = 1 - await db.execute(""" - INSERT INTO player_gym_battles - (player_id, gym_id, victories, highest_difficulty, first_victory_date, last_battle_date) - VALUES (?, ?, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - """, (player_id, gym_id)) - - await db.commit() - - return { - "victories": new_victories, - "is_first_victory": current is None, - "next_difficulty": new_victories + 1 - } - - async def initialize_gyms(self): - """Initialize gyms from config file""" - import json - - try: - with open("config/gyms.json", "r") as f: - gyms_data = json.load(f) - except FileNotFoundError: - print("Gyms config file not found") - return - - async with aiosqlite.connect(self.db_path) as db: - # Clear existing gym data - await db.execute("DELETE FROM gym_teams") - await db.execute("DELETE FROM gyms") - - for gym_config in gyms_data: - # Get location ID - cursor = await db.execute( - "SELECT id FROM locations WHERE name = ?", - (gym_config["location"],) - ) - location_row = await cursor.fetchone() - if not location_row: - print(f"Location '{gym_config['location']}' not found for gym '{gym_config['name']}'") - continue - - location_id = location_row[0] - - # Insert gym - cursor = await db.execute(""" - INSERT OR REPLACE INTO gyms - (location_id, name, leader_name, description, theme, badge_name, badge_icon, badge_description) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - location_id, - gym_config["name"], - gym_config["leader_name"], - gym_config["description"], - gym_config["theme"], - gym_config["badge"]["name"], - gym_config["badge"]["icon"], - gym_config["badge"]["description"] - )) - - gym_id = cursor.lastrowid - - # Insert gym team - for member in gym_config["team"]: - # Get species ID - species_cursor = await db.execute( - "SELECT id FROM pet_species WHERE name = ?", - (member["species"],) - ) - species_row = await species_cursor.fetchone() - if species_row: - moves_str = ",".join(member["moves"]) if member["moves"] else "" - await db.execute(""" - INSERT INTO gym_teams - (gym_id, species_id, base_level, move_set, team_position) - VALUES (?, ?, ?, ?, ?) - """, ( - gym_id, - species_row[0], - member["base_level"], - moves_str, - member["position"] - )) - - await db.commit() - print("Gyms initialized from config") - - async def start_gym_battle(self, player_id: int, gym_id: int, difficulty_level: int, gym_team: List[Dict]) -> int: - """Start a new gym battle""" - import json - gym_team_json = json.dumps(gym_team) - - async with aiosqlite.connect(self.db_path) as db: - # End any existing gym battle for this player - await db.execute(""" - UPDATE active_gym_battles - SET battle_status = 'ended' - WHERE player_id = ? AND battle_status = 'active' - """, (player_id,)) - - # Create new gym battle - cursor = await db.execute(""" - INSERT INTO active_gym_battles - (player_id, gym_id, difficulty_level, current_pet_index, gym_team_data) - VALUES (?, ?, ?, 0, ?) - """, (player_id, gym_id, difficulty_level, gym_team_json)) - - battle_id = cursor.lastrowid - await db.commit() - return battle_id - - async def get_active_gym_battle(self, player_id: int) -> Optional[Dict]: - """Get player's active gym battle""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT agb.*, g.name as gym_name, g.leader_name, g.badge_icon - FROM active_gym_battles agb - JOIN gyms g ON agb.gym_id = g.id - WHERE agb.player_id = ? AND agb.battle_status = 'active' - ORDER BY agb.id DESC LIMIT 1 - """, (player_id,)) - - row = await cursor.fetchone() - if row: - battle_data = dict(row) - # Parse gym team data - import json - battle_data["gym_team"] = json.loads(battle_data["gym_team_data"]) - return battle_data - return None - - async def advance_gym_battle(self, player_id: int) -> bool: - """Advance to next pet in gym battle""" - async with aiosqlite.connect(self.db_path) as db: - # Get current battle - cursor = await db.execute(""" - SELECT current_pet_index, gym_team_data - FROM active_gym_battles - WHERE player_id = ? AND battle_status = 'active' - """, (player_id,)) - - battle = await cursor.fetchone() - if not battle: - return False - - import json - gym_team = json.loads(battle[1]) - current_index = battle[0] - - # Check if there are more pets - if current_index + 1 >= len(gym_team): - return False # No more pets - - # Advance to next pet - await db.execute(""" - UPDATE active_gym_battles - SET current_pet_index = current_pet_index + 1 - WHERE player_id = ? AND battle_status = 'active' - """, (player_id,)) - - await db.commit() - return True - - async def end_gym_battle(self, player_id: int, victory: bool = False) -> Optional[Dict]: - """End gym battle and return final status""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - # Get battle info before ending it - cursor = await db.execute(""" - SELECT agb.gym_id, agb.difficulty_level, g.name as gym_name - FROM active_gym_battles agb - JOIN gyms g ON agb.gym_id = g.id - WHERE agb.player_id = ? AND agb.battle_status = 'active' - """, (player_id,)) - - battle = await cursor.fetchone() - if not battle: - return None - - battle_dict = dict(battle) - - # End the battle - await db.execute(""" - UPDATE active_gym_battles - SET battle_status = 'completed' - WHERE player_id = ? AND battle_status = 'active' - """, (player_id,)) - - result = { - "gym_id": battle_dict["gym_id"], - "gym_name": battle_dict["gym_name"], - "difficulty_level": battle_dict["difficulty_level"], - "victory": victory - } - - # Record victory if successful - if victory: - await self.record_gym_victory(player_id, battle_dict["gym_id"]) - - await db.commit() - return result - - async def record_encounter(self, player_id: int, species_name: str, was_caught: bool = False) -> bool: - """Record a player encountering a pet species""" - try: - async with aiosqlite.connect(self.db_path) as db: - # Get species ID - cursor = await db.execute( - "SELECT id FROM pet_species WHERE name = ?", (species_name,) - ) - species_row = await cursor.fetchone() - if not species_row: - return False - - species_id = species_row[0] - - # Check if encounter already exists - cursor = await db.execute(""" - SELECT total_encounters, caught_count FROM player_encounters - WHERE player_id = ? AND species_id = ? - """, (player_id, species_id)) - existing = await cursor.fetchone() - - if existing: - # Update existing encounter - new_total = existing[0] + 1 - new_caught = existing[1] + (1 if was_caught else 0) - await db.execute(""" - UPDATE player_encounters - SET total_encounters = ?, last_encounter_date = CURRENT_TIMESTAMP, caught_count = ? - WHERE player_id = ? AND species_id = ? - """, (new_total, new_caught, player_id, species_id)) - else: - # Create new encounter record - caught_count = 1 if was_caught else 0 - await db.execute(""" - INSERT INTO player_encounters (player_id, species_id, caught_count) - VALUES (?, ?, ?) - """, (player_id, species_id, caught_count)) - - await db.commit() - return True - - except Exception as e: - self.logger.error(f"Error recording encounter: {e}") - return False - - async def get_player_encounters(self, player_id: int) -> List[Dict]: - """Get all encounters for a player""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT pe.*, ps.name as species_name, ps.type1, ps.type2, ps.rarity - FROM player_encounters pe - JOIN pet_species ps ON pe.species_id = ps.id - WHERE pe.player_id = ? - ORDER BY pe.first_encounter_date ASC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_encounter_stats(self, player_id: int) -> Dict: - """Get encounter statistics for a player""" - async with aiosqlite.connect(self.db_path) as db: - # Total species encountered - cursor = await db.execute(""" - SELECT COUNT(*) FROM player_encounters WHERE player_id = ? - """, (player_id,)) - species_encountered = (await cursor.fetchone())[0] - - # Total encounters - cursor = await db.execute(""" - SELECT SUM(total_encounters) FROM player_encounters WHERE player_id = ? - """, (player_id,)) - total_encounters_result = await cursor.fetchone() - total_encounters = total_encounters_result[0] if total_encounters_result[0] else 0 - - # Total species available - cursor = await db.execute(""" - SELECT COUNT(*) FROM pet_species - """) - total_species = (await cursor.fetchone())[0] - - # Calculate completion percentage - completion_percentage = (species_encountered / total_species * 100) if total_species > 0 else 0 - - return { - "species_encountered": species_encountered, - "total_encounters": total_encounters, - "total_species": total_species, - "completion_percentage": round(completion_percentage, 1) - } - - async def set_pet_nickname(self, player_id: int, pet_identifier: str, nickname: str) -> Dict: - """Set a nickname for a pet. Returns result dict.""" - # Validate nickname - if not nickname.strip(): - return {"success": False, "error": "Nickname cannot be empty"} - - nickname = nickname.strip() - if len(nickname) > 20: - return {"success": False, "error": "Nickname must be 20 characters or less"} - - # Basic content validation - if any(char in nickname for char in ['<', '>', '&', '"', "'"]): - return {"success": False, "error": "Nickname contains invalid characters"} - - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Find pet by current nickname or species name - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - AND (p.nickname = ? OR ps.name = ?) - LIMIT 1 - """, (player_id, pet_identifier, pet_identifier)) - - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": f"Pet '{pet_identifier}' not found"} - - # Update nickname - await db.execute("UPDATE pets SET nickname = ? WHERE id = ?", (nickname, pet["id"])) - await db.commit() - - return { - "success": True, - "pet": dict(pet), - "old_name": pet["nickname"] or pet["species_name"], - "new_nickname": nickname - } - - # Team Builder PIN System Methods - async def generate_verification_pin(self, player_id: int, request_type: str, request_data: str = None) -> Dict: - """Generate a secure PIN for verification. Returns PIN code and expiration.""" - import secrets - import string - from datetime import datetime, timedelta - - # Generate cryptographically secure 6-digit PIN - pin_code = ''.join(secrets.choice(string.digits) for _ in range(6)) - - # PIN expires in 10 minutes - expires_at = datetime.now() + timedelta(minutes=10) - - async with aiosqlite.connect(self.db_path) as db: - # Clear any existing unused PINs for this player and request type - await db.execute(""" - UPDATE verification_pins - SET is_used = TRUE, used_at = CURRENT_TIMESTAMP - WHERE player_id = ? AND request_type = ? AND is_used = FALSE - """, (player_id, request_type)) - - # Insert new PIN - cursor = await db.execute(""" - INSERT INTO verification_pins - (player_id, pin_code, request_type, request_data, expires_at) - VALUES (?, ?, ?, ?, ?) - """, (player_id, pin_code, request_type, request_data, expires_at.isoformat())) - - await db.commit() - pin_id = cursor.lastrowid - - return { - "success": True, - "pin_id": pin_id, - "pin_code": pin_code, - "expires_at": expires_at, - "expires_in_minutes": 10 - } - - async def verify_pin(self, player_id: int, pin_code: str, request_type: str) -> Dict: - """Verify a PIN code and return request data if valid.""" - from datetime import datetime - - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Find valid PIN - cursor = await db.execute(""" - SELECT * FROM verification_pins - WHERE player_id = ? AND pin_code = ? AND request_type = ? - AND is_used = FALSE AND expires_at > datetime('now') - ORDER BY created_at DESC LIMIT 1 - """, (player_id, pin_code, request_type)) - - pin_record = await cursor.fetchone() - if not pin_record: - return {"success": False, "error": "Invalid or expired PIN"} - - # Mark PIN as used - await db.execute(""" - UPDATE verification_pins - SET is_used = TRUE, used_at = CURRENT_TIMESTAMP - WHERE id = ? - """, (pin_record["id"],)) - - await db.commit() - - return { - "success": True, - "pin_id": pin_record["id"], - "request_data": pin_record["request_data"], - "created_at": pin_record["created_at"] - } - - async def create_pending_team_change(self, player_id: int, new_team_data: str) -> Dict: - """Create a pending team change request and generate PIN.""" - from datetime import datetime, timedelta - import json - - # Validate team data is valid JSON - try: - json.loads(new_team_data) - except json.JSONDecodeError: - return {"success": False, "error": "Invalid team data format"} - - # Generate PIN for this request - pin_result = await self.generate_verification_pin( - player_id, "team_change", new_team_data - ) - - if not pin_result["success"]: - return pin_result - - # Store pending change - expires_at = datetime.now() + timedelta(minutes=10) - - async with aiosqlite.connect(self.db_path) as db: - # Clear any existing pending changes for this player - # This prevents race conditions with multiple pending team changes - await db.execute(""" - DELETE FROM pending_team_changes - WHERE player_id = ? AND is_verified = FALSE - """, (player_id,)) - - # Insert new pending change - cursor = await db.execute(""" - INSERT INTO pending_team_changes - (player_id, new_team_data, verification_pin, expires_at) - VALUES (?, ?, ?, ?) - """, (player_id, new_team_data, pin_result["pin_code"], expires_at.isoformat())) - - await db.commit() - change_id = cursor.lastrowid - - return { - "success": True, - "change_id": change_id, - "pin_code": pin_result["pin_code"], - "expires_at": expires_at, - "expires_in_minutes": 10 - } - - async def apply_team_change(self, player_id: int, pin_code: str) -> Dict: - """Apply pending team change after PIN verification.""" - import json - from datetime import datetime - - # Verify PIN - pin_result = await self.verify_pin(player_id, pin_code, "team_change") - if not pin_result["success"]: - return pin_result - - # Get team data from request - new_team_data = pin_result["request_data"] - if not new_team_data: - return {"success": False, "error": "No team data found for this PIN"} - - try: - change_data = json.loads(new_team_data) - # Handle new format with team slot or old format - if isinstance(change_data, dict) and 'teamData' in change_data: - team_changes = change_data['teamData'] - team_slot = change_data.get('teamSlot', 1) - else: - # Backwards compatibility - old format - team_changes = change_data - team_slot = 1 - - # Validate team slot - if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: - return {"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"} - - # Validate team_changes is a dictionary - if not isinstance(team_changes, dict): - return {"success": False, "error": f"Invalid team changes format. Expected dict, got {type(team_changes)}"} - - - except json.JSONDecodeError: - return {"success": False, "error": "Invalid team data format"} - - # Apply team changes atomically - async with aiosqlite.connect(self.db_path) as db: - try: - # Begin transaction - await db.execute("BEGIN TRANSACTION") - - # Handle Team 1 (Active Team) vs Teams 2-3 (Saved Configurations) differently - if team_slot == 1: - # Team 1: Apply directly to active team (immediate effect) - - # First, deactivate all pets for this player - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE player_id = ? - """, (player_id,)) - - # Then activate and position the selected pets - for pet_id, position in team_changes.items(): - if position: # If position is a number (1-6), pet is active - await db.execute(""" - UPDATE pets SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (position, int(pet_id), player_id)) - - # Mark pending change as verified - await db.execute(""" - UPDATE pending_team_changes - SET is_verified = TRUE, verified_at = CURRENT_TIMESTAMP - WHERE player_id = ? AND verification_pin = ? AND is_verified = FALSE - """, (player_id, pin_code)) - - await db.commit() - - # Count changes applied - changes_applied = sum(1 for pos in team_changes.values() if pos) - - return { - "success": True, - "message": f"Active team updated successfully", - "changes_applied": changes_applied, - "team_slot": team_slot - } - - else: - # Teams 2-3: Save as configuration only (no immediate effect on active team) - - # Prepare team configuration data - config_data = {} - for pet_id, position in team_changes.items(): - if position: # Only include pets that are in team slots - # Get pet info for the configuration - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? - """, (pet_id, player_id)) - pet_row = await cursor.fetchone() - - if pet_row: - pet_dict = dict(pet_row) - config_data[str(position)] = { - 'id': pet_dict['id'], - 'name': pet_dict['nickname'] or pet_dict['species_name'], - 'level': pet_dict['level'], - 'type_primary': pet_dict['type1'], - 'hp': pet_dict['hp'], - 'max_hp': pet_dict['max_hp'] - } - - # Save team configuration - config_json = json.dumps(config_data) - await db.execute(""" - INSERT OR REPLACE INTO team_configurations - (player_id, slot_number, config_name, team_data, updated_at) - VALUES (?, ?, ?, ?, datetime('now')) - """, (player_id, team_slot, f'Team {team_slot}', config_json)) - - # Mark pending change as verified - await db.execute(""" - UPDATE pending_team_changes - SET is_verified = TRUE, verified_at = CURRENT_TIMESTAMP - WHERE player_id = ? AND verification_pin = ? AND is_verified = FALSE - """, (player_id, pin_code)) - - await db.commit() - - # Count changes applied - changes_applied = sum(1 for pos in team_changes.values() if pos) - - return { - "success": True, - "message": f"Team {team_slot} configuration saved successfully", - "changes_applied": changes_applied, - "team_slot": team_slot - } - - except Exception as e: - await db.rollback() - self.logger.error(f"Database error in apply_team_change: {e}") - return {"success": False, "error": f"Database error: {str(e)}"} - - async def apply_individual_team_change(self, player_id: int, pin_code: str) -> Dict: - """Apply individual team change with simplified logic""" - import json - - # Verify PIN first - pin_result = await self.verify_pin(player_id, pin_code, "team_change") - if not pin_result["success"]: - return pin_result - - # Get team data from request - new_team_data = pin_result["request_data"] - if not new_team_data: - return {"success": False, "error": "No team data found for this PIN"} - - try: - change_data = json.loads(new_team_data) - team_slot = change_data.get('teamSlot', 1) - team_changes = change_data.get('teamData', {}) - - # Simple validation - if not isinstance(team_changes, dict): - return {"success": False, "error": "Invalid team data format"} - - # Apply changes atomically - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - try: - await db.execute("BEGIN TRANSACTION") - - # NEW DESIGN: All team slots (1, 2, 3) are saved as configurations - pets_list = [] - for pet_id_str, position in team_changes.items(): - if position: - # Get full pet information from database - cursor = await db.execute(""" - SELECT p.id, p.nickname, p.level, p.hp, p.max_hp, p.attack, p.defense, p.speed, p.happiness, - ps.name as species_name, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? - """, (int(pet_id_str), player_id)) - pet_row = await cursor.fetchone() - - if pet_row: - # Convert Row object to dict properly - pet_dict = { - 'id': pet_row['id'], - 'nickname': pet_row['nickname'], - 'level': pet_row['level'], - 'hp': pet_row['hp'], - 'max_hp': pet_row['max_hp'], - 'attack': pet_row['attack'], - 'defense': pet_row['defense'], - 'speed': pet_row['speed'], - 'happiness': pet_row['happiness'], - 'species_name': pet_row['species_name'], - 'type1': pet_row['type1'], - 'type2': pet_row['type2'], - 'team_order': int(position) - } - pets_list.append(pet_dict) - - # Save configuration for any slot (1, 2, or 3) - await db.execute(""" - INSERT OR REPLACE INTO team_configurations - (player_id, slot_number, config_name, team_data, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - """, (player_id, team_slot, f"Team {team_slot}", json.dumps(pets_list))) - - await db.commit() - return {"success": True, "message": f"Team {team_slot} saved successfully"} - - except Exception as e: - await db.execute("ROLLBACK") - self.logger.error(f"Database error in apply_individual_team_change: {e}") - return {"success": False, "error": f"Database error: {str(e)}"} - - except json.JSONDecodeError: - return {"success": False, "error": "Invalid team data format"} - except Exception as e: - print(f"Error in apply_individual_team_change: {e}") - return {"success": False, "error": str(e)} - - async def cleanup_expired_pins(self) -> Dict: - """Clean up expired PINs and pending changes.""" - async with aiosqlite.connect(self.db_path) as db: - # Clean expired verification pins - cursor = await db.execute(""" - DELETE FROM verification_pins - WHERE expires_at < datetime('now') OR is_used = TRUE - """) - pins_cleaned = cursor.rowcount - - # Clean expired pending team changes - cursor = await db.execute(""" - DELETE FROM pending_team_changes - WHERE expires_at < datetime('now') OR is_verified = TRUE - """) - changes_cleaned = cursor.rowcount - - await db.commit() - - return { - "success": True, - "pins_cleaned": pins_cleaned, - "changes_cleaned": changes_cleaned - } - - # Species Moves System Methods - async def get_species_moves(self, species_id: int, max_level: int = None) -> List[Dict]: - """Get moves that a species can learn up to a certain level""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - query = """ - SELECT sm.*, m.name, m.type, m.category, m.power, m.accuracy, m.pp, m.description - FROM species_moves sm - JOIN moves m ON sm.move_id = m.id - WHERE sm.species_id = ? - """ - params = [species_id] - - if max_level: - query += " AND sm.learn_level <= ?" - params.append(max_level) - - query += " ORDER BY sm.learn_level ASC" - - cursor = await db.execute(query, params) - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_learnable_moves_for_pet(self, pet_id: int) -> List[Dict]: - """Get moves a specific pet can learn at their current level""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Get pet info - cursor = await db.execute(""" - SELECT p.species_id, p.level, p.id - FROM pets p WHERE p.id = ? - """, (pet_id,)) - pet = await cursor.fetchone() - - if not pet: - return [] - - # Get moves the pet can learn but doesn't know yet - cursor = await db.execute(""" - SELECT sm.*, m.name, m.type, m.category, m.power, m.accuracy, m.pp, m.description - FROM species_moves sm - JOIN moves m ON sm.move_id = m.id - WHERE sm.species_id = ? AND sm.learn_level <= ? - AND sm.move_id NOT IN ( - SELECT pm.move_id FROM pet_moves pm WHERE pm.pet_id = ? - ) - ORDER BY sm.learn_level DESC - """, (pet["species_id"], pet["level"], pet_id)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def validate_team_composition(self, player_id: int, proposed_changes: Dict) -> Dict: - """Validate team changes before applying them""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Get current pet states - cursor = await db.execute(""" - SELECT id, is_active, team_order FROM pets WHERE player_id = ? - """, (player_id,)) - current_pets = {str(row["id"]): row["team_order"] if row["is_active"] else False for row in await cursor.fetchall()} - - # Apply proposed changes to current state - new_state = current_pets.copy() - for pet_id, new_position in proposed_changes.items(): - if pet_id in new_state: - new_state[pet_id] = new_position - - # Count active pets and validate positions - active_positions = [pos for pos in new_state.values() if pos] - active_count = len(active_positions) - - # Check for valid positions (1-6) - for pos in active_positions: - if not isinstance(pos, int) or pos < 1 or pos > 6: - return {"valid": False, "error": f"Invalid team position: {pos}"} - - # Check for duplicate positions - if len(active_positions) != len(set(active_positions)): - return {"valid": False, "error": "Duplicate team positions detected"} - - # Validate constraints - if active_count < 1: - return {"valid": False, "error": "Must have at least 1 active pet"} - - if active_count > 6: # Pokemon-style 6 pet limit - return {"valid": False, "error": "Cannot have more than 6 active pets"} - - return {"valid": True, "active_count": active_count} - - async def get_active_team(self, player_id: int) -> List: - """Get active team pets as a list using new active_teams table design""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Get the active slot for this player - cursor = await db.execute(""" - SELECT active_slot FROM active_teams WHERE player_id = ? - """, (player_id,)) - active_row = await cursor.fetchone() - - if not active_row: - # No active team set, return empty list - return [] - - active_slot = active_row['active_slot'] - - # Get the team configuration for the active slot - cursor = await db.execute(""" - SELECT team_data FROM team_configurations - WHERE player_id = ? AND slot_number = ? - """, (player_id, active_slot)) - config_row = await cursor.fetchone() - - if not config_row: - # No configuration for active slot, return empty list - return [] - - # Parse the team data and convert to list format - import json - try: - team_pets = json.loads(config_row['team_data']) - - # Convert to list format expected by webserver and team_management - team_list = [] - for pet in team_pets: - team_list.append({ - 'id': pet['id'], - 'nickname': pet['nickname'], - 'species_name': pet['species_name'], - 'level': pet['level'], - 'hp': pet['hp'], - 'max_hp': pet['max_hp'], - 'type1': pet['type1'], - 'type2': pet['type2'], - 'attack': pet['attack'], - 'defense': pet['defense'], - 'speed': pet['speed'], - 'moves': pet.get('moves', []), - 'team_order': pet.get('team_order') - }) - - # Sort by team_order to maintain consistent positioning - team_list.sort(key=lambda x: x.get('team_order', 0)) - return team_list - - except (json.JSONDecodeError, KeyError) as e: - print(f"Error parsing active team data: {e}") - return [] - - async def set_active_team_slot(self, player_id: int, slot_number: int) -> Dict: - """Set which team slot is currently active""" - try: - if slot_number not in [1, 2, 3]: - return {"success": False, "error": "Invalid slot number. Must be 1, 2, or 3"} - - async with aiosqlite.connect(self.db_path) as db: - # Check if the slot has a team configuration - cursor = await db.execute(""" - SELECT team_data FROM team_configurations - WHERE player_id = ? AND slot_number = ? - """, (player_id, slot_number)) - config = await cursor.fetchone() - - if not config: - return {"success": False, "error": f"No team configuration found in slot {slot_number}"} - - # Check if team has pets - import json - try: - team_data = json.loads(config[0]) - if not team_data: - return {"success": False, "error": f"Team {slot_number} is empty"} - except json.JSONDecodeError: - return {"success": False, "error": f"Invalid team data in slot {slot_number}"} - - # Update or insert active team slot - await db.execute(""" - INSERT OR REPLACE INTO active_teams (player_id, active_slot, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP) - """, (player_id, slot_number)) - - await db.commit() - - return { - "success": True, - "message": f"Team {slot_number} is now active", - "active_slot": slot_number, - "pet_count": len(team_data) - } - - except Exception as e: - print(f"Error setting active team slot: {e}") - return {"success": False, "error": str(e)} - - async def get_active_team_slot(self, player_id: int) -> int: - """Get the current active team slot for a player""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT active_slot FROM active_teams - WHERE player_id = ? - """, (player_id,)) - result = await cursor.fetchone() - - # Default to slot 1 if not set - return result[0] if result else 1 - - async def get_team_composition(self, player_id: int) -> Dict: - """Get current team composition stats""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT - COUNT(*) as total_pets, - SUM(CASE WHEN is_active THEN 1 ELSE 0 END) as active_pets, - SUM(CASE WHEN NOT is_active THEN 1 ELSE 0 END) as storage_pets - FROM pets WHERE player_id = ? - """, (player_id,)) - - result = await cursor.fetchone() - return { - "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) -> dict: - """Set weather for a specific location and return previous weather info""" - 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 {"success": False, "error": "Location not found"} - - location_id = location_row[0] - - # Get current weather before changing it - previous_weather = await self.get_location_weather_by_name(location_name) - previous_weather_type = previous_weather["weather_type"] if previous_weather else "calm" - - # 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 { - "success": True, - "location": location_name, - "previous_weather": previous_weather_type, - "new_weather": weather_type, - "changed": previous_weather_type != weather_type - } - except Exception as e: - print(f"Error setting weather for location {location_name}: {e}") - return {"success": False, "error": str(e)} - - # 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 - - async def get_player_team_configurations(self, player_id: int) -> List[Dict]: - """Get all team configurations for a player with team data""" - try: - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - configs = [] - for slot in range(1, 4): # Slots 1, 2, 3 - cursor = await db.execute(""" - SELECT config_name, team_data, updated_at - FROM team_configurations - WHERE player_id = ? AND slot_number = ? - """, (player_id, slot)) - - row = await cursor.fetchone() - if row: - import json - team_data = json.loads(row['team_data']) - - # Handle both new format (list) and old format (dict) - if isinstance(team_data, list): - pet_count = len(team_data) - elif isinstance(team_data, dict): - pet_count = len([p for p in team_data.values() if p]) - else: - pet_count = 0 - - configs.append({ - 'slot': slot, - 'name': row['config_name'], - 'team_data': team_data, - 'updated_at': row['updated_at'], - 'pet_count': pet_count - }) - else: - configs.append({ - 'slot': slot, - 'name': f'Team {slot}', - 'team_data': {}, - 'updated_at': None, - 'pet_count': 0 - }) - - return configs - - except Exception as e: - print(f"Error getting player team configurations: {e}") - return [] - - async def apply_team_configuration(self, player_id: int, slot_number: int) -> Dict: - """Apply a saved team configuration to the player's active team""" - try: - # Load the team configuration - config = await self.load_team_configuration(player_id, slot_number) - if not config: - return {"success": False, "error": f"No team configuration found in slot {slot_number}"} - - import json - team_data = json.loads(config["team_data"]) - - async with aiosqlite.connect(self.db_path) as db: - # First, deactivate all pets for this player - await db.execute(""" - UPDATE pets - SET is_active = FALSE, team_order = NULL - WHERE player_id = ? - """, (player_id,)) - - # Apply the saved team configuration - handle both formats - if isinstance(team_data, list): - # New format: list of pet objects with team_order - for pet_info in team_data: - if pet_info and 'id' in pet_info and 'team_order' in pet_info: - pet_id = pet_info['id'] - team_order = pet_info['team_order'] - - # Activate the pet and set its team position - await db.execute(""" - UPDATE pets - SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (team_order, pet_id, player_id)) - else: - # Old format: dict with positions as keys - for position, pet_info in team_data.items(): - if pet_info and 'id' in pet_info: - pet_id = pet_info['id'] - team_order = int(position) # position should be 1-6 - - # Activate the pet and set its team position - await db.execute(""" - UPDATE pets - SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (team_order, pet_id, player_id)) - - await db.commit() - - return { - "success": True, - "message": f"Applied team configuration '{config['config_name']}' to active team", - "config_name": config['config_name'] - } - - except Exception as e: - print(f"Error applying team configuration: {e}") - return {"success": False, "error": str(e)} - - # Pet Rename System Methods - async def request_pet_rename(self, player_id: int, pet_id: int, new_nickname: str) -> Dict: - """Request to rename a pet with PIN verification""" - try: - # Input validation - if not new_nickname or len(new_nickname.strip()) == 0: - return {"success": False, "error": "Pet nickname cannot be empty"} - - new_nickname = new_nickname.strip() - if len(new_nickname) > 20: - return {"success": False, "error": "Pet nickname must be 20 characters or less"} - - # Check for profanity/inappropriate content (basic check) - inappropriate_words = ["admin", "bot", "system", "null", "undefined"] - if any(word in new_nickname.lower() for word in inappropriate_words): - return {"success": False, "error": "Pet nickname contains inappropriate content"} - - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Verify pet ownership - cursor = await db.execute(""" - SELECT id, nickname, species_id FROM pets - WHERE id = ? AND player_id = ? - """, (pet_id, player_id)) - - pet = await cursor.fetchone() - if not pet: - return {"success": False, "error": "Pet not found or not owned by player"} - - # Check if new nickname is same as current - if pet["nickname"] == new_nickname: - return {"success": False, "error": "New nickname is the same as current nickname"} - - # Check for duplicate nicknames within player's pets - cursor = await db.execute(""" - SELECT id FROM pets - WHERE player_id = ? AND nickname = ? AND id != ? - """, (player_id, new_nickname, pet_id)) - - duplicate = await cursor.fetchone() - if duplicate: - return {"success": False, "error": "You already have a pet with this nickname"} - - # Generate PIN with 15-second timeout - pin_result = await self.generate_verification_pin(player_id, "pet_rename", f"{pet_id}:{new_nickname}") - if not pin_result["success"]: - return {"success": False, "error": "Failed to generate verification PIN"} - - return { - "success": True, - "pin": pin_result["pin_code"], - "expires_at": pin_result["expires_at"], - "pet_id": pet_id, - "new_nickname": new_nickname - } - - except Exception as e: - print(f"Error requesting pet rename: {e}") - return {"success": False, "error": f"Database error: {str(e)}"} - - async def verify_pet_rename(self, player_id: int, pin_code: str) -> Dict: - """Verify PIN and complete pet rename""" - try: - # Use the new PIN verification system - pin_result = await self.verify_pin(player_id, pin_code, "pet_rename") - if not pin_result["success"]: - return pin_result - - # Parse the request data to get pet_id and new_nickname - request_data = pin_result["request_data"] - try: - pet_id, new_nickname = request_data.split(":", 1) - pet_id = int(pet_id) - except (ValueError, IndexError): - return {"success": False, "error": "Invalid request data format"} - - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Verify pet ownership and update nickname - cursor = await db.execute(""" - UPDATE pets SET nickname = ? WHERE id = ? AND player_id = ? - """, (new_nickname, pet_id, player_id)) - - if cursor.rowcount == 0: - return {"success": False, "error": "Pet not found or permission denied"} - - await db.commit() - - return { - "success": True, - "new_nickname": new_nickname - } - - except Exception as e: - print(f"Error verifying pet rename: {e}") - return {"success": False, "error": "Database error occurred"} - - async def get_player_pets_for_rename(self, player_id: int) -> List[Dict]: - """Get all pets for a player with rename information""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - ORDER BY p.is_active DESC, p.team_order ASC, p.level DESC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - # NPC Events System Methods - async def create_npc_event(self, event_data: Dict) -> int: - """Create a new NPC event""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - INSERT INTO npc_events - (event_type, title, description, difficulty, target_contributions, - reward_experience, reward_money, reward_items, expires_at, completion_message) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - event_data['event_type'], - event_data['title'], - event_data['description'], - event_data['difficulty'], - event_data['target_contributions'], - event_data['reward_experience'], - event_data['reward_money'], - event_data.get('reward_items', ''), - event_data['expires_at'], - event_data.get('completion_message', '') - )) - - await db.commit() - return cursor.lastrowid - - async def get_active_npc_events(self) -> List[Dict]: - """Get all active NPC events""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT * FROM npc_events - WHERE status = 'active' AND expires_at > datetime('now') - ORDER BY created_at DESC - """) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_npc_event_by_id(self, event_id: int) -> Optional[Dict]: - """Get a specific NPC event by ID""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT * FROM npc_events WHERE id = ? - """, (event_id,)) - - row = await cursor.fetchone() - return dict(row) if row else None - - async def contribute_to_npc_event(self, event_id: int, player_id: int, contribution: int) -> Dict: - """Add player contribution to an NPC event""" - async with aiosqlite.connect(self.db_path) as db: - try: - await db.execute("BEGIN TRANSACTION") - - # Insert or update player contribution - await db.execute(""" - INSERT OR REPLACE INTO npc_event_contributions - (event_id, player_id, contributions, last_contribution_at) - VALUES (?, ?, - COALESCE((SELECT contributions FROM npc_event_contributions - WHERE event_id = ? AND player_id = ?), 0) + ?, - CURRENT_TIMESTAMP) - """, (event_id, player_id, event_id, player_id, contribution)) - - # Update total contributions for the event - await db.execute(""" - UPDATE npc_events - SET current_contributions = current_contributions + ? - WHERE id = ? - """, (contribution, event_id)) - - # Check if event is completed - cursor = await db.execute(""" - SELECT current_contributions, target_contributions - FROM npc_events WHERE id = ? - """, (event_id,)) - - row = await cursor.fetchone() - if row and row[0] >= row[1]: - # Mark event as completed - await db.execute(""" - UPDATE npc_events - SET status = 'completed' - WHERE id = ? - """, (event_id,)) - - await db.commit() - return {"success": True, "event_completed": True} - - await db.commit() - return {"success": True, "event_completed": False} - - except Exception as e: - await db.execute("ROLLBACK") - return {"success": False, "error": str(e)} - - async def get_player_event_contributions(self, player_id: int, event_id: int) -> int: - """Get player's contributions to a specific event""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT contributions FROM npc_event_contributions - WHERE event_id = ? AND player_id = ? - """, (event_id, player_id)) - - row = await cursor.fetchone() - return row[0] if row else 0 - - async def get_event_leaderboard(self, event_id: int) -> List[Dict]: - """Get leaderboard for an event""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.nickname, c.contributions, c.last_contribution_at - FROM npc_event_contributions c - JOIN players p ON c.player_id = p.id - WHERE c.event_id = ? - ORDER BY c.contributions DESC - """, (event_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def expire_npc_events(self) -> int: - """Mark expired events as expired and return count""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - UPDATE npc_events - SET status = 'expired' - WHERE status = 'active' AND expires_at <= datetime('now') - """) - - await db.commit() - return cursor.rowcount - - async def distribute_event_rewards(self, event_id: int) -> Dict: - """Distribute rewards to all participants of a completed event""" - async with aiosqlite.connect(self.db_path) as db: - try: - await db.execute("BEGIN TRANSACTION") - - # Get event details - cursor = await db.execute(""" - SELECT reward_experience, reward_money, reward_items - FROM npc_events WHERE id = ? AND status = 'completed' - """, (event_id,)) - - event_row = await cursor.fetchone() - if not event_row: - await db.execute("ROLLBACK") - return {"success": False, "error": "Event not found or not completed"} - - reward_exp, reward_money, reward_items = event_row - - # Get all participants - cursor = await db.execute(""" - SELECT player_id, contributions - FROM npc_event_contributions - WHERE event_id = ? - """, (event_id,)) - - participants = await cursor.fetchall() - - # Distribute rewards - for player_id, contributions in participants: - # Scale rewards based on contribution (minimum 50% of full reward) - contribution_multiplier = max(0.5, min(1.0, contributions / 10)) - - final_exp = int(reward_exp * contribution_multiplier) - final_money = int(reward_money * contribution_multiplier) - - # Update player rewards - await db.execute(""" - UPDATE players - SET experience = experience + ?, money = money + ? - WHERE id = ? - """, (final_exp, final_money, player_id)) - - await db.commit() - return {"success": True, "participants_rewarded": len(participants)} - - except Exception as e: - await db.execute("ROLLBACK") - return {"success": False, "error": str(e)} - - # Pet Healing System Methods - async def get_fainted_pets(self, player_id: int) -> List[Dict]: - """Get all fainted pets for a player""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.fainted_at IS NOT NULL - ORDER BY p.fainted_at DESC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def revive_pet(self, pet_id: int, new_hp: int) -> bool: - """Revive a fainted pet and restore HP""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = ?, fainted_at = NULL - WHERE id = ? - """, (new_hp, pet_id)) - - await db.commit() - return True - except Exception as e: - print(f"Error reviving pet: {e}") - return False - - async def faint_pet(self, pet_id: int) -> bool: - """Mark a pet as fainted""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = 0, fainted_at = CURRENT_TIMESTAMP - WHERE id = ? - """, (pet_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error fainting pet: {e}") - return False - - async def get_pets_for_auto_recovery(self) -> List[Dict]: - """Get pets that are eligible for auto-recovery (fainted for 30+ minutes)""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.fainted_at IS NOT NULL - AND p.fainted_at <= datetime('now', '-30 minutes') - """) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def auto_recover_pet(self, pet_id: int) -> bool: - """Auto-recover a pet to 1 HP after 30 minutes""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = 1, fainted_at = NULL - WHERE id = ? - """, (pet_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error auto-recovering pet: {e}") - return False - - async def get_last_heal_time(self, player_id: int) -> Optional[datetime]: - """Get the last time a player used the heal command""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT last_heal_time FROM players WHERE id = ? - """, (player_id,)) - - row = await cursor.fetchone() - if row and row[0]: - return datetime.fromisoformat(row[0]) - return None - - async def update_last_heal_time(self, player_id: int) -> bool: - """Update the last heal time for a player""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE players - SET last_heal_time = CURRENT_TIMESTAMP - WHERE id = ? - """, (player_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error updating last heal time: {e}") - return False - - async def get_active_pets(self, player_id: int) -> List[Dict]: - """Get all active pets for a player (excluding fainted pets) using new active_teams system""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - # Get the current active team slot - cursor = await db.execute(""" - SELECT active_slot FROM active_teams WHERE player_id = ? - """, (player_id,)) - active_team_row = await cursor.fetchone() - - if not active_team_row: - return [] # No active team set - - active_slot = active_team_row[0] - - # Get the team configuration for the active slot - cursor = await db.execute(""" - SELECT team_data FROM team_configurations - WHERE player_id = ? AND slot_number = ? - """, (player_id, active_slot)) - config_row = await cursor.fetchone() - - if not config_row or not config_row[0]: - return [] # No team configuration or empty team - - # Parse the team data - import json - try: - team_data = json.loads(config_row[0]) if isinstance(config_row[0], str) else config_row[0] - except (json.JSONDecodeError, TypeError): - return [] - - # Get full pet details for each pet in the team (excluding fainted) - active_pets = [] - for pet_data in team_data: - if pet_data and 'id' in pet_data: - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? AND p.fainted_at IS NULL - """, (pet_data['id'], player_id)) - pet_row = await cursor.fetchone() - if pet_row: - pet_dict = dict(pet_row) - # Add team_order from the saved configuration - pet_dict['team_order'] = pet_data.get('team_order', len(active_pets) + 1) - active_pets.append(pet_dict) - - # Sort by team_order - active_pets.sort(key=lambda x: x.get('team_order', 999)) - return active_pets - - async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: - """Get all pets for a player, optionally filtering to active only""" - if active_only: - # Use the new active team system (this calls the updated get_active_pets method) - return await self.get_active_pets(player_id) - else: - # Return all pets for storage view - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - ORDER BY p.level DESC, p.id ASC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - # Pet Moves System Methods - async def get_pet_moves(self, pet_id: int) -> List[Dict]: - """Get all moves for a specific pet""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT pm.*, m.type, m.category, m.power as base_power, - m.accuracy as base_accuracy, m.pp as base_pp, m.description - FROM pet_moves pm - JOIN moves m ON pm.move_name = m.name - WHERE pm.pet_id = ? - ORDER BY pm.learned_at_level, pm.id - """, (pet_id,)) - - moves = await cursor.fetchall() - - # Calculate actual stats with IVs - result = [] - for move in moves: - move_dict = dict(move) - move_dict['actual_power'] = max(1, move_dict['base_power'] + move_dict['power_iv']) - move_dict['actual_accuracy'] = max(10, min(100, move_dict['base_accuracy'] + move_dict['accuracy_iv'])) - move_dict['actual_pp'] = max(1, move_dict['base_pp'] + move_dict['pp_iv']) - result.append(move_dict) - - return result - - async def add_pet_move(self, pet_id: int, move_name: str, power_iv: int = 0, - accuracy_iv: int = 0, pp_iv: int = 0, learned_at_level: int = 1) -> bool: - """Add a move to a pet""" - try: - async with aiosqlite.connect(self.db_path) as db: - # Check if pet already has this move - cursor = await db.execute(""" - SELECT COUNT(*) FROM pet_moves - WHERE pet_id = ? AND move_name = ? - """, (pet_id, move_name)) - - if (await cursor.fetchone())[0] > 0: - return False # Pet already has this move - - # Add the move - await db.execute(""" - INSERT INTO pet_moves - (pet_id, move_name, power_iv, accuracy_iv, pp_iv, learned_at_level) - VALUES (?, ?, ?, ?, ?, ?) - """, (pet_id, move_name, power_iv, accuracy_iv, pp_iv, learned_at_level)) - - await db.commit() - return True - - except Exception as e: - print(f"Error adding pet move: {e}") - return False - - async def remove_pet_move(self, pet_id: int, move_name: str) -> bool: - """Remove a move from a pet""" - try: - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - DELETE FROM pet_moves - WHERE pet_id = ? AND move_name = ? - """, (pet_id, move_name)) - - await db.commit() - return cursor.rowcount > 0 - - except Exception as e: - print(f"Error removing pet move: {e}") - return False - - async def clear_pet_moves(self, pet_id: int) -> bool: - """Remove all moves from a pet""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute("DELETE FROM pet_moves WHERE pet_id = ?", (pet_id,)) - await db.commit() - return True - - except Exception as e: - print(f"Error clearing pet moves: {e}") - return False - - async def generate_pet_moves(self, pet_id: int, species_type: str, level: int = 1, - rarity: int = 1, is_starter: bool = False) -> bool: - """Generate random moves for a pet based on type and configuration""" - try: - import json - import random - - # Load configuration files - with open("config/move_pools.json", "r") as f: - move_pools = json.load(f) - - with open("config/move_generation.json", "r") as f: - generation_config = json.load(f) - - # Get type-specific move pool - type_pool = move_pools.get(species_type, move_pools.get("Normal", {})) - if not type_pool: - print(f"No move pool found for type {species_type}") - return False - - # Get generation settings - iv_config = generation_config["iv_system"] - level_config = generation_config["level_scaling"] - rarity_config = generation_config["rarity_bonuses"].get(str(rarity), {"bonus_moves": 0, "rare_move_multiplier": 1.0, "iv_bonus": 0}) - - # Determine move count - min_moves, max_moves = type_pool["move_count_range"] - move_count = random.randint(min_moves, max_moves) - - # Add rarity bonus moves - move_count += rarity_config.get("bonus_moves", 0) - move_count = min(move_count, generation_config["balance_settings"]["max_moves_per_pet"]) - - # Generate moves - selected_moves = set() - weights = type_pool["weights"] - - # Ensure basic moves for starters - if is_starter: - starter_rules = generation_config["generation_rules"]["starter_pets"] - basic_moves = type_pool.get("basic_moves", []) - guaranteed_basic = min(starter_rules["guaranteed_basic_moves"], len(basic_moves)) - - for i in range(guaranteed_basic): - if basic_moves and len(selected_moves) < move_count: - move = random.choice(basic_moves) - selected_moves.add(move) - - # Add guaranteed type move for starters - type_moves = type_pool.get("type_moves", []) - if type_moves and len(selected_moves) < move_count: - move = random.choice(type_moves) - selected_moves.add(move) - - # Fill remaining slots - all_possible_moves = [] - - # Add basic moves - for move in type_pool.get("basic_moves", []): - if random.random() < weights.get("basic", 0.8): - all_possible_moves.append(move) - - # Add type moves - for move in type_pool.get("type_moves", []): - if random.random() < weights.get("type", 0.4): - all_possible_moves.append(move) - - # Add rare moves (with level and rarity requirements) - if level >= level_config["rare_move_unlock"]["base_level"]: - rare_chance = weights.get("rare", 0.1) * rarity_config.get("rare_move_multiplier", 1.0) - for move in type_pool.get("rare_moves", []): - if random.random() < rare_chance: - all_possible_moves.append(move) - - # Randomly select remaining moves - while len(selected_moves) < move_count and all_possible_moves: - move = random.choice(all_possible_moves) - selected_moves.add(move) - all_possible_moves.remove(move) - - # Ensure at least one move - if not selected_moves: - fallback_moves = move_pools.get("_settings", {}).get("fallback_moves", ["Tackle"]) - selected_moves.add(random.choice(fallback_moves)) - - # Add moves to database with IVs - for move_name in selected_moves: - # Generate IVs - iv_ranges = iv_config["ranges"] - iv_bonus = rarity_config.get("iv_bonus", 0) - - power_iv = random.randint(iv_ranges["power"]["min"], iv_ranges["power"]["max"]) + iv_bonus - accuracy_iv = random.randint(iv_ranges["accuracy"]["min"], iv_ranges["accuracy"]["max"]) + (iv_bonus // 3) - pp_iv = random.randint(iv_ranges["pp"]["min"], iv_ranges["pp"]["max"]) + (iv_bonus // 2) - - # Clamp IVs to reasonable ranges - power_iv = max(-15, min(25, power_iv)) - accuracy_iv = max(-10, min(15, accuracy_iv)) - pp_iv = max(-10, min(20, pp_iv)) - - await self.add_pet_move(pet_id, move_name, power_iv, accuracy_iv, pp_iv, level) - - if generation_config["admin_controls"]["log_move_generation"]: - print(f"Generated {len(selected_moves)} moves for pet {pet_id}: {list(selected_moves)}") - - return True - - except Exception as e: - print(f"Error generating pet moves: {e}") - return False - - async def migrate_existing_pets_to_move_system(self) -> bool: - """Migrate existing pets to the new move system""" - try: - print("🔄 Migrating existing pets to new move system...") - - # Get all pets that don't have moves yet - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.id, p.level, ps.type1, ps.rarity, ps.name as species_name - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id NOT IN (SELECT DISTINCT pet_id FROM pet_moves) - """) - - pets_to_migrate = await cursor.fetchall() - - migrated_count = 0 - for pet in pets_to_migrate: - success = await self.generate_pet_moves( - pet["id"], - pet["type1"], - pet["level"], - pet["rarity"] or 1, - False # Not a starter - ) - - if success: - migrated_count += 1 - if migrated_count % 10 == 0: - print(f" Migrated {migrated_count}/{len(pets_to_migrate)} pets...") - - print(f"✅ Successfully migrated {migrated_count} pets to new move system") - return True - - except Exception as e: - print(f"Error migrating pets to move system: {e}") - return False \ No newline at end of file + return True \ No newline at end of file diff --git a/src/game_engine.py b/src/game_engine.py index 970ccba..acbdb0f 100644 --- a/src/game_engine.py +++ b/src/game_engine.py @@ -2,7 +2,6 @@ import json import random import aiosqlite import asyncio -import logging from typing import Dict, List, Optional from .database import Database from .battle_engine import BattleEngine @@ -17,9 +16,7 @@ class GameEngine: self.type_chart = {} self.weather_patterns = {} self.weather_task = None - self.pet_recovery_task = None self.shutdown_event = asyncio.Event() - self.logger = logging.getLogger(__name__) async def load_game_data(self): await self.load_pet_species() @@ -28,10 +25,8 @@ class GameEngine: await self.load_type_chart() await self.load_achievements() await self.database.initialize_items() - await self.database.initialize_gyms() await self.init_weather_system() await self.battle_engine.load_battle_data() - await self.start_pet_recovery_system() async def load_pet_species(self): try: @@ -39,28 +34,19 @@ class GameEngine: species_data = json.load(f) async with aiosqlite.connect(self.database.db_path) as db: - # Check if species already exist to avoid re-inserting and changing IDs - cursor = await db.execute("SELECT COUNT(*) FROM pet_species") - existing_count = (await cursor.fetchone())[0] - - if existing_count == 0: - # Only insert if no species exist to avoid ID conflicts - for species in species_data: - await db.execute(""" - INSERT OR IGNORE INTO pet_species - (name, type1, type2, base_hp, base_attack, base_defense, - base_speed, evolution_level, rarity, emoji) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - species["name"], species["type1"], species.get("type2"), - species["base_hp"], species["base_attack"], species["base_defense"], - species["base_speed"], species.get("evolution_level"), - species.get("rarity", 1), species.get("emoji", "🐾") - )) - await db.commit() - self.logger.info(f"✅ Loaded {len(species_data)} pet species into database") - else: - self.logger.info(f"✅ Found {existing_count} existing pet species - skipping reload to preserve IDs") + for species in species_data: + await db.execute(""" + INSERT OR IGNORE INTO pet_species + (name, type1, type2, base_hp, base_attack, base_defense, + base_speed, evolution_level, rarity) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + species["name"], species["type1"], species.get("type2"), + species["base_hp"], species["base_attack"], species["base_defense"], + species["base_speed"], species.get("evolution_level"), + species.get("rarity", 1) + )) + await db.commit() except FileNotFoundError: await self.create_default_species() @@ -209,44 +195,30 @@ class GameEngine: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, - attack, defense, speed, is_active, team_order, - iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + attack, defense, speed, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, species["id"], pet_data["level"], 0, - pet_data["hp"], pet_data["max_hp"], pet_data["attack"], - pet_data["defense"], pet_data["speed"], True, 1, - pet_data["iv_hp"], pet_data["iv_attack"], pet_data["iv_defense"], - pet_data["iv_speed"], player_id)) + pet_data["hp"], pet_data["hp"], pet_data["attack"], + pet_data["defense"], pet_data["speed"], True)) await db.commit() return {"species_name": chosen_starter, **pet_data} def generate_pet_stats(self, species: Dict, level: int = 1) -> Dict: - # Generate individual IVs for each stat (0-31) - iv_hp = random.randint(0, 31) - iv_attack = random.randint(0, 31) - iv_defense = random.randint(0, 31) - iv_speed = random.randint(0, 31) + iv_bonus = random.randint(0, 31) - # Calculate stats using individual IVs (Pokemon-style formula) - hp = int((2 * species["base_hp"] + iv_hp) * level / 100) + level + 10 - attack = int((2 * species["base_attack"] + iv_attack) * level / 100) + 5 - defense = int((2 * species["base_defense"] + iv_defense) * level / 100) + 5 - speed = int((2 * species["base_speed"] + iv_speed) * level / 100) + 5 + hp = int((2 * species["base_hp"] + iv_bonus) * level / 100) + level + 10 + attack = int((2 * species["base_attack"] + iv_bonus) * level / 100) + 5 + defense = int((2 * species["base_defense"] + iv_bonus) * level / 100) + 5 + speed = int((2 * species["base_speed"] + iv_bonus) * level / 100) + 5 return { "level": level, "hp": hp, - "max_hp": hp, # Initial HP is max HP "attack": attack, "defense": defense, - "speed": speed, - # Include IVs in the returned data for storage - "iv_hp": iv_hp, - "iv_attack": iv_attack, - "iv_defense": iv_defense, - "iv_speed": iv_speed + "speed": speed } async def attempt_catch(self, player_id: int, location_name: str) -> str: @@ -286,14 +258,11 @@ class GameEngine: if random.random() < catch_rate: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, - attack, defense, speed, is_active, - iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + attack, defense, speed, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, chosen_spawn["species_id"], pet_level, 0, - pet_stats["hp"], pet_stats["max_hp"], pet_stats["attack"], - pet_stats["defense"], pet_stats["speed"], False, - pet_stats["iv_hp"], pet_stats["iv_attack"], pet_stats["iv_defense"], - pet_stats["iv_speed"], player_id)) + pet_stats["hp"], pet_stats["hp"], pet_stats["attack"], + pet_stats["defense"], pet_stats["speed"], False)) await db.commit() return f"Caught a level {pet_level} {chosen_spawn['species_name']}!" @@ -312,11 +281,10 @@ class GameEngine: return [] cursor = await db.execute(""" - SELECT DISTINCT ps.name, ps.type1, ps.type2, MIN(ls.spawn_rate) as spawn_rate + SELECT ps.name, ps.type1, ps.type2, ls.spawn_rate FROM location_spawns ls JOIN pet_species ps ON ls.species_id = ps.id WHERE ls.location_id = ? - GROUP BY ps.id, ps.name, ps.type1, ps.type2 """, (location["id"],)) spawns = await cursor.fetchall() @@ -396,9 +364,6 @@ class GameEngine: except FileNotFoundError: return None - # Get global spawn multiplier from config - global_multiplier = items_data.get("_config", {}).get("global_spawn_multiplier", 1.0) - # Get all possible items for this location available_items = [] location_name = location["name"].lower().replace(" ", "_") @@ -409,25 +374,22 @@ class GameEngine: if "locations" in item: item_locations = item["locations"] if "all" in item_locations or location_name in item_locations: - # Apply global multiplier to spawn rate - item_copy = item.copy() - item_copy["effective_spawn_rate"] = item.get("spawn_rate", 0.1) * global_multiplier - available_items.append(item_copy) + available_items.append(item) if not available_items: return None - # Calculate total spawn rates for this location (using effective rates) - total_rate = sum(item.get("effective_spawn_rate", 0.1) for item in available_items) + # Calculate total spawn rates for this location + total_rate = sum(item.get("spawn_rate", 0.1) for item in available_items) # 30% base chance of finding an item if random.random() > 0.3: return None - # Choose item based on effective spawn rates (with global multiplier applied) + # Choose item based on spawn rates chosen_item = random.choices( available_items, - weights=[item.get("effective_spawn_rate", 0.1) for item in available_items] + weights=[item.get("spawn_rate", 0.1) for item in available_items] )[0] # Add item to player's inventory @@ -457,15 +419,12 @@ class GameEngine: async with aiosqlite.connect(self.database.db_path) as db: cursor = await db.execute(""" INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp, - attack, defense, speed, is_active, - iv_hp, iv_attack, iv_defense, iv_speed, original_trainer_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + attack, defense, speed, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (player_id, target_pet["species_id"], target_pet["level"], 0, - target_pet["stats"]["hp"], target_pet["stats"]["max_hp"], + target_pet["stats"]["hp"], target_pet["stats"]["hp"], target_pet["stats"]["attack"], target_pet["stats"]["defense"], - target_pet["stats"]["speed"], False, - target_pet["stats"]["iv_hp"], target_pet["stats"]["iv_attack"], - target_pet["stats"]["iv_defense"], target_pet["stats"]["iv_speed"], player_id)) + target_pet["stats"]["speed"], False)) await db.commit() return f"Success! You caught the Level {target_pet['level']} {target_pet['species_name']}!" @@ -493,7 +452,7 @@ class GameEngine: # Insert or update achievement await db.execute(""" - INSERT OR REPLACE INTO achievements + INSERT OR IGNORE INTO achievements (name, description, requirement_type, requirement_data, unlock_location_id) VALUES (?, ?, ?, ?, ?) """, ( @@ -505,7 +464,7 @@ class GameEngine: await db.commit() except FileNotFoundError: - self.logger.warning("No achievements.json found, skipping achievement loading") + print("No achievements.json found, skipping achievement loading") async def init_weather_system(self): """Initialize random weather for all locations""" @@ -520,7 +479,7 @@ class GameEngine: await self.start_weather_system() except FileNotFoundError: - self.logger.warning("No weather_patterns.json found, skipping weather system") + print("No weather_patterns.json found, skipping weather system") self.weather_patterns = {"weather_types": {}, "location_weather_chances": {}} async def update_all_weather(self): @@ -569,10 +528,6 @@ class GameEngine: """Check for new achievements after player actions""" return await self.database.check_player_achievements(player_id, action_type, data) - async def check_all_achievements(self, player_id: int): - """Check and award ALL possible achievements for a player""" - return await self.database.check_all_achievements(player_id) - async def get_weather_modified_spawns(self, location_id: int, spawns: list) -> list: """Apply weather modifiers to spawn rates""" weather = await self.database.get_location_weather(location_id) @@ -602,12 +557,12 @@ class GameEngine: async def start_weather_system(self): """Start the background weather update task""" if self.weather_task is None or self.weather_task.done(): - self.logger.info("🌤️ Starting weather update background task...") + print("🌤️ Starting weather update background task...") self.weather_task = asyncio.create_task(self._weather_update_loop()) async def stop_weather_system(self): """Stop the background weather update task""" - self.logger.info("🌤️ Stopping weather update background task...") + print("🌤️ Stopping weather update background task...") self.shutdown_event.set() if self.weather_task and not self.weather_task.done(): self.weather_task.cancel() @@ -633,37 +588,37 @@ class GameEngine: except asyncio.CancelledError: break except Exception as e: - self.logger.error(f"Error in weather update loop: {e}") + print(f"Error in weather update loop: {e}") # Continue the loop even if there's an error await asyncio.sleep(60) # Wait a minute before retrying except asyncio.CancelledError: - self.logger.info("Weather update task cancelled") + print("Weather update task cancelled") async def _check_and_update_expired_weather(self): - """Check for expired weather and update it with announcements""" + """Check for expired weather and update it""" try: async with aiosqlite.connect(self.database.db_path) as db: - # Find locations with expired weather and get their current weather + # Find locations with expired weather cursor = await db.execute(""" - SELECT l.id, l.name, lw.weather_type as current_weather + SELECT l.id, l.name FROM locations l - LEFT JOIN location_weather lw ON l.id = lw.location_id - AND lw.active_until > datetime('now') - WHERE lw.location_id IS NULL OR lw.active_until <= datetime('now') + WHERE l.id NOT IN ( + SELECT location_id FROM location_weather + WHERE active_until > datetime('now') + ) """) expired_locations = await cursor.fetchall() if expired_locations: - self.logger.info(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather") + print(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather") for location in expired_locations: location_id = location[0] location_name = location[1] - previous_weather = location[2] if location[2] else "calm" # Get possible weather for this location - possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["calm"]) + possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"]) # Choose random weather weather_type = random.choice(possible_weather) @@ -691,146 +646,14 @@ class GameEngine: ",".join(weather_config.get("affected_types", [])) )) - self.logger.info(f" 🌤️ {location_name}: Weather changed from {previous_weather} to {weather_type} for {duration_minutes} minutes") - - # Announce weather change to IRC - await self.announce_weather_change(location_name, previous_weather, weather_type, "auto") + print(f" 🌤️ {location_name}: New {weather_type} weather for {duration_minutes} minutes") await db.commit() except Exception as e: - self.logger.error(f"Error checking expired weather: {e}") - - async def announce_weather_change(self, location_name: str, previous_weather: str, new_weather: str, source: str = "auto"): - """Announce weather changes to IRC channel""" - try: - # Get weather emojis - weather_emojis = { - "sunny": "☀️", - "rainy": "🌧️", - "storm": "⛈️", - "blizzard": "❄️", - "earthquake": "🌍", - "calm": "🌤️" - } - - prev_emoji = weather_emojis.get(previous_weather, "🌤️") - new_emoji = weather_emojis.get(new_weather, "🌤️") - - # Create announcement message - if previous_weather == new_weather: - return # No change, no announcement - - if source == "admin": - message = f"🌤️ Weather Update: {location_name} weather has been changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather} by admin command!" - elif source == "web": - message = f"🌤️ Weather Update: {location_name} weather has been changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather} via web interface!" - else: - message = f"🌤️ Weather Update: {location_name} weather has naturally changed from {prev_emoji} {previous_weather} to {new_emoji} {new_weather}!" - - # Send to IRC channel via bot instance - if hasattr(self, 'bot') and self.bot: - from config import IRC_CONFIG - channel = IRC_CONFIG.get("channel", "#petz") - if hasattr(self.bot, 'send_message_sync'): - self.bot.send_message_sync(channel, message) - elif hasattr(self.bot, 'send_message'): - import asyncio - if hasattr(self.bot, 'main_loop') and self.bot.main_loop: - future = asyncio.run_coroutine_threadsafe( - self.bot.send_message(channel, message), - self.bot.main_loop - ) - else: - asyncio.create_task(self.bot.send_message(channel, message)) - - except Exception as e: - self.logger.error(f"Error announcing weather change: {e}") - - async def get_pet_emoji(self, species_name: str) -> str: - """Get emoji for a pet species""" - try: - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute( - "SELECT emoji FROM pet_species WHERE name = ?", - (species_name,) - ) - row = await cursor.fetchone() - return row[0] if row and row[0] else "🐾" - except Exception: - return "🐾" # Default emoji if something goes wrong - - def format_pet_name_with_emoji(self, pet_name: str, emoji: str = None, species_name: str = None) -> str: - """Format pet name with emoji for display in IRC or web - - Args: - pet_name: The pet's nickname or species name - emoji: Optional emoji to use (if None, will look up by species_name) - species_name: Species name for emoji lookup if emoji not provided - - Returns: - Formatted string like "🔥 Flamey" or "🐉 Infernowyrm" - """ - if emoji: - return f"{emoji} {pet_name}" - elif species_name: - # This would need to be async, but for IRC we can use sync fallback - return f"🐾 {pet_name}" # Use default for now, can be enhanced later - else: - return f"🐾 {pet_name}" - - async def start_pet_recovery_system(self): - """Start the background pet recovery task""" - if self.pet_recovery_task is None or self.pet_recovery_task.done(): - self.logger.info("🏥 Starting pet recovery background task...") - self.pet_recovery_task = asyncio.create_task(self._pet_recovery_loop()) - - async def stop_pet_recovery_system(self): - """Stop the background pet recovery task""" - self.logger.info("🏥 Stopping pet recovery background task...") - if self.pet_recovery_task and not self.pet_recovery_task.done(): - self.pet_recovery_task.cancel() - try: - await self.pet_recovery_task - except asyncio.CancelledError: - pass - - async def _pet_recovery_loop(self): - """Background task that checks for pets eligible for auto-recovery""" - try: - while not self.shutdown_event.is_set(): - try: - # Check every 5 minutes for pets that need recovery - await asyncio.sleep(300) # 5 minutes - - if self.shutdown_event.is_set(): - break - - # Get pets eligible for auto-recovery (fainted for 30+ minutes) - eligible_pets = await self.database.get_pets_for_auto_recovery() - - if eligible_pets: - self.logger.info(f"🏥 Auto-recovering {len(eligible_pets)} pet(s) after 30 minutes...") - - for pet in eligible_pets: - success = await self.database.auto_recover_pet(pet["id"]) - if success: - self.logger.info(f" 🏥 Auto-recovered {pet['nickname'] or pet['species_name']} (ID: {pet['id']}) to 1 HP") - else: - self.logger.error(f" ❌ Failed to auto-recover pet ID: {pet['id']}") - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in pet recovery loop: {e}") - # Continue the loop even if there's an error - await asyncio.sleep(60) # Wait a minute before retrying - - except asyncio.CancelledError: - self.logger.info("Pet recovery task cancelled") + print(f"Error checking expired weather: {e}") async def shutdown(self): """Gracefully shutdown the game engine""" - self.logger.info("🔄 Shutting down game engine...") - await self.stop_weather_system() - await self.stop_pet_recovery_system() \ No newline at end of file + print("🔄 Shutting down game engine...") + await self.stop_weather_system() \ No newline at end of file diff --git a/src/irc_connection_manager.py b/src/irc_connection_manager.py deleted file mode 100644 index 990532a..0000000 --- a/src/irc_connection_manager.py +++ /dev/null @@ -1,413 +0,0 @@ -import asyncio -import socket -import time -import logging -import random -from enum import Enum -from typing import Optional, Callable, Dict, Any -from datetime import datetime, timedelta - - -class ConnectionState(Enum): - DISCONNECTED = "disconnected" - CONNECTING = "connecting" - CONNECTED = "connected" - AUTHENTICATED = "authenticated" - JOINED = "joined" - RECONNECTING = "reconnecting" - FAILED = "failed" - - -class IRCConnectionManager: - """ - Robust IRC connection manager with automatic reconnection, - health monitoring, and exponential backoff. - """ - - def __init__(self, config: Dict[str, Any], bot_instance=None): - self.config = config - self.bot = bot_instance - self.socket = None - self.state = ConnectionState.DISCONNECTED - self.running = False - - # Connection monitoring - self.last_ping_time = 0 - self.last_pong_time = 0 - self.ping_interval = 60 # Send PING every 60 seconds - self.ping_timeout = 180 # Expect PONG within 3 minutes - - # Reconnection settings - self.reconnect_attempts = 0 - self.max_reconnect_attempts = 50 - self.base_reconnect_delay = 1 # Start with 1 second - self.max_reconnect_delay = 300 # Cap at 5 minutes - self.reconnect_jitter = 0.1 # 10% jitter - - # Connection tracking - self.connection_start_time = None - self.last_successful_connection = None - self.total_reconnections = 0 - self.connection_failures = 0 - - # Event callbacks - self.on_connect_callback = None - self.on_disconnect_callback = None - self.on_message_callback = None - self.on_connection_lost_callback = None - - # Health monitoring - self.health_check_interval = 30 # Check health every 30 seconds - self.health_check_task = None - self.message_count = 0 - self.last_message_time = 0 - - # Setup logging - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.INFO) - - # Create console handler if none exists - if not self.logger.handlers: - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - def set_callbacks(self, on_connect=None, on_disconnect=None, on_message=None, on_connection_lost=None): - """Set callback functions for connection events.""" - self.on_connect_callback = on_connect - self.on_disconnect_callback = on_disconnect - self.on_message_callback = on_message - self.on_connection_lost_callback = on_connection_lost - - async def start(self): - """Start the connection manager.""" - if self.running: - self.logger.warning("Connection manager is already running") - return - - self.running = True - self.logger.info("Starting IRC connection manager") - - # Start health monitoring - self.health_check_task = asyncio.create_task(self._health_monitor()) - - # Start connection loop - await self._connection_loop() - - async def stop(self): - """Stop the connection manager and close connections.""" - self.running = False - self.logger.info("Stopping IRC connection manager") - - # Cancel health monitoring - if self.health_check_task: - self.health_check_task.cancel() - try: - await self.health_check_task - except asyncio.CancelledError: - pass - - # Close socket - await self._disconnect() - - async def _connection_loop(self): - """Main connection loop with automatic reconnection.""" - while self.running: - try: - if self.state == ConnectionState.DISCONNECTED: - await self._connect() - - if self.state in [ConnectionState.CONNECTED, ConnectionState.AUTHENTICATED, ConnectionState.JOINED]: - await self._handle_messages() - - await asyncio.sleep(0.1) - - except Exception as e: - self.logger.error(f"Error in connection loop: {e}") - await self._handle_connection_error(e) - - async def _connect(self): - """Connect to IRC server with retry logic.""" - if self.reconnect_attempts >= self.max_reconnect_attempts: - self.logger.error(f"Maximum reconnection attempts ({self.max_reconnect_attempts}) reached") - self.state = ConnectionState.FAILED - return - - self.state = ConnectionState.CONNECTING - self.connection_start_time = datetime.now() - - try: - # Calculate reconnection delay with exponential backoff - if self.reconnect_attempts > 0: - delay = min( - self.base_reconnect_delay * (2 ** self.reconnect_attempts), - self.max_reconnect_delay - ) - # Add jitter to prevent thundering herd - jitter = delay * self.reconnect_jitter * random.random() - delay += jitter - - self.logger.info(f"Reconnection attempt {self.reconnect_attempts + 1}/{self.max_reconnect_attempts} after {delay:.1f}s delay") - await asyncio.sleep(delay) - - # Test basic connectivity first - await self._test_connectivity() - - # Create socket connection - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(10) # 10 second timeout for connection - - # Connect to server - await asyncio.get_event_loop().run_in_executor( - None, self.socket.connect, (self.config["server"], self.config["port"]) - ) - - self.socket.settimeout(1) # Shorter timeout for message handling - self.state = ConnectionState.CONNECTED - - # Send IRC handshake - await self._send_handshake() - - # Reset reconnection counter on successful connection - self.reconnect_attempts = 0 - self.last_successful_connection = datetime.now() - self.total_reconnections += 1 - - self.logger.info(f"Successfully connected to {self.config['server']}:{self.config['port']}") - - except Exception as e: - self.logger.error(f"Connection failed: {e}") - self.connection_failures += 1 - self.reconnect_attempts += 1 - await self._disconnect() - - if self.on_connection_lost_callback: - await self.on_connection_lost_callback(e) - - async def _test_connectivity(self): - """Test basic network connectivity to IRC server.""" - try: - test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - test_sock.settimeout(5) - - await asyncio.get_event_loop().run_in_executor( - None, test_sock.connect, (self.config["server"], self.config["port"]) - ) - - test_sock.close() - - except Exception as e: - raise ConnectionError(f"Network connectivity test failed: {e}") - - async def _send_handshake(self): - """Send IRC handshake messages.""" - nickname = self.config["nickname"] - await self._send_raw(f"NICK {nickname}") - await self._send_raw(f"USER {nickname} 0 * :{nickname}") - - # Initialize ping tracking - self.last_ping_time = time.time() - self.last_pong_time = time.time() - - async def _handle_messages(self): - """Handle incoming IRC messages.""" - try: - data = await asyncio.get_event_loop().run_in_executor( - None, self.socket.recv, 4096 - ) - - if not data: - raise ConnectionError("Connection closed by server") - - # Update message tracking - self.message_count += 1 - self.last_message_time = time.time() - - # Decode and process messages - lines = data.decode('utf-8', errors='ignore').strip().split('\n') - for line in lines: - if line.strip(): - await self._process_line(line.strip()) - - except socket.timeout: - # Timeout is expected, continue - pass - except Exception as e: - raise ConnectionError(f"Message handling error: {e}") - - async def _process_line(self, line): - """Process a single IRC line.""" - # Handle PING/PONG - 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 - - # 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 - if "376" in line or "422" in line: # End of MOTD - if self.state == ConnectionState.CONNECTED: - self.state = ConnectionState.AUTHENTICATED - await self._send_raw(f"JOIN {self.config['channel']}") - - # Handle successful channel join - if " JOIN " in line and self.config["channel"] in line: - if self.state == ConnectionState.AUTHENTICATED: - self.state = ConnectionState.JOINED - self.logger.info(f"Successfully joined {self.config['channel']}") - - if self.on_connect_callback: - await self.on_connect_callback() - - # Handle nickname conflicts - if "433" in line: # Nickname in use - new_nickname = f"{self.config['nickname']}_" - self.logger.warning(f"Nickname conflict, trying {new_nickname}") - await self._send_raw(f"NICK {new_nickname}") - - # Handle disconnection - if "ERROR :Closing Link" in line: - raise ConnectionError("Server closed connection") - - # Forward message to callback - if self.on_message_callback: - await self.on_message_callback(line) - - async def _send_raw(self, message): - """Send raw IRC message.""" - if not self.socket: - raise ConnectionError("Not connected to IRC server") - - try: - full_message = f"{message}\r\n" - await asyncio.get_event_loop().run_in_executor( - None, self.socket.send, full_message.encode('utf-8') - ) - except Exception as e: - raise ConnectionError(f"Failed to send message: {e}") - - async def send_message(self, target, message): - """Send a message to a channel or user.""" - if self.state != ConnectionState.JOINED: - self.logger.warning(f"Cannot send message, not joined to channel (state: {self.state})") - return False - - try: - await self._send_raw(f"PRIVMSG {target} :{message}") - return True - except Exception as e: - self.logger.error(f"Failed to send message to {target}: {e}") - return False - - async def _health_monitor(self): - """Monitor connection health and send periodic pings.""" - while self.running: - try: - await asyncio.sleep(self.health_check_interval) - - if self.state == ConnectionState.JOINED: - await self._check_connection_health() - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Health monitor error: {e}") - - async def _check_connection_health(self): - """Check if connection is healthy and send pings as needed.""" - current_time = time.time() - - # Send ping if interval has passed - if current_time - self.last_ping_time > self.ping_interval: - try: - 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 (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.""" - self.logger.error(f"Connection error: {error}") - - # Notify callback - if self.on_disconnect_callback: - await self.on_disconnect_callback(error) - - # Disconnect and prepare for reconnection - await self._disconnect() - - # Set state for reconnection - if self.running: - self.state = ConnectionState.DISCONNECTED - self.reconnect_attempts += 1 - - async def _disconnect(self): - """Disconnect from IRC server.""" - if self.socket: - try: - self.socket.close() - except: - pass - self.socket = None - - old_state = self.state - self.state = ConnectionState.DISCONNECTED - - if old_state != ConnectionState.DISCONNECTED: - self.logger.info("Disconnected from IRC server") - - def get_connection_stats(self) -> Dict[str, Any]: - """Get connection statistics.""" - uptime = None - if self.connection_start_time: - uptime = datetime.now() - self.connection_start_time - - return { - "state": self.state.value, - "connected": self.state == ConnectionState.JOINED, - "uptime": str(uptime) if uptime else None, - "reconnect_attempts": self.reconnect_attempts, - "total_reconnections": self.total_reconnections, - "connection_failures": self.connection_failures, - "last_successful_connection": self.last_successful_connection, - "message_count": self.message_count, - "last_message_time": datetime.fromtimestamp(self.last_message_time) if self.last_message_time else None, - "last_ping_time": datetime.fromtimestamp(self.last_ping_time) if self.last_ping_time else None, - "last_pong_time": datetime.fromtimestamp(self.last_pong_time) if self.last_pong_time else None - } - - def is_connected(self) -> bool: - """Check if bot is connected and ready.""" - return self.state == ConnectionState.JOINED - - def get_state(self) -> ConnectionState: - """Get current connection state.""" - return self.state \ No newline at end of file diff --git a/src/npc_events.py b/src/npc_events.py deleted file mode 100644 index a8324f6..0000000 --- a/src/npc_events.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -NPC Events System -Manages random collaborative events that all players can participate in -""" - -import asyncio -import random -from datetime import datetime, timedelta -from typing import Dict, List, Optional -from src.database import Database - -class NPCEventsManager: - def __init__(self, database: Database): - self.database = database - self.active_events = {} - self.event_templates = { - 1: [ # Difficulty 1 - Easy - { - "event_type": "resource_gathering", - "title": "Village Supply Run", - "description": "The village needs supplies! Help gather resources by exploring and finding items.", - "target_contributions": 25, - "reward_experience": 50, - "reward_money": 100, - "completion_message": "🎉 The village has enough supplies! Everyone who helped gets rewarded!", - "duration_hours": 4 - }, - { - "event_type": "pet_rescue", - "title": "Lost Pet Search", - "description": "A pet has gone missing! Help search different locations to find clues.", - "target_contributions": 20, - "reward_experience": 40, - "reward_money": 80, - "completion_message": "🐾 The lost pet has been found safe! Thanks to everyone who helped search!", - "duration_hours": 3 - }, - { - "event_type": "community_project", - "title": "Park Cleanup", - "description": "The local park needs cleaning! Help by contributing your time and effort.", - "target_contributions": 30, - "reward_experience": 35, - "reward_money": 75, - "completion_message": "🌳 The park is clean and beautiful again! Great teamwork everyone!", - "duration_hours": 5 - } - ], - 2: [ # Difficulty 2 - Medium - { - "event_type": "emergency_response", - "title": "Storm Recovery", - "description": "A storm has damaged the town! Help with recovery efforts by contributing resources and time.", - "target_contributions": 50, - "reward_experience": 100, - "reward_money": 200, - "completion_message": "⛈️ The town has recovered from the storm! Everyone's hard work paid off!", - "duration_hours": 6 - }, - { - "event_type": "festival_preparation", - "title": "Annual Festival Setup", - "description": "The annual pet festival is coming! Help set up decorations and prepare activities.", - "target_contributions": 40, - "reward_experience": 80, - "reward_money": 150, - "completion_message": "🎪 The festival is ready! Thanks to everyone who helped prepare!", - "duration_hours": 8 - }, - { - "event_type": "research_expedition", - "title": "Scientific Discovery", - "description": "Researchers need help documenting rare pets! Contribute by exploring and reporting findings.", - "target_contributions": 35, - "reward_experience": 90, - "reward_money": 180, - "completion_message": "🔬 The research is complete! Your discoveries will help future generations!", - "duration_hours": 7 - } - ], - 3: [ # Difficulty 3 - Hard - { - "event_type": "crisis_response", - "title": "Regional Emergency", - "description": "A regional crisis requires immediate community response! All trainers needed!", - "target_contributions": 75, - "reward_experience": 150, - "reward_money": 300, - "completion_message": "🚨 Crisis averted! The entire region is safe thanks to your heroic efforts!", - "duration_hours": 12 - }, - { - "event_type": "legendary_encounter", - "title": "Legendary Pet Sighting", - "description": "A legendary pet has been spotted! Help researchers track and document this rare encounter.", - "target_contributions": 60, - "reward_experience": 200, - "reward_money": 400, - "completion_message": "✨ The legendary pet has been successfully documented! History has been made!", - "duration_hours": 10 - }, - { - "event_type": "ancient_mystery", - "title": "Ancient Ruins Discovery", - "description": "Ancient ruins have been discovered! Help archaeologists uncover the secrets within.", - "target_contributions": 80, - "reward_experience": 180, - "reward_money": 350, - "completion_message": "🏛️ The ancient secrets have been revealed! Your efforts uncovered lost knowledge!", - "duration_hours": 14 - } - ] - } - - async def start_background_task(self): - """Start the background task that manages NPC events""" - while True: - try: - # Check for expired events - await self.expire_events() - - # Distribute rewards for completed events - await self.distribute_completed_rewards() - - # Maybe spawn a new event - await self.maybe_spawn_event() - - # Wait 30 minutes before next check - await asyncio.sleep(30 * 60) - - except Exception as e: - print(f"Error in NPC events background task: {e}") - await asyncio.sleep(5 * 60) # Wait 5 minutes on error - - async def expire_events(self): - """Mark expired events as expired""" - try: - expired_count = await self.database.expire_npc_events() - if expired_count > 0: - print(f"🕐 {expired_count} NPC events expired") - except Exception as e: - print(f"Error expiring NPC events: {e}") - - async def distribute_completed_rewards(self): - """Distribute rewards for completed events""" - try: - # Get completed events that haven't distributed rewards yet - completed_events = await self.database.get_active_npc_events() - - for event in completed_events: - if event['status'] == 'completed': - result = await self.database.distribute_event_rewards(event['id']) - if result['success']: - print(f"🎁 Distributed rewards for event '{event['title']}' to {result['participants_rewarded']} players") - - except Exception as e: - print(f"Error distributing event rewards: {e}") - - async def maybe_spawn_event(self): - """Maybe spawn a new event based on conditions""" - try: - # Check if we have any active events - active_events = await self.database.get_active_npc_events() - - # Don't spawn if we already have 2 or more active events - if len(active_events) >= 2: - return - - # 20% chance to spawn a new event each check (every 30 minutes) - if random.random() < 0.2: - await self.spawn_random_event() - - except Exception as e: - print(f"Error in maybe_spawn_event: {e}") - - async def spawn_random_event(self): - """Spawn a random event based on difficulty""" - try: - # Choose difficulty (weighted towards easier events) - difficulty_weights = {1: 0.6, 2: 0.3, 3: 0.1} - difficulty = random.choices(list(difficulty_weights.keys()), - weights=list(difficulty_weights.values()))[0] - - # Choose random event template - templates = self.event_templates[difficulty] - template = random.choice(templates) - - # Create event data - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - # Create the event - event_id = await self.database.create_npc_event(event_data) - - print(f"🎯 New NPC event spawned: '{template['title']}' (ID: {event_id}, Difficulty: {difficulty})") - - return event_id - - except Exception as e: - print(f"Error spawning random event: {e}") - return None - - async def get_active_events(self) -> List[Dict]: - """Get all active events""" - return await self.database.get_active_npc_events() - - async def contribute_to_event(self, event_id: int, player_id: int, contribution: int = 1) -> Dict: - """Add a player's contribution to an event""" - return await self.database.contribute_to_npc_event(event_id, player_id, contribution) - - async def get_event_details(self, event_id: int) -> Optional[Dict]: - """Get detailed information about an event""" - event = await self.database.get_npc_event_by_id(event_id) - if not event: - return None - - # Add leaderboard - leaderboard = await self.database.get_event_leaderboard(event_id) - event['leaderboard'] = leaderboard - - return event - - async def get_player_contributions(self, player_id: int, event_id: int) -> int: - """Get player's contributions to a specific event""" - return await self.database.get_player_event_contributions(player_id, event_id) - - async def force_spawn_event(self, difficulty: int = 1) -> Optional[int]: - """Force spawn an event (admin command)""" - if difficulty not in self.event_templates: - return None - - templates = self.event_templates[difficulty] - template = random.choice(templates) - - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - return await self.database.create_npc_event(event_data) - - async def force_spawn_specific_event(self, event_type: str, difficulty: int = 1) -> Optional[int]: - """Force spawn a specific event type (admin command)""" - if difficulty not in self.event_templates: - return None - - # Find template matching the event type - templates = self.event_templates[difficulty] - template = None - for t in templates: - if t['event_type'] == event_type: - template = t - break - - if not template: - return None - - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - return await self.database.create_npc_event(event_data) - - def get_progress_bar(self, current: int, target: int, width: int = 20) -> str: - """Generate a progress bar for event progress""" - filled = int((current / target) * width) - bar = "█" * filled + "░" * (width - filled) - percentage = min(100, int((current / target) * 100)) - return f"[{bar}] {percentage}% ({current}/{target})" \ No newline at end of file diff --git a/src/pin_authentication.py b/src/pin_authentication.py deleted file mode 100644 index 011b753..0000000 --- a/src/pin_authentication.py +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/env python3 -""" -PIN Authentication Service for PetBot -A standalone, reusable module for secure PIN-based verification of sensitive operations. - -Usage: - from src.pin_authentication import PinAuthenticationService - - pin_service = PinAuthenticationService(database, irc_bot) - - # Generate and send PIN - result = await pin_service.request_verification( - player_id=123, - nickname="user", - action_type="team_change", - action_data={"team": "data"}, - message_template="Custom PIN message for {pin_code}" - ) - - # Verify PIN and execute action - result = await pin_service.verify_and_execute( - player_id=123, - pin_code="123456", - action_type="team_change", - action_callback=my_callback_function - ) -""" - -import asyncio -import secrets -import string -import json -import logging -from datetime import datetime, timedelta -from typing import Dict, Optional, Callable, Any - - -class PinAuthenticationService: - """ - Standalone PIN authentication service that can be used by any component - requiring secure verification of user actions. - - Features: - - Secure 6-digit PIN generation - - Configurable expiration times - - IRC delivery integration - - Multiple request type support - - Automatic cleanup of expired PINs - - Callback-based action execution - """ - - def __init__(self, database, irc_bot=None): - """ - Initialize PIN authentication service. - - Args: - database: Database instance for PIN storage - irc_bot: Optional IRC bot instance for PIN delivery - """ - self.database = database - self.irc_bot = irc_bot - self.logger = logging.getLogger(__name__) - - # Default PIN settings - self.default_expiration_minutes = 10 - self.pin_length = 6 - - # Default message templates for different action types - self.message_templates = { - "team_change": """🔐 Team Change Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN on the team builder web page to confirm your team changes. - -⚠️ Keep this PIN private! Do not share it with anyone.""", - - "pet_rename": """🔐 Pet Rename Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN to confirm renaming your pet. - -⚠️ Keep this PIN private! Do not share it with anyone.""", - - "team_rename": """🔐 Team Rename Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN to confirm renaming your team. - -⚠️ Keep this PIN private! Do not share it with anyone.""", - - "default": """🔐 Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN to confirm your action. - -⚠️ Keep this PIN private! Do not share it with anyone.""" - } - - async def request_verification( - self, - player_id: int, - nickname: str, - action_type: str, - action_data: Any = None, - expiration_minutes: Optional[int] = None, - message_template: Optional[str] = None - ) -> Dict: - """ - Request PIN verification for a specific action. - - Args: - player_id: Player's database ID - nickname: Player's nickname for IRC delivery - action_type: Type of action (e.g., "team_change", "pet_rename") - action_data: Data associated with the action (will be JSON serialized) - expiration_minutes: Custom expiration time (default: 10 minutes) - message_template: Custom message template (default: uses action_type template) - - Returns: - Dict with success status, PIN code, and expiration info - """ - try: - # Use custom expiration or default - exp_minutes = expiration_minutes or self.default_expiration_minutes - - # Serialize action data if provided - action_data_str = json.dumps(action_data) if action_data is not None else None - - # Generate PIN - pin_result = await self.generate_verification_pin( - player_id=player_id, - request_type=action_type, - request_data=action_data_str, - expiration_minutes=exp_minutes - ) - - if not pin_result["success"]: - return pin_result - - # Send PIN via IRC if bot is available - if self.irc_bot and nickname: - await self.send_pin_via_irc( - nickname=nickname, - pin_code=pin_result["pin_code"], - action_type=action_type, - expiration_minutes=exp_minutes, - message_template=message_template - ) - - return { - "success": True, - "pin_code": pin_result["pin_code"], - "expires_at": pin_result["expires_at"], - "expires_in_minutes": exp_minutes, - "action_type": action_type - } - - except Exception as e: - return {"success": False, "error": f"Failed to request verification: {str(e)}"} - - async def verify_and_execute( - self, - player_id: int, - pin_code: str, - action_type: str, - action_callback: Optional[Callable] = None - ) -> Dict: - """ - Verify PIN and optionally execute the associated action. - - Args: - player_id: Player's database ID - pin_code: PIN code to verify - action_type: Expected action type - action_callback: Optional callback function to execute if PIN is valid - Callback receives (player_id, action_data) as arguments - - Returns: - Dict with verification result and callback execution status - """ - try: - # Verify PIN - pin_result = await self.verify_pin(player_id, pin_code, action_type) - - if not pin_result["success"]: - return pin_result - - # Parse action data if available - action_data = None - if pin_result.get("request_data"): - try: - action_data = json.loads(pin_result["request_data"]) - except json.JSONDecodeError: - action_data = pin_result["request_data"] # Keep as string if not JSON - - # Execute callback if provided - callback_result = None - if action_callback: - try: - if asyncio.iscoroutinefunction(action_callback): - callback_result = await action_callback(player_id, action_data) - else: - callback_result = action_callback(player_id, action_data) - except Exception as e: - return { - "success": False, - "error": f"Action callback failed: {str(e)}", - "pin_verified": True - } - - return { - "success": True, - "pin_verified": True, - "action_data": action_data, - "callback_result": callback_result, - "action_type": action_type - } - - except Exception as e: - return {"success": False, "error": f"Verification failed: {str(e)}"} - - async def generate_verification_pin( - self, - player_id: int, - request_type: str, - request_data: str = None, - expiration_minutes: int = None - ) -> Dict: - """ - Generate a secure PIN for verification. - - Args: - player_id: Player's database ID - request_type: Type of request (e.g., "team_change", "pet_rename") - request_data: Optional data associated with the request - expiration_minutes: PIN expiration time (default: 10 minutes) - - Returns: - Dict with PIN code and expiration information - """ - try: - # Use default expiration if not specified - exp_minutes = expiration_minutes or self.default_expiration_minutes - - # Generate cryptographically secure PIN - pin_code = ''.join(secrets.choice(string.digits) for _ in range(self.pin_length)) - - # Calculate expiration - expires_at = datetime.now() + timedelta(minutes=exp_minutes) - - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - # Clear any existing unused PINs for this player and request type - await db.execute(""" - UPDATE verification_pins - SET is_used = TRUE, used_at = CURRENT_TIMESTAMP - WHERE player_id = ? AND request_type = ? AND is_used = FALSE - """, (player_id, request_type)) - - # Insert new PIN - cursor = await db.execute(""" - INSERT INTO verification_pins - (player_id, pin_code, request_type, request_data, expires_at) - VALUES (?, ?, ?, ?, ?) - """, (player_id, pin_code, request_type, request_data, expires_at.isoformat())) - - await db.commit() - pin_id = cursor.lastrowid - - return { - "success": True, - "pin_id": pin_id, - "pin_code": pin_code, - "expires_at": expires_at, - "expires_in_minutes": exp_minutes - } - - except Exception as e: - return {"success": False, "error": f"Failed to generate PIN: {str(e)}"} - - async def verify_pin(self, player_id: int, pin_code: str, request_type: str) -> Dict: - """ - Verify a PIN code and mark it as used. - - Args: - player_id: Player's database ID - pin_code: PIN code to verify - request_type: Expected request type - - Returns: - Dict with verification result and request data - """ - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - db.row_factory = aiosqlite.Row - - # Find valid PIN - cursor = await db.execute(""" - SELECT * FROM verification_pins - WHERE player_id = ? AND pin_code = ? AND request_type = ? - AND is_used = FALSE AND expires_at > datetime('now') - ORDER BY created_at DESC LIMIT 1 - """, (player_id, pin_code, request_type)) - - pin_record = await cursor.fetchone() - - if not pin_record: - return {"success": False, "error": "Invalid or expired PIN"} - - # Mark PIN as used - await db.execute(""" - UPDATE verification_pins - SET is_used = TRUE, used_at = CURRENT_TIMESTAMP - WHERE id = ? - """, (pin_record["id"],)) - - await db.commit() - - return { - "success": True, - "request_data": pin_record["request_data"], - "request_type": pin_record["request_type"], - "pin_id": pin_record["id"] - } - - except Exception as e: - return {"success": False, "error": f"PIN verification failed: {str(e)}"} - - async def send_pin_via_irc( - self, - nickname: str, - pin_code: str, - action_type: str, - expiration_minutes: int, - message_template: Optional[str] = None - ): - """ - Send PIN to player via IRC private message. - - Args: - nickname: Player's IRC nickname - pin_code: PIN code to send - action_type: Type of action for message template selection - expiration_minutes: PIN expiration time for message - message_template: Custom message template (optional) - """ - if not self.irc_bot: - self.logger.warning(f"No IRC bot available to send PIN to {nickname}") - return - - try: - # Use custom template or select based on action type - if message_template: - message = message_template.format( - pin_code=pin_code, - expiration_minutes=expiration_minutes - ) - else: - template = self.message_templates.get(action_type, self.message_templates["default"]) - message = template.format( - pin_code=pin_code, - expiration_minutes=expiration_minutes - ) - - # Send via IRC bot - if hasattr(self.irc_bot, 'send_message_sync'): - # Use the sync wrapper method available in PetBot - self.irc_bot.send_message_sync(nickname, message) - elif hasattr(self.irc_bot, 'send_private_message'): - self.irc_bot.send_private_message(nickname, message) - elif hasattr(self.irc_bot, 'send_pm'): - await self.irc_bot.send_pm(nickname, message) - else: - self.logger.warning(f"IRC bot doesn't have a known method to send private messages") - - self.logger.info(f"🔐 Sent {action_type} PIN to {nickname}: {pin_code}") - - except Exception as e: - self.logger.error(f"Error sending PIN via IRC to {nickname}: {e}") - - async def cleanup_expired_pins(self) -> Dict: - """ - Clean up expired and used PINs from the database. - - Returns: - Dict with cleanup statistics - """ - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - # Clean expired verification pins - cursor = await db.execute(""" - DELETE FROM verification_pins - WHERE expires_at < datetime('now') OR is_used = TRUE - """) - pins_cleaned = cursor.rowcount - - await db.commit() - - return { - "success": True, - "pins_cleaned": pins_cleaned - } - - except Exception as e: - return {"success": False, "error": f"Cleanup failed: {str(e)}"} - - def add_message_template(self, action_type: str, template: str): - """ - Add or update a message template for a specific action type. - - Args: - action_type: Action type identifier - template: Message template with {pin_code} and {expiration_minutes} placeholders - """ - self.message_templates[action_type] = template - - async def cancel_pending_verification(self, player_id: int, action_type: str) -> Dict: - """ - Cancel any pending verification requests for a player and action type. - - Args: - player_id: Player's database ID - action_type: Action type to cancel - - Returns: - Dict with cancellation result - """ - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute(""" - UPDATE verification_pins - SET is_used = TRUE, used_at = CURRENT_TIMESTAMP - WHERE player_id = ? AND request_type = ? AND is_used = FALSE - """, (player_id, action_type)) - - cancelled_count = cursor.rowcount - await db.commit() - - return { - "success": True, - "cancelled_count": cancelled_count - } - - except Exception as e: - return {"success": False, "error": f"Cancellation failed: {str(e)}"} \ No newline at end of file diff --git a/src/rate_limiter.py b/src/rate_limiter.py deleted file mode 100644 index 64d6a42..0000000 --- a/src/rate_limiter.py +++ /dev/null @@ -1,419 +0,0 @@ -import time -import asyncio -from typing import Dict, Optional, Tuple -from datetime import datetime, timedelta -from enum import Enum -import logging - - -class CommandCategory(Enum): - """Categories of commands with different rate limits.""" - BASIC = "basic" # !help, !ping, !status - GAMEPLAY = "gameplay" # !explore, !catch, !battle - MANAGEMENT = "management" # !pets, !activate, !deactivate - ADMIN = "admin" # !reload, !setweather, !spawnevent - WEB = "web" # Web interface requests - - -class RateLimiter: - """ - Token bucket rate limiter with per-user tracking and command categories. - - Features: - - Token bucket algorithm for smooth rate limiting - - Per-user rate tracking - - Different limits for different command categories - - Burst capacity handling - - Admin exemption - - Detailed logging and monitoring - """ - - def __init__(self, config: Optional[Dict] = None): - self.logger = logging.getLogger(__name__) - - # Default rate limit configuration - self.config = { - "enabled": True, - "categories": { - CommandCategory.BASIC: { - "requests_per_minute": 20, - "burst_capacity": 5, - "cooldown_seconds": 1 - }, - CommandCategory.GAMEPLAY: { - "requests_per_minute": 10, - "burst_capacity": 3, - "cooldown_seconds": 3 - }, - CommandCategory.MANAGEMENT: { - "requests_per_minute": 5, - "burst_capacity": 2, - "cooldown_seconds": 5 - }, - CommandCategory.ADMIN: { - "requests_per_minute": 100, - "burst_capacity": 10, - "cooldown_seconds": 0 - }, - CommandCategory.WEB: { - "requests_per_minute": 60, - "burst_capacity": 10, - "cooldown_seconds": 1 - } - }, - "admin_users": ["megasconed"], # This will be overridden by bot initialization - "global_limits": { - "max_requests_per_minute": 200, - "max_concurrent_users": 100 - }, - "violation_penalties": { - "warning_threshold": 3, - "temporary_ban_threshold": 10, - "temporary_ban_duration": 300 # 5 minutes - } - } - - # Override with provided config - if config: - self._update_config(config) - - # Rate limiting state - self.user_buckets: Dict[str, Dict] = {} - self.global_stats = { - "requests_this_minute": 0, - "minute_start": time.time(), - "active_users": set(), - "total_requests": 0, - "blocked_requests": 0 - } - - # Violation tracking - self.violations: Dict[str, Dict] = {} - self.banned_users: Dict[str, float] = {} # user -> ban_end_time - - # Background cleanup task - self.cleanup_task = None - self.start_cleanup_task() - - def _update_config(self, config: Dict): - """Update configuration with provided values.""" - def deep_update(base_dict, update_dict): - for key, value in update_dict.items(): - if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict): - deep_update(base_dict[key], value) - else: - base_dict[key] = value - - deep_update(self.config, config) - - def start_cleanup_task(self): - """Start background cleanup task.""" - if self.cleanup_task is None or self.cleanup_task.done(): - self.cleanup_task = asyncio.create_task(self._cleanup_loop()) - - async def _cleanup_loop(self): - """Background task to clean up old data.""" - while True: - try: - await asyncio.sleep(60) # Cleanup every minute - await self._cleanup_old_data() - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in rate limiter cleanup: {e}") - - async def _cleanup_old_data(self): - """Clean up old rate limiting data.""" - current_time = time.time() - - # Clean up old user buckets (inactive for 10 minutes) - inactive_threshold = current_time - 600 - inactive_users = [ - user for user, data in self.user_buckets.items() - if data.get("last_request", 0) < inactive_threshold - ] - - for user in inactive_users: - del self.user_buckets[user] - - # Clean up old violations (older than 1 hour) - violation_threshold = current_time - 3600 - old_violations = [ - user for user, data in self.violations.items() - if data.get("last_violation", 0) < violation_threshold - ] - - for user in old_violations: - del self.violations[user] - - # Clean up expired bans - expired_bans = [ - user for user, ban_end in self.banned_users.items() - if current_time > ban_end - ] - - for user in expired_bans: - del self.banned_users[user] - self.logger.info(f"Temporary ban expired for user: {user}") - - # Reset global stats every minute - if current_time - self.global_stats["minute_start"] >= 60: - self.global_stats["requests_this_minute"] = 0 - self.global_stats["minute_start"] = current_time - self.global_stats["active_users"].clear() - - async def check_rate_limit(self, user: str, category: CommandCategory, - command: str = None) -> Tuple[bool, Optional[str]]: - """ - Check if a request is allowed under rate limiting. - - Returns: - (allowed: bool, message: Optional[str]) - """ - if not self.config["enabled"]: - return True, None - - current_time = time.time() - - # Check if user is temporarily banned - if user in self.banned_users: - if current_time < self.banned_users[user]: - remaining = int(self.banned_users[user] - current_time) - return False, f"⛔ You are temporarily banned for {remaining} seconds due to rate limit violations." - else: - del self.banned_users[user] - - # Admin exemption - if user.lower() in [admin.lower() for admin in self.config["admin_users"]]: - return True, None - - # Check global limits - if not await self._check_global_limits(): - return False, "🚫 Server is currently overloaded. Please try again later." - - # Check per-user rate limit - allowed, message = await self._check_user_rate_limit(user, category, current_time) - - if allowed: - # Update global stats - self.global_stats["requests_this_minute"] += 1 - self.global_stats["total_requests"] += 1 - self.global_stats["active_users"].add(user) - else: - # Track violation - await self._track_violation(user, category, command) - self.global_stats["blocked_requests"] += 1 - - return allowed, message - - async def _check_global_limits(self) -> bool: - """Check global rate limits.""" - # Check requests per minute - if (self.global_stats["requests_this_minute"] >= - self.config["global_limits"]["max_requests_per_minute"]): - return False - - # Check concurrent users - if (len(self.global_stats["active_users"]) >= - self.config["global_limits"]["max_concurrent_users"]): - return False - - return True - - async def _check_user_rate_limit(self, user: str, category: CommandCategory, - current_time: float) -> Tuple[bool, Optional[str]]: - """Check per-user rate limit using token bucket algorithm.""" - category_config = self.config["categories"][category] - - # Get or create user bucket - if user not in self.user_buckets: - self.user_buckets[user] = { - "tokens": category_config["burst_capacity"], - "last_refill": current_time, - "last_request": current_time - } - - bucket = self.user_buckets[user] - - # Calculate tokens to add (refill rate) - time_passed = current_time - bucket["last_refill"] - tokens_per_second = category_config["requests_per_minute"] / 60.0 - tokens_to_add = time_passed * tokens_per_second - - # Refill bucket (up to burst capacity) - bucket["tokens"] = min( - category_config["burst_capacity"], - bucket["tokens"] + tokens_to_add - ) - bucket["last_refill"] = current_time - - # Check if request is allowed - if bucket["tokens"] >= 1: - bucket["tokens"] -= 1 - bucket["last_request"] = current_time - return True, None - else: - # Calculate cooldown time - time_since_last = current_time - bucket["last_request"] - cooldown = category_config["cooldown_seconds"] - - if time_since_last < cooldown: - remaining = int(cooldown - time_since_last) - return False, f"⏱️ Rate limit exceeded. Please wait {remaining} seconds before using {category.value} commands." - else: - # Allow if cooldown has passed - bucket["tokens"] = category_config["burst_capacity"] - 1 - bucket["last_request"] = current_time - return True, None - - async def _track_violation(self, user: str, category: CommandCategory, command: str): - """Track rate limit violations for potential penalties.""" - current_time = time.time() - - if user not in self.violations: - self.violations[user] = { - "count": 0, - "last_violation": current_time, - "categories": {} - } - - violation = self.violations[user] - violation["count"] += 1 - violation["last_violation"] = current_time - - # Track by category - if category.value not in violation["categories"]: - violation["categories"][category.value] = 0 - violation["categories"][category.value] += 1 - - # Check for penalties - penalty_config = self.config["violation_penalties"] - - if violation["count"] >= penalty_config["temporary_ban_threshold"]: - # Temporary ban - ban_duration = penalty_config["temporary_ban_duration"] - self.banned_users[user] = current_time + ban_duration - self.logger.warning(f"User {user} temporarily banned for {ban_duration}s due to rate limit violations") - elif violation["count"] >= penalty_config["warning_threshold"]: - # Warning threshold reached - self.logger.warning(f"User {user} reached rate limit warning threshold ({violation['count']} violations)") - - def get_user_stats(self, user: str) -> Dict: - """Get rate limiting stats for a specific user.""" - stats = { - "user": user, - "is_banned": user in self.banned_users, - "ban_expires": None, - "violations": 0, - "buckets": {}, - "admin_exemption": user.lower() in [admin.lower() for admin in self.config["admin_users"]] - } - - # Ban info - if stats["is_banned"]: - stats["ban_expires"] = datetime.fromtimestamp(self.banned_users[user]) - - # Violation info - if user in self.violations: - stats["violations"] = self.violations[user]["count"] - - # Bucket info - if user in self.user_buckets: - bucket = self.user_buckets[user] - stats["buckets"] = { - "tokens": round(bucket["tokens"], 2), - "last_request": datetime.fromtimestamp(bucket["last_request"]) - } - - return stats - - def get_global_stats(self) -> Dict: - """Get global rate limiting statistics.""" - return { - "enabled": self.config["enabled"], - "requests_this_minute": self.global_stats["requests_this_minute"], - "active_users": len(self.global_stats["active_users"]), - "total_requests": self.global_stats["total_requests"], - "blocked_requests": self.global_stats["blocked_requests"], - "banned_users": len(self.banned_users), - "tracked_users": len(self.user_buckets), - "total_violations": sum(v["count"] for v in self.violations.values()), - "config": self.config - } - - def is_user_banned(self, user: str) -> bool: - """Check if a user is currently banned.""" - if user not in self.banned_users: - return False - - if time.time() > self.banned_users[user]: - del self.banned_users[user] - return False - - return True - - def unban_user(self, user: str) -> bool: - """Manually unban a user (admin function).""" - if user in self.banned_users: - del self.banned_users[user] - self.logger.info(f"User {user} manually unbanned") - return True - return False - - def reset_user_violations(self, user: str) -> bool: - """Reset violations for a user (admin function).""" - if user in self.violations: - del self.violations[user] - self.logger.info(f"Violations reset for user {user}") - return True - return False - - async def shutdown(self): - """Shutdown the rate limiter and cleanup tasks.""" - if self.cleanup_task: - self.cleanup_task.cancel() - try: - await self.cleanup_task - except asyncio.CancelledError: - pass - - self.logger.info("Rate limiter shutdown complete") - - -# Command category mapping -COMMAND_CATEGORIES = { - # Basic commands - "help": CommandCategory.BASIC, - "ping": CommandCategory.BASIC, - "status": CommandCategory.BASIC, - "uptime": CommandCategory.BASIC, - - # Gameplay commands - "start": CommandCategory.GAMEPLAY, - "explore": CommandCategory.GAMEPLAY, - "catch": CommandCategory.GAMEPLAY, - "battle": CommandCategory.GAMEPLAY, - "attack": CommandCategory.GAMEPLAY, - "moves": CommandCategory.GAMEPLAY, - "flee": CommandCategory.GAMEPLAY, - "travel": CommandCategory.GAMEPLAY, - "weather": CommandCategory.GAMEPLAY, - "gym": CommandCategory.GAMEPLAY, - - # Management commands - "pets": CommandCategory.MANAGEMENT, - "activate": CommandCategory.MANAGEMENT, - "deactivate": CommandCategory.MANAGEMENT, - "stats": CommandCategory.MANAGEMENT, - "inventory": CommandCategory.MANAGEMENT, - "use": CommandCategory.MANAGEMENT, - "nickname": CommandCategory.MANAGEMENT, - - # Admin commands - "reload": CommandCategory.ADMIN, -} - - -def get_command_category(command: str) -> CommandCategory: - """Get the rate limiting category for a command.""" - return COMMAND_CATEGORIES.get(command.lower(), CommandCategory.GAMEPLAY) \ No newline at end of file diff --git a/src/team_management.py b/src/team_management.py deleted file mode 100644 index aecab65..0000000 --- a/src/team_management.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python3 -""" -Team Management Service for PetBot -Handles team swapping, individual team editing, and team selection hub functionality. -""" - -import asyncio -import json -from typing import Dict, List, Optional - - -class TeamManagementService: - """Service for managing player teams and team swapping operations.""" - - def __init__(self, database, pin_service): - self.database = database - self.pin_service = pin_service - - async def get_team_overview(self, player_id: int) -> Dict: - """Get overview of all teams for a player.""" - try: - # Get active team - active_team = await self.database.get_active_team(player_id) - - # Get saved team configurations - team_configs = await self.database.get_player_team_configurations(player_id) - - # Structure the data - teams = { - "active": { - "pets": active_team, - "count": len(active_team), - "is_active": True - } - } - - # Add saved configurations - for i in range(1, 4): - config = next((c for c in team_configs if c.get("slot") == i), None) - if config: - # team_data is already parsed by get_player_team_configurations - team_data = config["team_data"] if config["team_data"] else {} - - # Use pet_count from database method which handles both formats - pet_count = config.get("pet_count", 0) - - teams[f"slot_{i}"] = { - "name": config.get("name", f"Team {i}"), - "pets": team_data, - "count": pet_count, - "last_updated": config.get("updated_at"), - "is_active": False - } - else: - teams[f"slot_{i}"] = { - "name": f"Team {i}", - "pets": {}, - "count": 0, - "last_updated": None, - "is_active": False - } - - return {"success": True, "teams": teams} - - except Exception as e: - return {"success": False, "error": f"Failed to get team overview: {str(e)}"} - - async def request_team_swap(self, player_id: int, nickname: str, source_slot: int) -> Dict: - """Request to swap a saved team configuration to active team.""" - try: - # Validate slot - if source_slot < 1 or source_slot > 3: - return {"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"} - - # Get the source team configuration - config = await self.database.load_team_configuration(player_id, source_slot) - if not config: - return {"success": False, "error": f"No team configuration found in slot {source_slot}"} - - # Parse team data - team_data = json.loads(config["team_data"]) - if not team_data: - return {"success": False, "error": f"Team {source_slot} is empty"} - - # Request PIN verification for team swap - pin_request = await self.pin_service.request_verification( - player_id=player_id, - nickname=nickname, - action_type="team_swap", - action_data={ - "source_slot": source_slot, - "team_data": team_data, - "config_name": config["config_name"] - }, - expiration_minutes=10 - ) - - if pin_request["success"]: - return { - "success": True, - "message": f"PIN sent to confirm swapping {config['config_name']} to active team", - "source_slot": source_slot, - "team_name": config["config_name"], - "expires_in_minutes": pin_request["expires_in_minutes"] - } - else: - return pin_request - - except Exception as e: - return {"success": False, "error": f"Failed to request team swap: {str(e)}"} - - async def verify_team_swap(self, player_id: int, pin_code: str) -> Dict: - """Verify PIN and execute team swap.""" - try: - # Define team swap callback - async def apply_team_swap_callback(player_id, action_data): - """Apply the team swap operation.""" - source_slot = action_data["source_slot"] - team_data = action_data["team_data"] - config_name = action_data["config_name"] - - # Get current active team before swapping - current_active = await self.database.get_active_team(player_id) - - # Apply the saved team as active team - result = await self.database.apply_team_configuration(player_id, source_slot) - - if result["success"]: - return { - "success": True, - "message": f"Successfully applied {config_name} as active team", - "source_slot": source_slot, - "pets_applied": len(team_data), - "previous_active_count": len(current_active) - } - else: - raise Exception(f"Failed to apply team configuration: {result.get('error', 'Unknown error')}") - - # Verify PIN and execute swap - result = await self.pin_service.verify_and_execute( - player_id=player_id, - pin_code=pin_code, - action_type="team_swap", - action_callback=apply_team_swap_callback - ) - - return result - - except Exception as e: - return {"success": False, "error": f"Team swap verification failed: {str(e)}"} - - async def get_individual_team_data(self, player_id: int, team_identifier: str) -> Dict: - """Get data for editing an individual team.""" - try: - if team_identifier == "active": - # Get active team - active_pets = await self.database.get_active_team(player_id) - return { - "success": True, - "team_type": "active", - "team_name": "Active Team", - "team_data": active_pets, - "is_active_team": True - } - else: - # Get saved team configuration - try: - slot = int(team_identifier) - if slot < 1 or slot > 3: - return {"success": False, "error": "Invalid team slot"} - except ValueError: - return {"success": False, "error": "Invalid team identifier"} - - config = await self.database.load_team_configuration(player_id, slot) - if config: - # Parse team_data - it should be a JSON string containing list of pets - try: - team_pets = json.loads(config["team_data"]) if config["team_data"] else [] - - # Ensure team_pets is a list (new format) - if isinstance(team_pets, list): - pets_data = team_pets - else: - # Handle old format - convert dict to list - pets_data = [] - if isinstance(team_pets, dict): - for position, pet_info in team_pets.items(): - if pet_info and 'pet_id' in pet_info: - # This is old format, we'll need to get full pet data - pet_data = await self._get_full_pet_data(player_id, pet_info['pet_id']) - if pet_data: - pet_data['team_order'] = int(position) - pets_data.append(pet_data) - - return { - "success": True, - "team_type": "saved", - "team_slot": slot, - "team_name": config["config_name"], - "pets": pets_data, # Use 'pets' key expected by webserver - "is_active_team": False, - "last_updated": config.get("updated_at") - } - except json.JSONDecodeError: - return { - "success": True, - "team_type": "saved", - "team_slot": slot, - "team_name": config["config_name"], - "pets": [], - "is_active_team": False, - "last_updated": config.get("updated_at") - } - else: - return { - "success": True, - "team_type": "saved", - "team_slot": slot, - "team_name": f"Team {slot}", - "pets": [], # Use 'pets' key expected by webserver - "is_active_team": False, - "last_updated": None - } - - except Exception as e: - return {"success": False, "error": f"Failed to get team data: {str(e)}"} - - async def _get_full_pet_data(self, player_id: int, pet_id: int) -> Optional[Dict]: - """Helper method to get full pet data for backward compatibility.""" - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.id, p.nickname, p.level, p.hp, p.max_hp, p.attack, p.defense, p.speed, p.happiness, - ps.name as species_name, ps.type1, ps.type2 - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.id = ? AND p.player_id = ? - """, (pet_id, player_id)) - row = await cursor.fetchone() - return dict(row) if row else None - except Exception as e: - print(f"Error getting full pet data: {e}") - return None - - async def save_individual_team( - self, - player_id: int, - nickname: str, - team_identifier: str, - team_data: Dict - ) -> Dict: - """Save changes to an individual team.""" - try: - if team_identifier == "active": - # Save to active team - action_type = "active_team_change" - action_data = { - "team_type": "active", - "team_data": team_data - } - else: - # Save to team configuration slot - try: - slot = int(team_identifier) - if slot < 1 or slot > 3: - return {"success": False, "error": "Invalid team slot"} - except ValueError: - return {"success": False, "error": "Invalid team identifier"} - - action_type = f"team_{slot}_change" - action_data = { - "team_type": "saved", - "team_slot": slot, - "team_data": team_data - } - - # Request PIN verification - pin_request = await self.pin_service.request_verification( - player_id=player_id, - nickname=nickname, - action_type=action_type, - action_data=action_data, - expiration_minutes=10 - ) - - return pin_request - - except Exception as e: - return {"success": False, "error": f"Failed to save individual team: {str(e)}"} - - async def verify_individual_team_save(self, player_id: int, pin_code: str, team_identifier: str) -> Dict: - """Verify PIN and save individual team changes.""" - try: - if team_identifier == "active": - action_type = "active_team_change" - else: - try: - slot = int(team_identifier) - action_type = f"team_{slot}_change" - except ValueError: - return {"success": False, "error": "Invalid team identifier"} - - # Define save callback - async def apply_individual_team_save_callback(player_id, action_data): - """Apply individual team save.""" - team_type = action_data["team_type"] - team_data = action_data["team_data"] - - if team_type == "active": - # Apply to active team - changes_applied = await self._apply_to_active_team(player_id, team_data) - return { - "success": True, - "message": "Active team updated successfully", - "changes_applied": changes_applied, - "team_type": "active" - } - else: - # Save to configuration slot - slot = action_data["team_slot"] - changes_applied = await self._save_team_configuration(player_id, slot, team_data) - return { - "success": True, - "message": f"Team {slot} configuration saved successfully", - "changes_applied": changes_applied, - "team_slot": slot, - "team_type": "saved" - } - - # Verify PIN and execute save - result = await self.pin_service.verify_and_execute( - player_id=player_id, - pin_code=pin_code, - action_type=action_type, - action_callback=apply_individual_team_save_callback - ) - - return result - - except Exception as e: - return {"success": False, "error": f"Individual team save verification failed: {str(e)}"} - - async def _apply_to_active_team(self, player_id: int, team_data: Dict) -> int: - """Apply team changes to active pets.""" - changes_count = 0 - - # Deactivate all pets - await self.database.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE player_id = ? - """, (player_id,)) - - # Activate selected pets - for pet_id, position in team_data.items(): - if position: - await self.database.execute(""" - UPDATE pets SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (position, int(pet_id), player_id)) - changes_count += 1 - - return changes_count - - async def _save_team_configuration(self, player_id: int, slot: int, team_data: Dict) -> int: - """Save team as configuration.""" - pets_list = [] - changes_count = 0 - - for pet_id, position in team_data.items(): - if position: - pet_info = await self.database.get_pet_by_id(pet_id) - if pet_info and pet_info["player_id"] == player_id: - # Create full pet data object in new format - pet_dict = { - 'id': pet_info['id'], - 'nickname': pet_info['nickname'] or pet_info.get('species_name', 'Unknown'), - 'level': pet_info['level'], - 'hp': pet_info.get('hp', 0), - 'max_hp': pet_info.get('max_hp', 0), - 'attack': pet_info.get('attack', 0), - 'defense': pet_info.get('defense', 0), - 'speed': pet_info.get('speed', 0), - 'happiness': pet_info.get('happiness', 0), - 'species_name': pet_info.get('species_name', 'Unknown'), - 'type1': pet_info.get('type1'), - 'type2': pet_info.get('type2'), - 'team_order': int(position) - } - pets_list.append(pet_dict) - changes_count += 1 - - # Save configuration in new list format - success = await self.database.save_team_configuration( - player_id, slot, f'Team {slot}', json.dumps(pets_list) - ) - - return changes_count if success else 0 \ No newline at end of file diff --git a/start_petbot.sh b/start_petbot.sh deleted file mode 100755 index b7fa441..0000000 --- a/start_petbot.sh +++ /dev/null @@ -1,203 +0,0 @@ -#!/bin/bash -# -# PetBot Startup Script -# Complete one-command startup for PetBot with all dependencies and validation -# -# Usage: ./start_petbot.sh -# - -set -e # Exit on any error - -echo "🐾 Starting PetBot..." -echo "====================" -echo "Version: $(date '+%Y-%m-%d %H:%M:%S') - Enhanced with startup validation" -echo "" - -# Get script directory (works even if called from elsewhere) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -echo "📁 Working directory: $SCRIPT_DIR" - -# Check if virtual environment exists -if [ ! -d "venv" ]; then - echo "❌ Virtual environment not found!" - echo "🔧 Creating virtual environment..." - python3 -m venv venv - echo "✅ Virtual environment created" -fi - -# Activate virtual environment -echo "🔄 Activating virtual environment..." -source venv/bin/activate - -# Upgrade pip to latest version -echo "🔄 Ensuring pip is up to date..." -pip install --upgrade pip -q - -# Check if requirements are installed -echo "🔄 Checking dependencies..." -if ! python -c "import aiosqlite, irc" 2>/dev/null; then - echo "📦 Installing/updating dependencies from requirements.txt..." - - # Verify requirements.txt exists - if [ ! -f "requirements.txt" ]; then - echo "❌ requirements.txt not found!" - echo "🔧 Please run install_prerequisites.sh first" - exit 1 - fi - - pip install -r requirements.txt - echo "✅ Dependencies installed" -else - echo "✅ Dependencies already satisfied" -fi - -# Verify core modules can be imported -echo "🔄 Verifying core modules..." -python3 -c " -import sys -sys.path.append('.') - -try: - from src.database import Database - from src.game_engine import GameEngine - from src.rate_limiter import RateLimiter - from src.irc_connection_manager import IRCConnectionManager - from config import ADMIN_USER, IRC_CONFIG, RATE_LIMIT_CONFIG - print('✅ Core modules verified') - print(f'ℹ️ Admin user: {ADMIN_USER}') - print(f'ℹ️ IRC Channel: {IRC_CONFIG[\"channel\"]}') - print(f'ℹ️ Rate limiting: {\"Enabled\" if RATE_LIMIT_CONFIG[\"enabled\"] else \"Disabled\"}') -except ImportError as e: - print(f'❌ Module import error: {e}') - print('💡 Try running: ./install_prerequisites.sh') - sys.exit(1) -" - -# Create required directories -echo "🔄 Creating required directories..." -mkdir -p data backups logs - -# Check configuration files -echo "🔄 Verifying configuration files..." -config_files=("config/pets.json" "config/locations.json" "config/items.json" "config/achievements.json" "config/gyms.json") -missing_configs=() - -for config_file in "${config_files[@]}"; do - if [ ! -f "$config_file" ]; then - missing_configs+=("$config_file") - fi -done - -if [ ${#missing_configs[@]} -gt 0 ]; then - echo "❌ Missing configuration files:" - for missing in "${missing_configs[@]}"; do - echo " - $missing" - done - echo "💡 These files are required for proper bot operation" - exit 1 -fi - -echo "✅ All configuration files present" - -# Database pre-flight check -if [ -f "data/petbot.db" ]; then - echo "🔄 Validating existing database..." - - # Quick database validation - python3 -c " -import sqlite3 -import sys - -try: - conn = sqlite3.connect('data/petbot.db') - cursor = conn.cursor() - - # Check essential tables exist - tables = ['players', 'pets', 'pet_species', 'locations'] - for table in tables: - cursor.execute(f'SELECT COUNT(*) FROM {table}') - count = cursor.fetchone()[0] - print(f' ✅ {table}: {count} records') - - conn.close() - print('✅ Database validation passed') - -except Exception as e: - print(f'❌ Database validation failed: {e}') - print('💡 Database may be corrupted - backup and recreate if needed') - sys.exit(1) -" -else - echo "ℹ️ First-time setup detected - database will be created automatically" -fi - -# Pre-startup system test -echo "🔄 Running pre-startup system test..." -python3 -c " -import sys -sys.path.append('.') - -try: - # Test basic imports and initialization - from run_bot_debug import PetBotDebug - - # Create a test bot instance to verify everything loads - print(' 🔧 Testing bot initialization...') - bot = PetBotDebug() - print(' ✅ Bot instance created successfully') - print('✅ Pre-startup test passed') - -except Exception as e: - print(f'❌ Pre-startup test failed: {e}') - import traceback - traceback.print_exc() - sys.exit(1) -" - -# Display comprehensive startup information -echo "" -echo "🚀 Launching PetBot with Enhanced Features..." -echo "=============================================" -echo "🌐 Web interface: http://localhost:8080" -echo "📱 Public access: http://petz.rdx4.com/" -echo "💬 IRC Server: irc.libera.chat" -echo "📢 IRC Channel: #petz" -echo "👤 Admin User: $(python3 -c 'from config import ADMIN_USER; print(ADMIN_USER)')" -echo "" -echo "🎮 Features Available:" -echo " ✅ 33 Pet Species with Emojis" -echo " ✅ 6 Exploration Locations" -echo " ✅ Team Builder with PIN Verification" -echo " ✅ Achievement System" -echo " ✅ Gym Battles" -echo " ✅ Weather System" -echo " ✅ Rate Limiting & Anti-Abuse" -echo " ✅ Auto-Reconnection" -echo " ✅ Startup Data Validation" -echo " ✅ Background Monitoring" -echo "" -echo "🔧 Technical Details:" -echo " 📊 Database: SQLite with validation" -echo " 🌐 Webserver: Integrated with bot instance" -echo " 🛡️ Security: Rate limiting enabled" -echo " 🔄 Reliability: Auto-reconnect on failure" -echo " 📈 Monitoring: Background validation every 30min" -echo "" -echo "Press Ctrl+C to stop the bot" -echo "=============================================" -echo "" - -# Launch the appropriate bot based on what's available -if [ -f "run_bot_with_reconnect.py" ]; then - echo "🚀 Starting with auto-reconnect support..." - exec python run_bot_with_reconnect.py -elif [ -f "run_bot_debug.py" ]; then - echo "🚀 Starting in debug mode..." - exec python run_bot_debug.py -else - echo "❌ No bot startup file found!" - echo "💡 Expected: run_bot_with_reconnect.py or run_bot_debug.py" - exit 1 -fi \ No newline at end of file diff --git a/webserver.py b/webserver.py index b6c29de..8b02c03 100644 --- a/webserver.py +++ b/webserver.py @@ -7,1718 +7,171 @@ Provides web interface for bot data including help, player stats, and pet collec import os import sys import asyncio -import json -import re -import hashlib -import random -from datetime import datetime, timedelta from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs from threading import Thread import time -import math # Add the project directory to the path sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.database import Database -from src.rate_limiter import RateLimiter, CommandCategory -from src.web_security import WebSecurity, escape_html, escape_js, escape_attr, safe_json -from src.pin_authentication import PinAuthenticationService class PetBotRequestHandler(BaseHTTPRequestHandler): """HTTP request handler for PetBot web server""" - # Class-level session storage - admin_sessions = {} - player_sessions = {} - - @property - def database(self): - """Get database instance from server""" - return self.server.database - - @property - def bot(self): - """Get bot instance from server""" - return getattr(self.server, 'bot', None) - - @property - def rate_limiter(self): - """Get rate limiter from bot instance""" - bot = self.bot - return getattr(bot, 'rate_limiter', None) if bot else None - - @property - def pin_service(self): - """Get PIN service from server""" - return getattr(self.server, 'pin_service', None) - - def get_client_ip(self): - """Get client IP address for rate limiting""" - # Check for X-Forwarded-For header (in case of proxy) - forwarded_for = self.headers.get('X-Forwarded-For') - if forwarded_for: - return forwarded_for.split(',')[0].strip() - - # Check for X-Real-IP header - real_ip = self.headers.get('X-Real-IP') - if real_ip: - return real_ip.strip() - - # Fallback to client address - return self.client_address[0] - - def check_rate_limit(self): - """Check rate limit for web requests""" - if not self.rate_limiter: - return True, None - - client_ip = self.get_client_ip() - # Use IP address as user identifier for web requests - user_identifier = f"web:{client_ip}" - - # Run async rate limit check - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - allowed, message = loop.run_until_complete( - self.rate_limiter.check_rate_limit(user_identifier, CommandCategory.WEB) - ) - return allowed, message - finally: - loop.close() - - def send_rate_limit_error(self, message): - """Send rate limit error response""" - self.send_response(429) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.send_header('Retry-After', '60') - self.add_security_headers() - self.end_headers() - - content = f""" - <!DOCTYPE html> - <html> - <head> - <title>Rate Limit Exceeded - PetBot - - - -
-

⛔ Rate Limit Exceeded

-
{message}
-
Please wait before making more requests.
-
- - - """ - self.wfile.write(content.encode()) - - 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.add_security_headers() - self.end_headers() - self.wfile.write(json.dumps(data).encode()) - - def add_security_headers(self): - """Add comprehensive HTTP security headers to all responses""" - # Content Security Policy - configured for PetBot's inline styles/scripts - csp_policy = ( - "default-src 'self'; " - "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " - "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: blob:; " - "font-src 'self'; " - "connect-src 'self'; " - "form-action 'self'; " - "frame-ancestors 'none'; " - "object-src 'none'; " - "base-uri 'self'" - ) - self.send_header('Content-Security-Policy', csp_policy) - - # Prevent clickjacking attacks - self.send_header('X-Frame-Options', 'DENY') - - # Prevent MIME type sniffing - self.send_header('X-Content-Type-Options', 'nosniff') - - # Enable XSS protection (legacy but still useful) - self.send_header('X-XSS-Protection', '1; mode=block') - - # Control referrer information - self.send_header('Referrer-Policy', 'strict-origin-when-cross-origin') - - # Only send HSTS if we detect HTTPS (check for forwarded protocol or direct HTTPS) - forwarded_proto = self.headers.get('X-Forwarded-Proto', '').lower() - if forwarded_proto == 'https' or getattr(self.connection, 'cipher', None): - # HSTS: Force HTTPS for 1 year, include subdomains - self.send_header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') - - # Additional security headers - self.send_header('X-Permitted-Cross-Domain-Policies', 'none') - self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') - self.send_header('Cross-Origin-Opener-Policy', 'same-origin') - self.send_header('Cross-Origin-Resource-Policy', 'same-origin') - - 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; - border-radius: 0 0 15px 15px; - } - - .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; - } - - /* IV Display Styles */ - .iv-section { - margin-top: 15px; - padding: 12px; - background: var(--bg-primary); - border-radius: 8px; - border: 1px solid var(--border-color); - } - - .iv-title { - font-size: 0.9em; - font-weight: bold; - color: var(--text-accent); - margin-bottom: 8px; - text-align: center; - } - - .iv-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 6px; - margin-bottom: 8px; - } - - .iv-stat { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.8em; - padding: 2px 0; - } - - .iv-value { - font-weight: bold; - padding: 2px 6px; - border-radius: 4px; - min-width: 28px; - text-align: center; - } - - .iv-perfect { background: #4caf50; color: white; } - .iv-excellent { background: #2196f3; color: white; } - .iv-good { background: #ff9800; color: white; } - .iv-fair { background: #ff5722; color: white; } - .iv-poor { background: #607d8b; color: white; } - - .iv-total { - text-align: center; - font-size: 0.85em; - padding-top: 8px; - border-top: 1px solid var(--border-color); - color: var(--text-secondary); - } - - .iv-total-value { - font-weight: bold; - color: var(--text-accent); - } - - /* 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; - } - - .iv-grid { - 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?sort=type", "🔷 by Type"), - ("petdex?sort=rarity", "⭐ by Rarity"), - ("petdex?sort=name", "🔤 by Name"), - ("petdex?sort=location", "🗺️ by Location"), - ("petdex?sort=all", "📋 Show All"), - ("petdex#search", "🔍 Search") - ]), - ("help", "📖 Help", [ - ("help", "📋 Commands"), - ("faq", "❓ 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}' - - # Add authentication status to navigation - auth_links = "" - authenticated_player = self.check_player_session() - if authenticated_player: - # Player is logged in - show profile link and logout - auth_links = f''' - ''' - else: - # No player logged in - show login link - auth_links = '🔐 Login' - - return f""" - - - - """ - - def get_page_template(self, title, content, current_page=""): - """Return complete page HTML with unified theme""" - safe_title = escape_html(title) - return f""" - - - - - {safe_title} - PetBot - - - - {self.get_navigation_bar(current_page)} -
- {content} -
- -""" - def do_GET(self): - """Handle GET requests with rate limiting""" - print(f"GET request: {self.path}") - - # Check rate limit first - allowed, rate_limit_message = self.check_rate_limit() - if not allowed: - self.send_rate_limit_error(rate_limit_message) - return - + """Handle GET requests""" parsed_path = urlparse(self.path) path = parsed_path.path - print(f"Parsed path: {path}") # Route handling if path == '/': self.serve_index() elif path == '/help': self.serve_help() - elif path == '/faq': - self.serve_faq() elif path == '/players': self.serve_players() - elif path.startswith('/player/') and path.endswith('/pets'): - # Handle /player/{nickname}/pets - must come before general /player/ route - nickname = path[8:-5] # Remove '/player/' prefix and '/pets' suffix - # Check authentication for player pets - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.redirect_to_login(f"Please log in to view {nickname}'s pets") - return - self.serve_player_pets(nickname) elif path.startswith('/player/'): nickname = path[8:] # Remove '/player/' prefix - # Check authentication for player profile - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.redirect_to_login(f"Please log in to view {nickname}'s profile") - return self.serve_player_profile(nickname) elif path == '/leaderboard': self.serve_leaderboard() elif path == '/locations': self.serve_locations() - elif path == '/petdex': - self.serve_petdex() - elif path.startswith('/teambuilder/') and '/config/load/' in path: - # Handle team configuration load: /teambuilder/{nickname}/config/load/{slot} - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - slot = parts[5] - self.handle_team_config_load(nickname, slot) - else: - self.send_error(400, "Invalid configuration load path") - elif path.startswith('/teambuilder/') and '/team/' in path: - # Handle individual team editor: /teambuilder/{nickname}/team/{slot} - parts = path.split('/') - if len(parts) >= 5: - nickname = parts[2] - team_identifier = parts[4] # Could be 1, 2, 3, or 'active' - # Check authentication for individual team editor - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.redirect_to_login(f"Please log in to access {nickname}'s team editor") - return - self.serve_individual_team_editor(nickname, team_identifier) - else: - self.send_error(400, "Invalid team editor path") - elif path.startswith('/teambuilder/'): - # Check if it's just the base teambuilder path (hub) - path_parts = path[13:].split('/') # Remove '/teambuilder/' prefix - if len(path_parts) == 1 and path_parts[0]: # Just nickname - nickname = path_parts[0] - # Check authentication for team builder - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.redirect_to_login(f"Please log in to access {nickname}'s team builder") - return - self.serve_team_selection_hub(nickname) - else: - self.send_error(404, "Invalid teambuilder path") - elif path.startswith('/testteambuilder/'): - nickname = path[17:] # Remove '/testteambuilder/' prefix - self.serve_test_teambuilder(nickname) - elif path == '/admin': - self.serve_admin_login() - elif path == '/admin/dashboard': - self.serve_admin_dashboard() - elif path == '/login': - self.serve_player_login() - elif path == '/admin/auth': - self.handle_admin_auth() - elif path == '/admin/verify': - self.handle_admin_verify() - elif path.startswith('/admin/api/'): - print(f"Admin API path detected in GET: {path}") - print(f"Extracted endpoint: {path[11:]}") - self.handle_admin_api(path[11:]) # Remove '/admin/api/' prefix else: - print(f"No route found for path: {path}") - self.send_error(404, "Page not found") - - def do_POST(self): - """Handle POST requests with rate limiting""" - # Check rate limit first (POST requests have stricter limits) - allowed, rate_limit_message = self.check_rate_limit() - if not allowed: - self.send_json_response({"success": False, "error": "Rate limit exceeded"}, 429) - return - - parsed_path = urlparse(self.path) - path = parsed_path.path - - if path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'): - # Handle individual team save: /teambuilder/{nickname}/team/{slot}/save - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - team_slot = parts[4] - self.handle_individual_team_save(nickname, team_slot) - else: - self.send_error(400, "Invalid individual team save path") - elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'): - # Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - team_slot = parts[4] - self.handle_individual_team_verify(nickname, team_slot) - else: - self.send_error(400, "Invalid individual team verify path") - elif path.startswith('/teambuilder/') and path.endswith('/save'): - nickname = path[13:-5] # Remove '/teambuilder/' prefix and '/save' suffix - # Check authentication for team save - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_team_save(nickname) - elif path.startswith('/teambuilder/') and path.endswith('/verify'): - nickname = path[13:-7] # Remove '/teambuilder/' prefix and '/verify' suffix - # Check authentication for team verify - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_team_verify(nickname) - elif path.startswith('/testteambuilder/') and path.endswith('/save'): - nickname = path[17:-5] # Remove '/testteambuilder/' prefix and '/save' suffix - self.handle_test_team_save(nickname) - elif path.startswith('/testteambuilder/') and path.endswith('/verify'): - nickname = path[17:-7] # Remove '/testteambuilder/' prefix and '/verify' suffix - self.handle_test_team_verify(nickname) - elif path.startswith('/player/') and '/pets/rename' in path: - # Handle pet rename request: /player/{nickname}/pets/rename - nickname = path.split('/')[2] - # Check authentication for pet rename - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_pet_rename_request(nickname) - elif path.startswith('/player/') and '/pets/verify' in path: - # Handle pet rename PIN verification: /player/{nickname}/pets/verify - nickname = path.split('/')[2] - # Check authentication for pet rename verify - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_pet_rename_verify(nickname) - elif path.startswith('/teambuilder/') and '/config/save/' in path: - # Handle team configuration save: /teambuilder/{nickname}/config/save/{slot} - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - slot = parts[5] - self.handle_team_config_save(nickname, slot) - else: - self.send_error(400, "Invalid configuration save path") - elif path.startswith('/teambuilder/') and '/config/load/' in path: - # Handle team configuration load: /teambuilder/{nickname}/config/load/{slot} - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - slot = parts[5] - self.handle_team_config_load(nickname, slot) - else: - self.send_error(400, "Invalid configuration load path") - elif path.startswith('/teambuilder/') and '/config/rename/' in path: - # Handle team configuration rename: /teambuilder/{nickname}/config/rename/{slot} - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - slot = parts[5] - self.handle_team_config_rename(nickname, slot) - else: - self.send_error(400, "Invalid configuration rename path") - elif path.startswith('/teambuilder/') and '/config/apply/' in path: - # Handle team configuration apply: /teambuilder/{nickname}/config/apply/{slot} - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - slot = parts[5] - self.handle_team_config_apply(nickname, slot) - else: - self.send_error(400, "Invalid configuration apply path") - elif path.startswith('/teambuilder/') and '/swap/' in path: - # Handle team swapping: /teambuilder/{nickname}/swap/{slot} - parts = path.split('/') - if len(parts) >= 5: - nickname = parts[2] - slot = parts[4] - self.handle_team_swap_request(nickname, slot) - else: - self.send_error(400, "Invalid team swap path") - elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/save'): - # Handle individual team save: /teambuilder/{nickname}/team/{slot}/save - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - team_slot = parts[4] - # Check authentication for individual team save - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_individual_team_save(nickname, team_slot) - else: - self.send_error(400, "Invalid individual team save path") - elif path.startswith('/teambuilder/') and '/team/' in path and path.endswith('/verify'): - # Handle individual team verify: /teambuilder/{nickname}/team/{slot}/verify - parts = path.split('/') - if len(parts) >= 6: - nickname = parts[2] - team_slot = parts[4] - # Check authentication for individual team verify - authenticated_player = self.check_player_session(nickname) - if not authenticated_player: - self.send_json_response({"success": False, "error": "Authentication required"}, 401) - return - self.handle_individual_team_verify(nickname, team_slot) - else: - self.send_error(400, "Invalid individual team verify path") - elif path == '/admin/auth': - self.handle_admin_auth() - elif path == '/admin/verify': - self.handle_admin_verify() - elif path == '/login/auth': - self.handle_player_auth() - elif path == '/login/verify': - self.handle_player_verify() - elif path == '/logout': - self.handle_player_logout() - elif path.startswith('/admin/api/'): - print(f"Admin API path detected: {path}") - print(f"Extracted endpoint: {path[11:]}") - self.handle_admin_api(path[11:]) # Remove '/admin/api/' prefix - else: - print(f"No route found for path: {path}") self.send_error(404, "Page not found") def serve_index(self): """Serve the main index page""" - content = """ -
-

🐾 PetBot Game Hub

-

Welcome to the PetBot web interface!

-

Connect to irc.libera.chat #petz to play

-
+ 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

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

📚 PetBot Commands

-

Complete guide to Pokemon-style pet collecting in IRC

-
- -
-
🚀 Getting Started
-
-
-
-
!start
-
Begin your pet collecting journey! Creates your trainer account and gives you your first starter pet.
-
Example: !start
-
-
-
!help
-
Get a link to this comprehensive command reference page.
-
Example: !help
-
-
-
!stats
-
View your basic trainer information including level, experience, and money.
-
Example: !stats
-
-
-
-
- -
-
🌍 Exploration & Travel
-
-
-
-
!explore
-
Search your current location for wild pets or items. You might find pets to battle/catch or discover useful items!
-
Example: !explore
-
-
-
!travel <location>
-
Move to a different location. Each area has unique pets and gyms. Some locations require achievements to unlock.
-
Example: !travel whispering woods
-
-
-
!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
-
Access your team builder web interface for drag-and-drop team management with PIN verification.
-
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
-
-
-
!nickname <pet> <new_name>
-
Give a custom nickname to one of your pets. Nicknames must be 20 characters or less.
-
Example: !nickname flamey FireStorm
-
-
-
-
- -
-
🎒 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 including rare Coin Pouches with 1-3 coins. 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
  • -
-
-
- -
-
⚡ Admin Commands ADMIN ONLY
-
-
-
-
!reload
-
Reload all bot modules without restarting. Useful for applying code changes.
-
Example: !reload
-
-
-
!weather [location|all]
-
Check current weather conditions in specific location or all locations.
-
Example: !weather Electric Canyon
-
-
-
!setweather <weather> [location] [duration]
-
Force change weather. Types: sunny, rainy, storm, blizzard, earthquake, calm
-
Example: !setweather storm all 60
-
-
-
!backup create [description]
-
Create manual database backup with optional description.
-
Example: !backup create "before update"
-
-
-
!rate_stats [user]
-
View rate limiting statistics for all users or specific user.
-
Example: !rate_stats username
-
-
-
!status / !uptime
-
Check bot connection status, uptime, and system health information.
-
Example: !status
-
-
-
!backups / !restore
-
List available backups or restore from backup. Use with caution!
-
Example: !backups
-
-
- -
- 🔒 Admin Access: These commands require administrator privileges and are restricted to authorized users only. -
-
-
- -
-
🌐 Web Interface
-
-
- Access detailed information through the web dashboard at http://petz.rdx4.com/ -
    -
  • Player Profiles - Complete stats, pet collections, and inventories with usage commands
  • -
  • Team Builder - Drag-and-drop team management with PIN verification
  • -
  • Enhanced Leaderboards - 8 categories: levels, experience, wealth, achievements, gym badges, rare pets
  • -
  • Locations Guide - All areas with spawn information and current weather
  • -
  • Gym Badges - Display your earned badges and battle progress
  • -
  • Inventory Management - Visual item display with command instructions
  • -
-
-
-
- - - """ - - # 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); - } - """ - - # Load help.html content and extract both CSS and body content + """Serve the help page""" try: - with open('help.html', 'r', encoding='utf-8') as f: - help_content = f.read() - - - # Extract CSS from help.html - css_match = re.search(r']*>(.*?)', help_content, re.DOTALL) - help_css = css_match.group(1) if css_match else "" - - # Extract body content (everything between tags) - body_match = re.search(r']*>(.*?)', help_content, re.DOTALL) - if body_match: - body_content = body_match.group(1) - # Remove the back link since we'll have the navigation bar - body_content = re.sub(r'.*?', '', body_content, flags=re.DOTALL) - else: - # Fallback: use original content if we can't parse it - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(help_content.encode()) - return - - # Create template with merged CSS - html_content = f""" - - - - - PetBot - Help & Commands - - - - {self.get_navigation_bar("help")} -
- {body_content} -
- -""" - + with open('help.html', 'r') as f: + content = f.read() self.send_response(200) self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(content.encode()) except FileNotFoundError: - self.serve_error_page("Help", "Help file not found") - except Exception as e: - self.serve_error_page("Help", f"Error loading help file: {str(e)}") - - def serve_faq(self): - """Serve the FAQ page using unified template""" - try: - with open('faq.html', 'r', encoding='utf-8') as f: - faq_content = f.read() - - - # Extract CSS from faq.html - css_match = re.search(r']*>(.*?)', faq_content, re.DOTALL) - faq_css = css_match.group(1) if css_match else "" - - # Extract body content (everything between tags) - body_match = re.search(r']*>(.*?)', faq_content, re.DOTALL) - if body_match: - body_content = body_match.group(1) - # Remove the back link since we'll have the navigation bar - body_content = re.sub(r'.*?', '', body_content, flags=re.DOTALL) - else: - # Fallback: use original content if we can't parse it - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(faq_content.encode()) - return - - # Create template with merged CSS - html_content = f""" - - - - - PetBot - FAQ - - - - {self.get_navigation_bar("faq")} -
- {body_content} -
- -""" - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - except FileNotFoundError: - self.serve_error_page("FAQ", "FAQ file not found") - except Exception as e: - self.serve_error_page("FAQ", f"Error loading FAQ file: {str(e)}") + self.send_error(404, "Help file not found") def serve_players(self): """Serve the players page with real data""" @@ -1786,55 +239,23 @@ 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 = "" for i, player in enumerate(players_data, 1): rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.") - safe_nickname = escape_attr(player['nickname']) - safe_nickname_display = escape_html(player['nickname']) - safe_location = escape_html(player.get('location_name', 'Unknown')) - players_html += f""" - + {rank_emoji} - {safe_nickname_display} + {player['nickname']} {player['level']} {player['experience']} ${player['money']} {player['pet_count']} {player['active_pets']} {player['achievement_count']} - {safe_location} + {player.get('location_name', 'Unknown')} """ else: players_html = """ @@ -1844,11 +265,175 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): """ - # Build table content - table_content = f""" -
-

🏆 Player Rankings

- + 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
+
+
@@ -1866,478 +451,99 @@ 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') - self.add_security_headers() self.end_headers() self.wfile.write(html.encode()) def serve_error_page(self, page_name, error_msg): - """Serve a generic error page using unified template""" - content = f""" -
-

⚠️ Error Loading {page_name}

-
+ """Serve a generic error page""" + html = f""" + + + + + PetBot - Error + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← Back to Game Hub + +
+

⚠️ Error Loading {page_name}

+
+ +
+

Unable to load page

+

{error_msg}

+

Please try again later or contact an administrator.

+
+ +""" self.send_response(500) self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_leaderboard(self): - """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.add_security_headers() - 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, escape_html(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, escape_html(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, escape_html(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, escape_html(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, escape_html(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, escape_html(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, escape_html(p['nickname']), f"Lvl {p['highest_pet_level']}", escape_html(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, escape_html(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 + """Serve the leaderboard page - redirect to players for now""" + # For now, leaderboard is the same as players page since they're ranked + # In the future, this could have different categories + self.send_response(302) # Temporary redirect + self.send_header('Location', '/players') + self.end_headers() def serve_locations(self): """Serve the locations page with real data""" @@ -2355,10 +561,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): asyncio.set_event_loop(loop) locations_data = loop.run_until_complete(self.fetch_locations_data(database)) - player_locations = loop.run_until_complete(self.fetch_player_locations(database)) loop.close() - self.serve_locations_data(locations_data, player_locations) + self.serve_locations_data(locations_data) except Exception as e: print(f"Error fetching locations data: {e}") @@ -2372,7 +577,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Get all locations cursor = await db.execute(""" SELECT l.*, - GROUP_CONCAT(DISTINCT ps.name || ' (' || ps.type1 || + GROUP_CONCAT(ps.name || ' (' || ps.type1 || CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns FROM locations l LEFT JOIN location_spawns ls ON l.id = ls.location_id @@ -2400,244 +605,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Database error fetching locations: {e}") return [] - async def fetch_player_locations(self, database): - """Fetch player locations for the interactive map""" - try: - import aiosqlite - async with aiosqlite.connect(database.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.nickname, p.current_location_id, l.name as location_name - FROM players p - JOIN locations l ON p.current_location_id = l.id - ORDER BY p.nickname - """) - - rows = await cursor.fetchall() - players = [] - for row in rows: - player_dict = { - 'nickname': row['nickname'], - 'location_id': row['current_location_id'], - 'location_name': row['location_name'] - } - players.append(player_dict) - return players - - except Exception as e: - print(f"Database error fetching player locations: {e}") - return [] - - def create_interactive_map(self, locations_data, player_locations): - """Create an interactive SVG map showing player locations""" - if not locations_data: - return "" - - # Define map layout - create a unique visual design - map_positions = { - 1: {"x": 200, "y": 400, "shape": "circle", "color": "#4CAF50"}, # Starter Town - central - 2: {"x": 100, "y": 200, "shape": "hexagon", "color": "#2E7D32"}, # Whispering Woods - forest - 3: {"x": 400, "y": 150, "shape": "diamond", "color": "#FF9800"}, # Thunder Peaks - mountain - 4: {"x": 550, "y": 300, "shape": "octagon", "color": "#795548"}, # Stone Caverns - cave - 5: {"x": 300, "y": 500, "shape": "star", "color": "#2196F3"}, # Frozen Lake - ice - 6: {"x": 500, "y": 450, "shape": "triangle", "color": "#F44336"} # Volcanic Crater - fire - } - - # Create player location groups - location_players = {} - for player in player_locations or []: - loc_id = player['location_id'] - if loc_id not in location_players: - location_players[loc_id] = [] - location_players[loc_id].append(player['nickname']) - - # SVG map content - svg_content = "" - - # Add connecting paths between locations - paths = [ - (1, 2), (1, 3), (1, 5), # Starter Town connections - (2, 5), (3, 4), (4, 6), (5, 6) # Other connections - ] - - for start, end in paths: - if start in map_positions and end in map_positions: - start_pos = map_positions[start] - end_pos = map_positions[end] - svg_content += f""" - - """ - - # Add location shapes - for location in locations_data: - loc_id = location['id'] - if loc_id not in map_positions: - continue - - pos = map_positions[loc_id] - players_here = location_players.get(loc_id, []) - player_count = len(players_here) - - # Create shape based on type - shape_svg = self.create_location_shape(pos, location, player_count) - svg_content += shape_svg - - # Add location label - svg_content += f""" - - {location['name']} - - """ - - # Add player names if any - if players_here: - player_text = ", ".join(players_here) - svg_content += f""" - - {player_text} - - """ - - return f""" -
-

🗺️ Interactive World Map

-

- Current player locations - shapes represent different terrain types -

- -
- - {svg_content} - -
- -
-
-
- Towns -
-
-
- Forests -
-
-
- Mountains -
-
-
- Caves -
-
-
- Ice Areas -
-
-
- Volcanic -
-
-
- """ - - def create_location_shape(self, pos, location, player_count): - """Create SVG shape for a location based on its type""" - x, y = pos['x'], pos['y'] - color = pos['color'] - shape = pos['shape'] - - # Add glow effect if players are present - glow = 'filter="url(#glow)"' if player_count > 0 else '' - - # Base size with scaling for player count - base_size = 25 + (player_count * 3) - - if shape == "circle": - return f""" - - - - - - - - - - - """ - elif shape == "hexagon": - points = [] - for i in range(6): - angle = i * 60 * math.pi / 180 - px = x + base_size * math.cos(angle) - py = y + base_size * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - elif shape == "diamond": - return f""" - - """ - elif shape == "triangle": - return f""" - - """ - elif shape == "star": - # Create 5-pointed star - points = [] - for i in range(10): - angle = i * 36 * math.pi / 180 - radius = base_size if i % 2 == 0 else base_size * 0.5 - px = x + radius * math.cos(angle) - py = y + radius * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - elif shape == "octagon": - points = [] - for i in range(8): - angle = i * 45 * math.pi / 180 - px = x + base_size * math.cos(angle) - py = y + base_size * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - else: - # Default to circle - return f""" - - """ - - def serve_locations_data(self, locations_data, player_locations=None): - """Serve locations page with real data using unified template""" + def serve_locations_data(self, locations_data): + """Serve locations page with real data""" # Build locations HTML locations_html = "" @@ -2647,28 +616,13 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if not spawns or spawns == 'None': spawns = "No pets spawn here yet" - # Split spawns into a readable list and remove duplicates - if spawns != "No pets spawn here yet": - spawn_list = list(set([spawn.strip() for spawn in spawns.split(',') if spawn.strip()])) - spawn_list.sort() # Sort alphabetically for consistency - else: - spawn_list = [] - + # Split spawns into a readable list + spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else [] spawn_badges = "" - visible_spawns = spawn_list[:6] # Show first 6 - hidden_spawns = spawn_list[6:] # Hide the rest - - # Add visible spawn badges - for spawn in visible_spawns: - spawn_badges += f'{spawn}' - - # Add hidden spawn badges (initially hidden) - if hidden_spawns: - location_id = location['id'] - for spawn in hidden_spawns: - spawn_badges += f'{spawn}' - # Add functional "show more" button - spawn_badges += f'+{len(hidden_spawns)} more' + for spawn in spawn_list[:6]: # Limit to first 6 for display + spawn_badges += f'{spawn.strip()}' + if len(spawn_list) > 6: + spawn_badges += f'+{len(spawn_list) - 6} more' if not spawn_badges: spawn_badges = 'No pets spawn here yet' @@ -2701,214 +655,119 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
""" - # Create interactive map HTML - map_html = self.create_interactive_map(locations_data, player_locations) + html = f""" + + + + + PetBot - Locations + tag - html_content = html_content.replace("", additional_css + "") + }} + + + + ← 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 +

+
+ +""" self.send_response(200) self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - def serve_petdex(self): - """Serve the petdex page with all pet species data""" - # Get database instance from the server class - database = self.server.database if hasattr(self.server, 'database') else None - - if not database: - self.serve_error_page("Petdex", "Database not available") - return - - # Parse URL parameters for sorting - parsed_url = urlparse(self.path) - query_params = parse_qs(parsed_url.query) - sort_mode = query_params.get('sort', ['rarity'])[0] # Default to rarity - search_query = query_params.get('search', [''])[0] # Default to empty search - - # Fetch petdex data - try: - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - petdex_data = loop.run_until_complete(self.fetch_petdex_data(database)) - loop.close() - - self.serve_petdex_data(petdex_data, sort_mode, search_query) - - except Exception as e: - print(f"Error fetching petdex data: {e}") - self.serve_error_page("Petdex", f"Error loading petdex: {str(e)}") - - async def fetch_petdex_data(self, database): - """Fetch all pet species data from database""" - try: - import aiosqlite - async with aiosqlite.connect(database.db_path) as db: - # Get all pet species with evolution information (no duplicates) - cursor = await db.execute(""" - SELECT DISTINCT ps.id, ps.name, ps.type1, ps.type2, ps.base_hp, ps.base_attack, - ps.base_defense, ps.base_speed, ps.evolution_level, ps.evolution_species_id, - ps.rarity, ps.emoji, - evolve_to.name as evolves_to_name, - (SELECT COUNT(*) FROM location_spawns ls WHERE ls.species_id = ps.id) as location_count - FROM pet_species ps - LEFT JOIN pet_species evolve_to ON ps.evolution_species_id = evolve_to.id - ORDER BY ps.rarity ASC, ps.name ASC - """) - - rows = await cursor.fetchall() - pets = [] - for row in rows: - pet_dict = { - 'id': row[0], 'name': row[1], 'type1': row[2], 'type2': row[3], - 'base_hp': row[4], 'base_attack': row[5], 'base_defense': row[6], - 'base_speed': row[7], 'evolution_level': row[8], - 'evolution_species_id': row[9], 'rarity': row[10], 'emoji': row[11], - 'evolves_to_name': row[12], 'location_count': row[13] - } - pets.append(pet_dict) - - # Get spawn locations for each pet - for pet in pets: - cursor = await db.execute(""" - SELECT l.name, ls.min_level, ls.max_level, ls.spawn_rate - FROM location_spawns ls - JOIN locations l ON ls.location_id = l.id - WHERE ls.species_id = ? - ORDER BY l.name ASC - """, (pet['id'],)) - spawn_rows = await cursor.fetchall() - pet['spawn_locations'] = [] - for spawn_row in spawn_rows: - spawn_dict = { - 'location_name': spawn_row[0], 'min_level': spawn_row[1], - 'max_level': spawn_row[2], 'spawn_rate': spawn_row[3] - } - pet['spawn_locations'].append(spawn_dict) - - return pets - - except Exception as e: - print(f"Database error fetching petdex: {e}") - return [] - - def remove_pet_duplicates(self, pets_list): - """Remove duplicate pets based on ID and sort by name""" - seen_ids = set() - unique_pets = [] - for pet in pets_list: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - return sorted(unique_pets, key=lambda x: x['name']) - - def serve_petdex_data(self, petdex_data, sort_mode='rarity', search_query=''): - """Serve petdex page with all pet species data""" - - # Remove duplicates from input data first - petdex_data = self.remove_pet_duplicates(petdex_data) - - # Apply search filter if provided - if search_query: - search_query = search_query.lower() - filtered_data = [] - for pet in petdex_data: - # Search in name, type1, type2 - if (search_query in pet['name'].lower() or - search_query in pet['type1'].lower() or - (pet['type2'] and search_query in pet['type2'].lower())): - filtered_data.append(pet) - petdex_data = filtered_data - - # Build pet cards HTML grouped by rarity - rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"} - rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"} - - # Calculate statistics - total_species = len(petdex_data) - type_counts = {} - for pet in petdex_data: - if pet['type1'] not in type_counts: - type_counts[pet['type1']] = 0 - type_counts[pet['type1']] += 1 - if pet['type2'] and pet['type2'] not in type_counts: - type_counts[pet['type2']] = 0 - if pet['type2']: - type_counts[pet['type2']] += 1 - - # Build statistics section - stats_content = f""" -
-
-

📊 Total Species

-
{total_species}
-
-
-

🎨 Types

-
{len(type_counts)}
-
-
-

⭐ Rarities

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

🧬 Evolutions

-
{len([p for p in petdex_data if p['evolution_level']])}
-
-
- """ - - # Sort and group pets based on sort_mode - petdex_html = "" - total_species = len(petdex_data) - - if sort_mode == 'type': - # Group by type1 - pets_by_type = {} - for pet in petdex_data: - pet_type = pet['type1'] - if pet_type not in pets_by_type: - pets_by_type[pet_type] = [] - # Check for duplicates within this type - if pet not in pets_by_type[pet_type]: - pets_by_type[pet_type].append(pet) - - # Sort each type group by name and remove any remaining duplicates - for type_name in pets_by_type: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_type[type_name]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_type[type_name] = sorted(unique_pets, key=lambda x: x['name']) - - type_colors = { - 'Fire': '#F08030', 'Water': '#6890F0', 'Grass': '#78C850', 'Electric': '#F8D030', - 'Psychic': '#F85888', 'Ice': '#98D8D8', 'Dragon': '#7038F8', 'Dark': '#705848', - 'Fighting': '#C03028', 'Poison': '#A040A0', 'Ground': '#E0C068', 'Flying': '#A890F0', - 'Bug': '#A8B820', 'Rock': '#B8A038', 'Ghost': '#705898', 'Steel': '#B8B8D0', - 'Normal': '#A8A878', 'Fairy': '#EE99AC' - } - - for type_name in sorted(pets_by_type.keys()): - pets_in_type = pets_by_type[type_name] - type_color = type_colors.get(type_name, '#A8A878') - - petdex_html += f""" -
-

- {type_name} Type ({len(pets_in_type)} species) -

-
""" - - for pet in pets_in_type: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

- {type_str} -
-
-
- HP: - {pet['base_hp']} -
-
- Attack: - {pet['base_attack']} -
-
- Defense: - {pet['base_defense']} -
-
- Speed: - {pet['base_speed']} -
-
-
- - {'★' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
-
""" - - petdex_html += """ -
-
""" - - elif sort_mode == 'name': - # Sort alphabetically by name (duplicates already removed) - sorted_pets = sorted(petdex_data, key=lambda x: x['name']) - - petdex_html += f""" -
-

- All Species (A-Z) ({len(sorted_pets)} total) -

-
""" - - for pet in sorted_pets: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

- {type_str} -
-
-
- HP: - {pet['base_hp']} -
-
- Attack: - {pet['base_attack']} -
-
- Defense: - {pet['base_defense']} -
-
- Speed: - {pet['base_speed']} -
-
-
- - {'★' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
-
""" - - petdex_html += """ -
-
""" - - elif sort_mode == 'location': - # Group by spawn locations - pets_by_location = {} - pets_no_location = [] - - for pet in petdex_data: - if pet['spawn_locations']: - for location in pet['spawn_locations']: - loc_name = location['location_name'] - if loc_name not in pets_by_location: - pets_by_location[loc_name] = [] - # Check for duplicates within this location - if pet not in pets_by_location[loc_name]: - pets_by_location[loc_name].append(pet) - else: - pets_no_location.append(pet) - - # Sort each location group by name and remove any remaining duplicates - for location_name in pets_by_location: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_location[location_name]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_location[location_name] = sorted(unique_pets, key=lambda x: x['name']) - - location_colors = { - 'Starter Town': '#4CAF50', - 'Whispering Woods': '#2E7D32', - 'Thunder Peaks': '#FF9800', - 'Stone Caverns': '#795548', - 'Frozen Lake': '#2196F3', - 'Volcanic Crater': '#F44336' - } - - for location_name in sorted(pets_by_location.keys()): - pets_in_location = pets_by_location[location_name] - location_color = location_colors.get(location_name, '#A8A878') - - petdex_html += f""" -
-

- 🗺️ {location_name} ({len(pets_in_location)} species) -

-
""" - - for pet in pets_in_location: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - # Get level range for this location - level_range = "" - for location in pet['spawn_locations']: - if location['location_name'] == location_name: - level_range = f"Lv.{location['min_level']}-{location['max_level']}" - break - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

- {type_str} -
-
-
- HP: - {pet['base_hp']} -
-
- Attack: - {pet['base_attack']} -
-
- Defense: - {pet['base_defense']} -
-
- Speed: - {pet['base_speed']} -
-
-
- - 📍 {level_range} | {'★' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
-
""" - - petdex_html += """ -
-
""" - - # Add pets with no location at the end (remove duplicates) - if pets_no_location: - seen_ids = set() - unique_no_location = [] - for pet in pets_no_location: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_no_location.append(pet) - pets_no_location = sorted(unique_no_location, key=lambda x: x['name']) - - if pets_no_location: - petdex_html += f""" -
-

- ❓ Unknown Locations ({len(pets_no_location)} species) -

-
""" - - for pet in pets_no_location: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

- {type_str} -
-
-
- HP: - {pet['base_hp']} -
-
- Attack: - {pet['base_attack']} -
-
- Defense: - {pet['base_defense']} -
-
- Speed: - {pet['base_speed']} -
-
-
- - ❓ Location Unknown | {'★' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
-
""" - - petdex_html += """ -
-
""" - - elif sort_mode == 'all': - # Show all pets in a grid format without grouping (duplicates already removed) - sorted_pets = sorted(petdex_data, key=lambda x: (x['rarity'], x['name'])) - - petdex_html += f""" -
-

- 📋 All Pet Species ({len(sorted_pets)} total) -

-
""" - - for pet in sorted_pets: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - # Get all spawn locations - location_text = "" - if pet['spawn_locations']: - locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})" - for loc in pet['spawn_locations'][:2]] - if len(pet['spawn_locations']) > 2: - locations.append(f"+{len(pet['spawn_locations']) - 2} more") - location_text = f"📍 {', '.join(locations)}" - else: - location_text = "📍 Location Unknown" - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

- {type_str} -
-
-
- HP: - {pet['base_hp']} -
-
- Attack: - {pet['base_attack']} -
-
- Defense: - {pet['base_defense']} -
-
- Speed: - {pet['base_speed']} -
-
-
- - {location_text} - -
-
- - {'★' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
-
""" - - petdex_html += """ -
-
""" - - else: # Default to rarity sorting - pets_by_rarity = {} - for pet in petdex_data: - rarity = pet['rarity'] - if rarity not in pets_by_rarity: - pets_by_rarity[rarity] = [] - # Check for duplicates within this rarity - if pet not in pets_by_rarity[rarity]: - pets_by_rarity[rarity].append(pet) - - # Sort each rarity group by name and remove any remaining duplicates - for rarity in pets_by_rarity: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_rarity[rarity]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_rarity[rarity] = sorted(unique_pets, key=lambda x: x['name']) - - for rarity in sorted(pets_by_rarity.keys()): - pets_in_rarity = pets_by_rarity[rarity] - rarity_name = rarity_names.get(rarity, f"Rarity {rarity}") - rarity_color = rarity_colors.get(rarity, "#ffffff") - - petdex_html += f""" -
-

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

-
""" - - for pet in pets_in_rarity: - # Build type display - type_str = pet['type1'] - if pet['type2']: - type_str += f"/{pet['type2']}" - - # Build evolution info - evolution_info = "" - if pet['evolution_level'] and pet['evolves_to_name']: - evolution_info = f"
Evolves: Level {pet['evolution_level']} → {pet['evolves_to_name']}" - elif pet['evolution_level']: - evolution_info = f"
Evolves: Level {pet['evolution_level']}" - - # Build spawn locations - spawn_info = "" - if pet['spawn_locations']: - locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})" - for loc in pet['spawn_locations'][:3]] - if len(pet['spawn_locations']) > 3: - locations.append(f"+{len(pet['spawn_locations']) - 3} more") - spawn_info = f"
Found in: {', '.join(locations)}" - else: - spawn_info = "
Found in: Not yet available" - - # Calculate total base stats - total_stats = pet['base_hp'] + pet['base_attack'] + pet['base_defense'] + pet['base_speed'] - - petdex_html += f""" -
-
-

{pet.get('emoji', '🐾')} {pet['name']}

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

No pet species found!

-

The petdex appears to be empty. Contact an administrator.

-
""" - - # Create search interface - search_interface = f""" - - """ - - # Determine header text based on sort mode - if sort_mode == 'type': - header_text = "📊 Pet Collection by Type" - description = "🔷 Pets are organized by their primary type. Each type has different strengths and weaknesses!" - elif sort_mode == 'name': - header_text = "📊 Pet Collection (A-Z)" - description = "🔤 All pets sorted alphabetically by name. Perfect for finding specific species!" - elif sort_mode == 'location': - header_text = "📊 Pet Collection by Location" - description = "🗺️ Pets are organized by where they can be found. Use !travel <location> to visit these areas!" - elif sort_mode == 'all': - header_text = "📊 Complete Pet Collection" - description = "📋 All pets displayed in a comprehensive grid view with locations and stats!" - else: - header_text = "📊 Pet Collection by Rarity" - description = "🌟 Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!" - - # Combine all content - content = f""" -
-

📖 Petdex

-

Complete encyclopedia of all available pets

-
- - {stats_content} - - {search_interface} - -
-

{header_text}

-

{description}

- - {petdex_html} -
- """ - - html = self.get_page_template("Petdex", content, "petdex") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() self.wfile.write(html.encode()) @@ -3681,7 +865,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Get player info import aiosqlite async with aiosqlite.connect(database.db_path) as db: - db.row_factory = aiosqlite.Row # Get player basic info cursor = await db.execute(""" SELECT p.*, l.name as location_name, l.description as location_desc @@ -3693,19 +876,40 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if not player: return None - # Convert Row to dict - player_dict = dict(player) + # Convert to dict manually + player_dict = { + 'id': player[0], + 'nickname': player[1], + 'created_at': player[2], + 'last_active': player[3], + 'level': player[4], + 'experience': player[5], + 'money': player[6], + 'current_location_id': player[7], + 'location_name': player[8], + 'location_desc': player[9] + } # Get player pets cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji + SELECT p.*, ps.name as species_name, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? ORDER BY p.is_active DESC, p.level DESC, p.id ASC """, (player_dict['id'],)) pets_rows = await cursor.fetchall() - pets = [dict(row) for row in pets_rows] + pets = [] + for row in pets_rows: + pet_dict = { + 'id': row[0], 'player_id': row[1], 'species_id': row[2], + 'nickname': row[3], 'level': row[4], 'experience': row[5], + 'hp': row[6], 'max_hp': row[7], 'attack': row[8], + 'defense': row[9], 'speed': row[10], 'happiness': row[11], + 'caught_at': row[12], 'is_active': row[13], + 'species_name': row[14], 'type1': row[15], 'type2': row[16] + } + pets.append(pet_dict) # Get player achievements cursor = await db.execute(""" @@ -3716,7 +920,13 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): ORDER BY pa.completed_at DESC """, (player_dict['id'],)) achievements_rows = await cursor.fetchall() - achievements = [dict(row) for row in achievements_rows] + achievements = [] + for row in achievements_rows: + achievement_dict = { + 'id': row[0], 'player_id': row[1], 'achievement_id': row[2], + 'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5] + } + achievements.append(achievement_dict) # Get player inventory cursor = await db.execute(""" @@ -3727,754 +937,168 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): ORDER BY i.rarity DESC, i.name ASC """, (player_dict['id'],)) inventory_rows = await cursor.fetchall() - inventory = [dict(row) for row in inventory_rows] - - # Get player gym badges - cursor = await db.execute(""" - SELECT g.name, g.badge_name, g.badge_icon, l.name as location_name, - pgb.victories, pgb.first_victory_date, pgb.highest_difficulty - FROM player_gym_battles pgb - JOIN gyms g ON pgb.gym_id = g.id - JOIN locations l ON g.location_id = l.id - WHERE pgb.player_id = ? AND pgb.victories > 0 - ORDER BY pgb.first_victory_date ASC - """, (player_dict['id'],)) - gym_badges_rows = await cursor.fetchall() - gym_badges = [dict(row) for row in gym_badges_rows] - - # Get player encounters using database method - encounters = [] - try: - # Use the existing database method which handles row factory properly - temp_encounters = await database.get_player_encounters(player_dict['id']) - for enc in temp_encounters: - encounter_dict = { - 'species_name': enc['species_name'], - 'type1': enc['type1'], - 'type2': enc['type2'], - 'rarity': enc['rarity'], - 'total_encounters': enc['total_encounters'], - 'caught_count': enc['caught_count'], - 'first_encounter_date': enc['first_encounter_date'] - } - encounters.append(encounter_dict) - except Exception as e: - print(f"Error fetching encounters: {e}") - encounters = [] - - # Get encounter stats - try: - encounter_stats = await database.get_encounter_stats(player_dict['id']) - except Exception as e: - print(f"Error fetching encounter stats: {e}") - encounter_stats = { - 'species_encountered': 0, - 'total_encounters': 0, - 'total_species': 0, - 'completion_percentage': 0.0 + inventory = [] + for row in inventory_rows: + item_dict = { + 'name': row[0], 'description': row[1], 'category': row[2], + 'rarity': row[3], 'quantity': row[4] } + inventory.append(item_dict) return { 'player': player_dict, 'pets': pets, 'achievements': achievements, - 'inventory': inventory, - 'gym_badges': gym_badges, - 'encounters': encounters, - 'encounter_stats': encounter_stats + 'inventory': inventory } except Exception as e: print(f"Database error fetching player {nickname}: {e}") return None - def serve_player_pets(self, nickname): - """Serve pet management page for a player""" - try: - # Get player data using database method directly - player = asyncio.run(self.database.get_player(nickname)) - if not player: - self.serve_player_not_found(nickname) - return - - # Get player pets for management - player_pets = asyncio.run(self.database.get_player_pets_for_rename(player['id'])) - - # Render the pets management page - self.serve_pets_management_interface(nickname, player_pets) - - except Exception as e: - print(f"Error serving player pets page: {e}") - self.serve_player_error(nickname, str(e)) - - def get_iv_grade(self, iv_value): - """Get color grade for IV value""" - if iv_value >= 27: # 27-31 (Perfect) - return "perfect" - elif iv_value >= 21: # 21-26 (Excellent) - return "excellent" - elif iv_value >= 15: # 15-20 (Good) - return "good" - elif iv_value >= 10: # 10-14 (Fair) - return "fair" - else: # 0-9 (Poor) - return "poor" - - def serve_pets_management_interface(self, nickname, pets): - """Serve the pet management interface""" - if not pets: - self.serve_no_pets_error(nickname) - return - - # Generate pet cards - pet_cards = [] - for pet in pets: - status_badge = "" - if pet.get('is_active'): - team_order = pet.get('team_order', 0) - if team_order > 0: - status_badge = f'Team #{team_order}' - else: - status_badge = 'Active' - else: - status_badge = 'Storage' - - fainted_badge = "" - if pet.get('fainted_at'): - fainted_badge = '💀 Fainted' - - current_name = escape_html(pet.get('nickname') or pet.get('species_name')) - pet_id = pet.get('id') - safe_species = escape_html(pet.get('species_name')) - - pet_card = f""" -
-
-
-
{pet.get('emoji', '🐾')} {current_name}
-
Level {pet.get('level', 1)} {safe_species}
-
-
- {status_badge} - {fainted_badge} -
-
- -
-
- HP: - {pet.get('hp', 0)}/{pet.get('max_hp', 0)} -
-
- ATK: - {pet.get('attack', 0)} -
-
- DEF: - {pet.get('defense', 0)} -
-
- SPD: - {pet.get('speed', 0)} -
-
- -
-
- Individual Values (IVs) - ℹ️ -
-
-
- HP: - {pet.get('iv_hp', 15)} -
-
- ATK: - {pet.get('iv_attack', 15)} -
-
- DEF: - {pet.get('iv_defense', 15)} -
-
- SPD: - {pet.get('iv_speed', 15)} -
-
-
- Total IV: - {pet.get('iv_hp', 15) + pet.get('iv_attack', 15) + pet.get('iv_defense', 15) + pet.get('iv_speed', 15)}/124 -
-
- -
- -
- - -
- """ - pet_cards.append(pet_card) - - pets_html = "".join(pet_cards) - - content = f""" -
-

🐾 My Pets - {nickname}

-

Manage your pet collection and customize their names

-
- -
- {pets_html} -
- -
- ← Back to Profile -
-

💡 Tips:

-
    -
  • Click "Rename" to change a pet's nickname
  • -
  • You'll receive a PIN via IRC for security
  • -
  • PIN expires in 15 seconds
  • -
  • Names must be unique among your pets
  • -
-
-
- - - - - - - - """ - - page_html = self.get_page_template("My Pets - " + nickname, content, "pets") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(page_html.encode()) - - def serve_no_pets_error(self, nickname): - """Serve error page when player has no pets""" - content = f""" -
-

🐾 No Pets Found

-
- -
-

You don't have any pets yet!

-

Start your journey by using !start in #petz to get your first pet.

-

Then explore locations and catch more pets with !explore and !catch.

-
- - - """ - - page_html = self.get_page_template("No Pets - " + nickname, content, "pets") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(page_html.encode()) - def serve_player_not_found(self, nickname): - """Serve player not found page using unified template""" - content = f""" -
-

🚫 Player Not Found

-
+ """Serve player not found page""" + html = f""" + + + + + PetBot - Player Not Found + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← 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!

+
+ +""" self.send_response(404) self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_player_error(self, nickname, error_msg): - """Serve player error page using unified template""" - safe_error_msg = escape_html(error_msg) - content = f""" -
-

⚠️ Error

-
+ """Serve player error page""" + html = f""" + + + + + PetBot - Error + ", additional_css + "") + border: 2px solid #ff4444; + }} + + + + ← Back to Game Hub + +
+

⚠️ Error

+
+ +
+

Unable to load player data

+

{error_msg}

+

Please try again later or contact an administrator.

+
+ +""" self.send_response(500) self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() - self.wfile.write(html_content.encode()) + self.wfile.write(html.encode()) def serve_player_data(self, nickname, player_data): """Serve player profile page with real data""" @@ -4482,9 +1106,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): pets = player_data['pets'] achievements = player_data['achievements'] inventory = player_data.get('inventory', []) - gym_badges = player_data.get('gym_badges', []) - encounters = player_data.get('encounters', []) - encounter_stats = player_data.get('encounter_stats', {}) # Calculate stats active_pets = [pet for pet in pets if pet['is_active']] @@ -4497,18 +1118,17 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): for pet in pets: status = "⭐ Active" if pet['is_active'] else "📦 Storage" status_class = "pet-active" if pet['is_active'] else "pet-stored" - name = escape_html(pet['nickname'] or pet['species_name']) - species = escape_html(pet['species_name']) + name = pet['nickname'] or pet['species_name'] - type_str = escape_html(pet['type1']) + type_str = pet['type1'] if pet['type2']: - type_str += f"/{escape_html(pet['type2'])}" + type_str += f"/{pet['type2']}" pets_html += f""" {status} - {pet.get('emoji', '🐾')} {name} - {species} + {name} + {pet['species_name']} {type_str} {pet['level']} {pet['hp']}/{pet['max_hp']} @@ -4527,20 +1147,15 @@ 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 @@ -4567,5183 +1182,104 @@ 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()}
-
💬 Use with: !use {item['name']}
+
+ {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 - badges_html = "" - if gym_badges: - for badge in gym_badges: - # Safely handle date formatting - try: - if badge['first_victory_date'] and isinstance(badge['first_victory_date'], str): - badge_date = badge['first_victory_date'].split()[0] - else: - badge_date = 'Unknown' - except (AttributeError, IndexError): - badge_date = 'Unknown' - badges_html += f""" -
-
{badge['badge_icon']}
-
-

{badge['badge_name']}

-

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

-
- First victory: {badge_date} - Total victories: {badge['victories']} - Highest difficulty: Level {badge['highest_difficulty']} -
-
-
""" - else: - badges_html = """ -
-
🏆
-

No gym badges yet

-

Challenge gyms to earn badges and prove your training skills!

-
""" + html = f""" + + + + + PetBot - {nickname}'s Profile + ", additional_css + "") - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - def log_message(self, format, *args): - """Override to reduce logging noise""" - pass - - def serve_teambuilder(self, nickname): - """Serve the team builder interface""" - from urllib.parse import unquote - nickname = unquote(nickname) - - try: - from src.database import Database - database = Database() - - # Get event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - player_data = loop.run_until_complete(self.fetch_player_data(database, nickname)) - - if player_data is None: - self.serve_player_not_found(nickname) - return - - pets = player_data['pets'] - if not pets: - self.serve_teambuilder_no_pets(nickname) - return - - self.serve_teambuilder_interface(nickname, pets) - - except Exception as e: - print(f"Error loading team builder for {nickname}: {e}") - self.serve_player_error(nickname, f"Error loading team builder: {str(e)}") - - def serve_teambuilder_no_pets(self, nickname): - """Show message when player has no pets using unified template""" - content = f""" -
-

🐾 Team Builder

-

Build your perfect team for battles and adventures

-
- -
-

🐾 No Pets Found

-

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

-

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

- ← Back to Profile -
- """ - - # Add no-pets-specific CSS - additional_css = """ - .main-container { - text-align: center; - max-width: 800px; - margin: 0 auto; - } - - .no-pets-message { - background: var(--bg-secondary); - padding: 40px; - border-radius: 15px; - border: 2px solid var(--warning-color); - margin-top: 30px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - } - - .no-pets-message h2 { - color: var(--warning-color); - margin-top: 0; - } - """ - - html_content = self.get_page_template(f"Team Builder - {nickname}", content, "players") - html_content = html_content.replace("", additional_css + "") - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - def serve_teambuilder_interface(self, nickname, pets): - """Serve the full interactive team builder interface""" - active_pets = [pet for pet in pets if pet['is_active']] - inactive_pets = [pet for pet in pets if not pet['is_active']] - - # Get team configurations for team selection interface - import asyncio - from src.database import Database - database = Database() - - # Get event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Get player and team configurations - player = loop.run_until_complete(database.get_player(nickname)) - team_configs = [] - current_active_slot = 1 # Default - if player: - team_configs = loop.run_until_complete(database.get_player_team_configurations(player['id'])) - current_active_slot = loop.run_until_complete(database.get_active_team_slot(player['id'])) - - # Debug logging - print(f"Team Builder Debug for {nickname}:") - print(f"Total pets: {len(pets)}") - active_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in active_pets] - inactive_names = [f"{pet['nickname'] or pet['species_name']} (ID:{pet['id']}, is_active:{pet['is_active']})" for pet in inactive_pets] - print(f"Active pets: {len(active_pets)} - {active_names}") - print(f"Inactive pets: {len(inactive_pets)} - {inactive_names}") - - # Generate detailed pet cards with debugging - def make_pet_card(pet, is_active): - name = escape_html(pet['nickname'] or pet['species_name']) - raw_name = pet['nickname'] or pet['species_name'] # Keep for logging - status = "Active" if is_active else "Storage" - status_class = "active" if is_active else "storage" - type_str = escape_html(pet['type1']) - if pet['type2']: - type_str += f"/{escape_html(pet['type2'])}" - - # Get emoji for the pet species - emoji = pet.get('emoji', '🐾') # Default to paw emoji if none specified - - # Debug logging - print(f"Making pet card for {raw_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""" -
-
-

{emoji} {name}

-
{status}
-
-
Level {pet['level']} {escape_html(pet['species_name'])}
-
{type_str}
- -
-
HP: {pet['hp']}/{pet['max_hp']}
-
-
-
-
- -
-
- ATK - {pet['attack']} -
-
- DEF - {pet['defense']} -
-
- SPD - {pet['speed']} -
-
- EXP - {pet['experience']} -
-
- -
- {'😊' if pet['happiness'] > 70 else '😐' if pet['happiness'] > 40 else '😞'} - Happiness: {pet['happiness']}/100 -
-
""" - - # Create 6 numbered slots and place pets in their positions - team_slots = [''] * 6 # Initialize 6 empty slots - - # Place active pets in their team_order positions - for pet in active_pets: - team_order = pet.get('team_order') - if team_order and 1 <= team_order <= 6: - team_slots[team_order - 1] = make_pet_card(pet, True) - - storage_cards = ''.join(make_pet_card(pet, False) for pet in inactive_pets) - - # Old template removed - using new unified template system below - - # 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

-

Choose a team to edit, then drag pets between Active Team and Storage.

-
- - -
-

Select Team to Edit

-
- -
-
- -
-
-

⚔️ 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 -
- - - """ - - # Generate team cards HTML - print(f"Debug: Generating team cards for {len(team_configs)} configs") - team_cards_html = "" - - # If no team configs exist, create default slots with Team 1 showing current active team - if not team_configs: - print("Debug: No team configs found, creating default empty slots") - for slot in range(1, 4): - # For Team 1, show current active pets - if slot == 1: - pet_previews = "" - active_count = 0 - for pos in range(1, 7): - # Find pet in position pos - pet_in_slot = next((pet for pet in active_pets if pet.get('team_order') == pos), None) - if pet_in_slot: - pet_name = pet_in_slot['nickname'] or pet_in_slot['species_name'] - pet_emoji = pet_in_slot.get('emoji', '🐾') - pet_previews += f'
{pet_emoji} {pet_name}
' - active_count += 1 - else: - pet_previews += '
Empty
' - - status_text = f"{active_count}/6 pets" if active_count > 0 else "Empty team" - else: - # Teams 2 and 3 are empty by default - pet_previews = '
Empty
' * 6 - status_text = "Empty team" - - active_class = "active" if slot == current_active_slot else "" - is_active = slot == current_active_slot - - team_cards_html += f''' -
-
-

Team {slot}

- {status_text} -
-
- {pet_previews} -
-
- - {'🟢 Active Team' if is_active else f''} -
-
- ''' - else: - for config in team_configs: - print(f"Debug: Processing config: {config}") - pet_previews = "" - - # Special handling for Team 1 - show current active team instead of saved config - if config['slot'] == 1: - active_count = 0 - for pos in range(1, 7): - # Find pet in position pos from current active team - pet_in_slot = next((pet for pet in active_pets if pet.get('team_order') == pos), None) - if pet_in_slot: - pet_name = pet_in_slot['nickname'] or pet_in_slot['species_name'] - pet_emoji = pet_in_slot.get('emoji', '🐾') - pet_previews += f'
{pet_emoji} {pet_name}
' - active_count += 1 - else: - pet_previews += '
Empty
' - status_text = f"{active_count}/6 pets" if active_count > 0 else "Empty team" - else: - # For Teams 2 and 3, use saved configuration data - if config['team_data']: - for pos in range(1, 7): - if str(pos) in config['team_data'] and config['team_data'][str(pos)]: - pet_info = config['team_data'][str(pos)] - pet_emoji = pet_info.get('emoji', '🐾') - pet_previews += f'
{pet_emoji} {pet_info["name"]}
' - else: - pet_previews += '
Empty
' - else: - pet_previews = '
Empty
' * 6 - status_text = f"{config['pet_count']}/6 pets" if config['pet_count'] > 0 else "Empty team" - - active_class = "active" if config['slot'] == current_active_slot else "" - is_active = config['slot'] == current_active_slot - - team_cards_html += f''' -
-
-

{config['name']}

- {status_text} -
-
- {pet_previews} -
-
- - {'🟢 Active Team' if is_active else f''} -
-
- ''' - - # Replace placeholder with actual team cards - team_builder_content = team_builder_content.replace('', team_cards_html) - - # 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.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - def serve_test_teambuilder(self, nickname): - """Serve the test team builder interface with simplified team management""" - from urllib.parse import unquote - nickname = unquote(nickname) - - - try: - from src.database import Database - database = Database() - - # Get event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - player_data = loop.run_until_complete(self.fetch_player_data(database, nickname)) - - if player_data is None: - self.serve_player_not_found(nickname) - return - - pets = player_data['pets'] - if not pets: - self.serve_test_teambuilder_no_pets(nickname) - return - - self.serve_test_teambuilder_interface(nickname, pets) - - except Exception as e: - print(f"Error loading test team builder for {nickname}: {e}") - self.serve_player_error(nickname, f"Error loading test team builder: {str(e)}") - - def serve_test_teambuilder_no_pets(self, nickname): - """Show message when player has no pets using unified template""" - content = f""" -
-

🐾 Test Team Builder

-

Simplified team management (Test Version)

-
- -
-

🐾 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 -
- """ - - html_content = self.get_page_template(f"Test Team Builder - {nickname}", content, "") - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - def serve_test_teambuilder_interface(self, nickname, pets): - """Serve the simplified test team builder interface""" - # Get team configurations for this player - import asyncio - - try: - from src.database import Database - database = Database() - - # Get event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - print(f"Debug: Getting player data for {nickname}") - # Get player info - player = loop.run_until_complete(database.get_player(nickname)) - if not player: - self.serve_player_error(nickname, "Player not found") - return - - print(f"Debug: Player found: {player}") - # Get team configurations - team_configs = loop.run_until_complete(database.get_player_team_configurations(player['id'])) - print(f"Debug: Team configs: {team_configs}") - - # Create the simplified interface - print(f"Debug: Creating content with {len(pets)} pets") - # TEMPORARY: Use simple content to test - content = f""" -

Test Team Builder - {nickname}

-

Found {len(pets)} pets and {len(team_configs)} team configs

-

First pet: {pets[0]['nickname'] if pets else 'No pets'}

- """ - print("Debug: Content created successfully") - - html_content = self.get_page_template(f"Test Team Builder - {nickname}", content, "") - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html_content.encode()) - - except Exception as e: - print(f"Error in serve_test_teambuilder_interface: {e}") - self.serve_player_error(nickname, f"Error loading interface: {str(e)}") - - def create_test_teambuilder_content(self, nickname, pets, team_configs): - """Create the simplified test team builder HTML content""" - import json - - # Pre-process pets data for JavaScript - escape user data - pets_data = [] - for pet in pets: - pets_data.append({ - 'id': pet['id'], - 'name': pet['nickname'], # Will be safely encoded by safe_json() - 'level': pet['level'], - 'type_primary': pet['type1'], - 'rarity': 1 - }) - pets_json = safe_json(pets_data) - - # Build team cards for each configuration - team_cards_html = "" - for config in team_configs: - pet_previews = "" - if config['team_data']: - for pos in range(1, 7): - if str(pos) in config['team_data'] and config['team_data'][str(pos)]: - pet_info = config['team_data'][str(pos)] - pet_previews += f'
{pet_info["name"]}
' - else: - pet_previews += '
Empty
' - else: - pet_previews = '
No pets assigned
' - - status_text = f"{config['pet_count']}/6 pets" if config['pet_count'] > 0 else "Empty team" - - team_cards_html += f''' -
-
-

{config['name']}

- {status_text} -
-
- {pet_previews} -
-
- -
-
- ''' - - return f''' - - -
-
-

🐾 Test Team Builder

-

Choose a team to edit, make changes, and save with PIN verification

-
- -
-

Choose Team to Edit

-

Select one of your 3 teams to edit. Each team can have up to 6 pets.

- -
- {team_cards_html} -
-
- -
-
-

Editing Team 1

- -
- -
- -
- -
- -
- -
-

🔐 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:

- - -
-
-
-
- - - ''' - - def handle_team_save(self, nickname): - """Handle team save request and generate PIN""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - # Parse JSON data - import json - try: - team_data = json.loads(post_data) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_save_async(nickname, team_data)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_team_save: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_save_async(self, nickname, save_data): - """Async handler for team save""" - try: - # Extract team data and slot from new structure - if isinstance(save_data, dict) and 'teamData' in save_data: - team_data = save_data['teamData'] - team_slot = save_data.get('teamSlot', 1) # Default to slot 1 - else: - # Backwards compatibility - old format - team_data = save_data - team_slot = 1 - - # Validate team slot - if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: - return {"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"} - - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Validate team composition - validation = await self.database.validate_team_composition(player["id"], team_data) - if not validation["valid"]: - return {"success": False, "error": validation["error"]} - - # Use unified PIN service to request verification - change_data = { - 'teamData': team_data, - 'teamSlot': team_slot - } - - result = await self.pin_service.request_verification( - player_id=player["id"], - nickname=nickname, - action_type="team_change", - action_data=change_data, - expiration_minutes=10 - ) - - if result["success"]: - return { - "success": True, - "message": "PIN sent to your IRC private messages", - "expires_in_minutes": result["expires_in_minutes"] - } - else: - return {"success": False, "error": result.get("error", "Failed to generate PIN")} - - except Exception as e: - print(f"Error in _handle_team_save_async: {e}") - return {"success": False, "error": str(e)} - - def handle_team_verify(self, nickname): - """Handle PIN verification and apply team changes""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No PIN provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - # Parse JSON data - import json - try: - data = json.loads(post_data) - pin_code = data.get("pin", "").strip() - except (json.JSONDecodeError, AttributeError): - self.send_json_response({"success": False, "error": "Invalid data format"}, 400) - return - - if not pin_code: - self.send_json_response({"success": False, "error": "PIN code is required"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_verify_async(nickname, pin_code)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_team_verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_verify_async(self, nickname, pin_code): - """Async handler for PIN verification""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Define callback to apply team changes - async def apply_team_changes(player_id, action_data): - # Validate team composition again before applying - validation = await self.database.validate_team_composition(player_id, action_data['teamData']) - if not validation["valid"]: - raise Exception(validation["error"]) - - # Apply the team changes directly using database operations - import aiosqlite - team_changes = action_data['teamData'] - team_slot = action_data.get('teamSlot', 1) - - # Validate team slot - if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: - raise Exception("Invalid team slot. Must be 1, 2, or 3") - - # Apply team changes atomically - async with aiosqlite.connect(self.database.db_path) as db: - try: - # Begin transaction - await db.execute("BEGIN TRANSACTION") - - if team_slot == 1: - # Team 1: Apply directly to active team (immediate effect) - # First, deactivate all pets for this player - await db.execute(""" - UPDATE pets SET is_active = FALSE, team_order = NULL - WHERE player_id = ? - """, (player_id,)) - - # Then activate and position the selected pets - for pet_id, position in team_changes.items(): - if position: # If position is a number (1-6), pet is active - await db.execute(""" - UPDATE pets SET is_active = TRUE, team_order = ? - WHERE id = ? AND player_id = ? - """, (position, int(pet_id), player_id)) - - changes_applied = sum(1 for pos in team_changes.values() if pos) - - else: - # Teams 2-3: Save as configuration - import json - - # Get pet details for the configuration - pets_list = [] - for pet_id, position in team_changes.items(): - if position: - cursor = await db.execute(""" - SELECT nickname, species_name, level, happiness - FROM pets WHERE id = ? AND player_id = ? - """, (int(pet_id), player_id)) - pet_row = await cursor.fetchone() - - if pet_row: - pet_dict = { - "id": int(pet_id), - "nickname": pet_row[0], - "species": pet_row[1], - "level": pet_row[2], - "happiness": pet_row[3], - "position": position - } - pets_list.append(pet_dict) - - # Save configuration - await db.execute(""" - INSERT OR REPLACE INTO team_configurations - (player_id, slot_number, config_name, team_data, updated_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - """, (player_id, team_slot, f"Team {team_slot}", json.dumps(pets_list))) - - changes_applied = len(pets_list) - - await db.commit() - return { - "success": True, - "changes_applied": changes_applied, - "team_slot": team_slot - } - - except Exception as e: - await db.execute("ROLLBACK") - raise Exception(f"Database error: {str(e)}") - - # Use unified PIN service to verify and execute - result = await self.pin_service.verify_and_execute( - player_id=player["id"], - pin_code=pin_code, - action_type="team_change", - action_callback=apply_team_changes - ) - - if result["success"]: - callback_result = result.get("callback_result", {}) - changes_applied = callback_result.get("changes_applied", 0) if callback_result else 0 - return { - "success": True, - "message": f"Team changes applied successfully! {changes_applied} pets updated.", - "changes_applied": changes_applied - } - else: - return {"success": False, "error": result.get("error", "PIN verification failed")} - - except Exception as e: - print(f"Error in _handle_team_verify_async: {e}") - return {"success": False, "error": str(e)} - - - def handle_team_config_save(self, nickname, slot): - """Handle saving team configuration to a slot""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - # Parse JSON data - import json - try: - data = json.loads(post_data) - config_name = data.get("name", f"Team Config {slot}") - team_data = data.get("team", []) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Validate slot number - try: - slot_num = int(slot) - if slot_num < 1 or slot_num > 3: - self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) - return - except ValueError: - self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_config_save_async(nickname, slot_num, config_name, team_data)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_team_config_save: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_config_save_async(self, nickname, slot_num, config_name, team_data): - """Async handler for team configuration save""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Save configuration - import json - success = await self.database.save_team_configuration( - player["id"], slot_num, config_name, json.dumps(team_data) - ) - - if success: - return {"success": True, "message": f"Team configuration '{config_name}' saved to slot {slot_num}"} - else: - return {"success": False, "error": "Failed to save team configuration"} - - except Exception as e: - print(f"Error in _handle_team_config_save_async: {e}") - return {"success": False, "error": str(e)} - - def handle_team_config_load(self, nickname, slot): - """Handle loading team configuration from a slot""" - try: - # Validate slot number - try: - slot_num = int(slot) - if slot_num < 1 or slot_num > 3: - self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) - return - except ValueError: - self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_config_load_async(nickname, slot_num)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) - - except Exception as e: - print(f"Error in handle_team_config_load: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_config_load_async(self, nickname, slot_num): - """Async handler for team configuration load""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Load configuration - config = await self.database.load_team_configuration(player["id"], slot_num) - - if config: - import json - team_data = json.loads(config["team_data"]) - return { - "success": True, - "config_name": config["config_name"], - "team_data": team_data, - "updated_at": config["updated_at"] - } - else: - return {"success": False, "error": f"No team configuration found in slot {slot_num}"} - - except Exception as e: - print(f"Error in _handle_team_config_load_async: {e}") - return {"success": False, "error": str(e)} - - def handle_team_config_rename(self, nickname, slot): - """Handle renaming team configuration in a slot""" - try: - # Validate slot number - try: - slot_num = int(slot) - if slot_num < 1 or slot_num > 3: - self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) - return - except ValueError: - self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) - return - - # Get the new name from request body - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - - import json - try: - data = json.loads(post_data.decode('utf-8')) - new_name = data.get('new_name', '').strip() - - if not new_name: - self.send_json_response({"success": False, "error": "Configuration name cannot be empty"}, 400) - return - - if len(new_name) > 50: - self.send_json_response({"success": False, "error": "Configuration name too long (max 50 characters)"}, 400) - return - - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_config_rename_async(nickname, slot_num, new_name)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) - - except Exception as e: - print(f"Error in handle_team_config_rename: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_config_rename_async(self, nickname, slot_num, new_name): - """Async handler for team configuration rename""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Check if configuration exists in the slot - existing_config = await self.database.load_team_configuration(player["id"], slot_num) - if not existing_config: - return {"success": False, "error": f"No team configuration found in slot {slot_num}"} - - # Rename the configuration - success = await self.database.rename_team_configuration(player["id"], slot_num, new_name) - - if success: - return { - "success": True, - "message": f"Configuration renamed to '{new_name}'", - "new_name": new_name - } - else: - return {"success": False, "error": "Failed to rename configuration"} - - except Exception as e: - print(f"Error in _handle_team_config_rename_async: {e}") - return {"success": False, "error": str(e)} - - def handle_team_config_apply(self, nickname, slot): - """Handle applying team configuration to active team""" - try: - # Validate slot number - try: - slot_num = int(slot) - if slot_num < 1 or slot_num > 3: - self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) - return - except ValueError: - self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_config_apply_async(nickname, slot_num)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) - - except Exception as e: - print(f"Error in handle_team_config_apply: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_config_apply_async(self, nickname, slot_num): - """Async handler for team configuration apply""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Apply the team configuration - result = await self.database.apply_team_configuration(player["id"], slot_num) - return result - - except Exception as e: - print(f"Error in _handle_team_config_apply_async: {e}") - return {"success": False, "error": str(e)} - - def handle_team_swap_request(self, nickname, slot): - """Handle team swap request to change active team""" - try: - # Validate slot number - try: - slot_num = int(slot) - if slot_num < 1 or slot_num > 3: - self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) - return - except ValueError: - self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_team_swap_async(nickname, slot_num)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) - - except Exception as e: - print(f"Error in handle_team_swap_request: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_team_swap_async(self, nickname, slot_num): - """Async handler for team swapping""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Set the new active team slot - result = await self.database.set_active_team_slot(player["id"], slot_num) - return result - - except Exception as e: - print(f"Error in _handle_team_swap_async: {e}") - return {"success": False, "error": str(e)} - - def handle_test_team_save(self, nickname): - """Handle test team builder save request and generate PIN""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - team_slot = data.get('team_slot') - team_data = data.get('team_data', {}) - - # Validate team slot - if not isinstance(team_slot, int) or team_slot < 1 or team_slot > 3: - self.send_json_response({"success": False, "error": "Invalid team slot. Must be 1, 2, or 3"}, 400) - return - - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_test_team_save_async(nickname, team_slot, team_data)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_test_team_save: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_test_team_save_async(self, nickname, team_slot, team_data): - """Async handler for test team save""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Generate PIN and store pending change - import json - team_json = json.dumps(team_data) - config_name = f"Team {team_slot}" - - result = await self.database.save_team_configuration( - player["id"], team_slot, config_name, team_json - ) - - if result: - # Generate PIN for verification (using existing PIN system) - pin_result = await self.database.create_team_change_pin( - player["id"], team_json - ) - - if pin_result["success"]: - # Send PIN to IRC - if hasattr(self.server, 'bot') and self.server.bot: - self.server.bot.send_private_message( - nickname, - f"🔐 Team {team_slot} Save PIN: {pin_result['pin_code']} (expires in 10 minutes)" - ) - - return {"success": True, "message": "PIN sent to IRC"} - else: - return {"success": False, "error": "Failed to generate PIN"} - else: - return {"success": False, "error": "Failed to save team configuration"} - - except Exception as e: - print(f"Error in _handle_test_team_save_async: {e}") - return {"success": False, "error": str(e)} - - def handle_test_team_verify(self, nickname): - """Handle test team builder PIN verification""" - try: - # Get PIN from request body - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - - import json - try: - data = json.loads(post_data.decode('utf-8')) - pin_code = data.get('pin', '').strip() - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid request data"}, 400) - return - - if not pin_code or len(pin_code) != 6: - self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400) - return - - # Run async verification - import asyncio - result = asyncio.run(self._handle_test_team_verify_async(nickname, pin_code)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_test_team_verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_test_team_verify_async(self, nickname, pin_code): - """Async handler for test team PIN verification""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Verify PIN - result = await self.database.verify_team_change_pin(player["id"], pin_code) - - if result["success"]: - return {"success": True, "message": "Team configuration saved successfully!"} - else: - return {"success": False, "error": result.get("error", "Invalid PIN")} - - except Exception as e: - print(f"Error in _handle_test_team_verify_async: {e}") - return {"success": False, "error": str(e)} - - def handle_individual_team_save(self, nickname, team_slot): - """Handle individual team save request and generate PIN""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - team_identifier = data.get('team_identifier', team_slot) - is_active_team = data.get('is_active_team', False) - pets = data.get('pets', []) - - # Validate team slot - if team_slot not in ['1', '2', '3', 'active']: - self.send_json_response({"success": False, "error": "Invalid team slot"}, 400) - return - - # Convert team_slot to numeric for database operations - if team_slot == 'active': - team_slot_num = 1 - is_active_team = True - else: - team_slot_num = int(team_slot) - - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_individual_team_save_async(nickname, team_slot_num, pets, is_active_team)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_individual_team_save: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_individual_team_save_async(self, nickname, team_slot, pets, is_active_team): - """Async handler for individual team save""" - try: - # Get player - player = await self.server.database.get_player(nickname) - - if not player: - return {"success": False, "error": "Player not found"} - - # Validate pets exist and belong to player - if pets: - player_pets = await self.server.database.get_player_pets(player['id']) - player_pet_ids = [pet['id'] for pet in player_pets] - - for pet_data in pets: - pet_id = pet_data.get('pet_id') - if not pet_id: - continue - - # Convert pet_id to int for comparison with database IDs - try: - pet_id_int = int(pet_id) - except (ValueError, TypeError): - return {"success": False, "error": WebSecurity.sanitize_error_message("Invalid pet ID", pet_id)} - - # Check if pet belongs to player - if pet_id_int not in player_pet_ids: - return {"success": False, "error": WebSecurity.sanitize_error_message("Pet {} not found or doesn't belong to you", str(pet_id))} - - # Convert pets array to the expected format for database - # Expected format: {"pet_id": position, "pet_id": position, ...} - team_changes = {} - if pets: # Ensure pets is not None or empty - for pet_data in pets: - if isinstance(pet_data, dict): # Ensure pet_data is a dictionary - pet_id = str(pet_data.get('pet_id')) # Ensure pet_id is string - position = pet_data.get('position', False) # Position or False for inactive - if pet_id and pet_id != 'None': # Only add valid pet IDs - team_changes[pet_id] = position - - - # Generate PIN and store pending change - import json - team_data = { - 'teamSlot': int(team_slot), # Convert to int and use expected key name - 'teamData': team_changes, # Use the dictionary format expected by database - 'is_active_team': is_active_team - } - - # Use unified PIN service for team changes - pin_result = await self.server.pin_service.request_verification( - player_id=player["id"], - nickname=nickname, - action_type="team_change", - action_data=team_data - ) - - return {"success": True, "requires_pin": True} - - except Exception as e: - print(f"Error in _handle_individual_team_save_async: {e}") - return {"success": False, "error": str(e)} - - def handle_individual_team_verify(self, nickname, team_slot): - """Handle individual team PIN verification""" - try: - # Get PIN from request body - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - - import json - try: - data = json.loads(post_data.decode('utf-8')) - pin_code = data.get('pin', '').strip() - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid request data"}, 400) - return - - if not pin_code or len(pin_code) != 6: - self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400) - return - - # Run async verification - import asyncio - result = asyncio.run(self._handle_individual_team_verify_async(nickname, pin_code)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_individual_team_verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_individual_team_verify_async(self, nickname, pin_code): - """Async handler for individual team PIN verification""" - try: - # Get player - player = await self.server.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Verify PIN and apply changes using simplified method - result = await self.server.database.apply_individual_team_change(player["id"], pin_code) - - if result["success"]: - return {"success": True, "message": "Team saved successfully!"} - else: - return {"success": False, "error": result.get("error", "Invalid PIN")} - - except Exception as e: - print(f"Error in _handle_individual_team_verify_async: {e}") - return {"success": False, "error": str(e)} - - def handle_pet_rename_request(self, nickname): - """Handle pet rename request and generate PIN""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - try: - result = asyncio.run(self._handle_pet_rename_request_async(nickname, data)) - except Exception as async_error: - print(f"Async error in pet rename: {async_error}") - self.send_json_response({"success": False, "error": f"Async error: {str(async_error)}"}, 500) - return - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_pet_rename_request: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_pet_rename_request_async(self, nickname, data): - """Async handler for pet rename request""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Validate required fields - if "pet_id" not in data or "new_nickname" not in data: - return {"success": False, "error": "Missing pet_id or new_nickname"} - - pet_id = data["pet_id"] - new_nickname = data["new_nickname"] - - # Validate nickname (same validation as database method) - if not new_nickname or len(new_nickname.strip()) == 0: - return {"success": False, "error": "Pet nickname cannot be empty"} - - new_nickname = new_nickname.strip() - if len(new_nickname) > 20: - return {"success": False, "error": "Pet nickname must be 20 characters or less"} - - # Check for profanity/inappropriate content (basic check) - inappropriate_words = ["admin", "bot", "system", "null", "undefined"] - if any(word in new_nickname.lower() for word in inappropriate_words): - return {"success": False, "error": "Pet nickname contains inappropriate content"} - - # Verify pet ownership - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute(""" - SELECT nickname FROM pets WHERE id = ? AND player_id = ? - """, (pet_id, player["id"])) - pet_row = await cursor.fetchone() - - if not pet_row: - return {"success": False, "error": "Pet not found or not owned by player"} - - # Use unified PIN service to request verification - rename_data = { - "pet_id": pet_id, - "new_nickname": new_nickname, - "old_nickname": pet_row[0] - } - - result = await self.pin_service.request_verification( - player_id=player["id"], - nickname=nickname, - action_type="pet_rename", - action_data=rename_data, - expiration_minutes=5 # Shorter expiration for pet renames - ) - - if result["success"]: - return { - "success": True, - "message": f"PIN sent to {nickname} via IRC. Check your messages!" - } - else: - return {"success": False, "error": result.get("error", "Failed to generate PIN")} - - except Exception as e: - print(f"Error in _handle_pet_rename_request_async: {e}") - return {"success": False, "error": str(e)} - - def handle_pet_rename_verify(self, nickname): - """Handle PIN verification for pet rename""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - # Run async operations - import asyncio - result = asyncio.run(self._handle_pet_rename_verify_async(nickname, data)) - - if result["success"]: - self.send_json_response(result, 200) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_pet_rename_verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _handle_pet_rename_verify_async(self, nickname, data): - """Async handler for pet rename PIN verification""" - try: - # Get player - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Validate required field - if "pin" not in data: - return {"success": False, "error": "Missing PIN"} - - pin_code = data["pin"] - - # Define callback to apply pet rename - async def apply_pet_rename(player_id, action_data): - pet_id = action_data["pet_id"] - new_nickname = action_data["new_nickname"] - - # Verify pet ownership again before applying - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - # Check if pet still exists and belongs to player - cursor = await db.execute(""" - SELECT id FROM pets WHERE id = ? AND player_id = ? - """, (pet_id, player_id)) - pet_row = await cursor.fetchone() - - if not pet_row: - raise Exception("Pet not found or no longer owned by player") - - # Apply the rename - await db.execute(""" - UPDATE pets SET nickname = ? WHERE id = ? AND player_id = ? - """, (new_nickname, pet_id, player_id)) - - await db.commit() - - return {"new_nickname": new_nickname, "pet_id": pet_id} - - # Use unified PIN service to verify and execute - result = await self.pin_service.verify_and_execute( - player_id=player["id"], - pin_code=pin_code, - action_type="pet_rename", - action_callback=apply_pet_rename - ) - - if result["success"]: - callback_result = result.get("callback_result", {}) - new_nickname = callback_result.get("new_nickname", "Unknown") - return { - "success": True, - "message": f"Pet renamed to '{new_nickname}' successfully!", - "new_nickname": new_nickname - } - else: - return {"success": False, "error": result.get("error", "PIN verification failed")} - - except Exception as e: - print(f"Error in _handle_pet_rename_verify_async: {e}") - return {"success": False, "error": str(e)} - - - def serve_admin_login(self): - """Serve the admin login page""" - import sys - sys.path.append('.') - from config import ADMIN_USER - - content = """ -
-

🔐 Admin Control Panel

-

Authorized access only

-
- -
-

Authentication Required

-

This area is restricted to bot administrators.

- -
-
- - -
- - - -
- A PIN will be sent to your IRC private messages -
-
- - - -
-
- - - - - """ - - html = self.get_page_template("Admin Login", content, "") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html.encode()) - - def serve_player_login(self): - """Serve the player login page""" - content = """ -
-

🔐 Player Login

-

Access your PetBot profile

-
- -
-

Login to Your Profile

-

Enter your IRC nickname to receive a login PIN via private message.

- -
-
- - -
- - - -
- A PIN will be sent to your IRC private messages -
- -
- Don't have an account? Type !start in IRC to register! -
-
- - - -
-
- - - - - """ - - html = self.get_page_template("Player Login", content, "") - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.add_security_headers() - self.end_headers() - self.wfile.write(html.encode()) - - def handle_admin_auth(self): - """Handle admin authentication request and generate PIN""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - nickname = data.get('nickname', '').strip() - - # Verify admin user - import sys - sys.path.append('.') - from config import ADMIN_USER - - if nickname.lower() != ADMIN_USER.lower(): - self.send_json_response({"success": False, "error": "Access denied"}, 403) - return - - # Use a special admin_user_id (-1) for admin authentication with PIN service - admin_user_id = -1 - - # Use unified PIN service to request verification - import asyncio - result = asyncio.run(self._request_admin_pin_async(admin_user_id, nickname)) - - if result["success"]: - self.send_json_response({"success": True, "message": "PIN sent via IRC"}) - else: - self.send_json_response({"success": False, "error": result.get("error", "Failed to generate PIN")}, 500) - - except Exception as e: - print(f"Error in handle_admin_auth: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _request_admin_pin_async(self, admin_user_id, nickname): - """Request admin PIN using unified PIN service""" - try: - # Use unified PIN service with special admin action type - result = await self.pin_service.request_verification( - player_id=admin_user_id, # Special ID for admin - nickname=nickname, - action_type="admin_auth", - action_data={"admin_nickname": nickname}, - expiration_minutes=15 - ) - return result - except Exception as e: - return {"success": False, "error": f"Failed to request admin PIN: {str(e)}"} - - - def handle_admin_verify(self): - """Handle admin PIN verification""" - try: - # Get POST data - content_length = int(self.headers.get('Content-Length', 0)) - if content_length == 0: - self.send_json_response({"success": False, "error": "No data provided"}, 400) - return - - post_data = self.rfile.read(content_length).decode('utf-8') - - try: - import json - data = json.loads(post_data) - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - return - - nickname = data.get('nickname', '').strip() - pin = data.get('pin', '').strip() - - # Use special admin_user_id (-1) for admin authentication - admin_user_id = -1 - - # Define callback to create admin session - def create_admin_session(player_id, action_data): - admin_nickname = action_data["admin_nickname"] - - # Create session token - import hashlib - import time - session_token = hashlib.sha256(f"{admin_nickname}:{pin}:{time.time()}".encode()).hexdigest() - - # Store session - self.admin_sessions[session_token] = { - 'nickname': admin_nickname, - 'expires': time.time() + (60 * 60) # 1 hour session - } - - return {"session_token": session_token, "admin_nickname": admin_nickname} - - # Verify PIN using unified PIN service - import asyncio - result = asyncio.run(self._verify_admin_pin_async(admin_user_id, pin, create_admin_session)) - - if result["success"]: - callback_result = result.get("callback_result", {}) - session_token = callback_result.get("session_token") - - # Set cookie - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_header('Set-Cookie', f'admin_session={session_token}; Path=/admin; HttpOnly') - self.add_security_headers() - self.end_headers() - self.wfile.write(json.dumps({"success": True}).encode()) - else: - self.send_json_response({"success": False, "error": result.get("error", "Invalid or expired PIN")}, 401) - - except Exception as e: - print(f"Error in handle_admin_verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _verify_admin_pin_async(self, admin_user_id, pin_code, callback): - """Verify admin PIN using unified PIN service""" - try: - # Use unified PIN service to verify and execute - result = await self.pin_service.verify_and_execute( - player_id=admin_user_id, - pin_code=pin_code, - action_type="admin_auth", - action_callback=callback - ) - return result - except Exception as e: - return {"success": False, "error": f"Admin PIN verification failed: {str(e)}"} - - - - def handle_player_auth(self): - """Handle player authentication request and generate PIN""" - try: - # Parse request data - content_length = int(self.headers.get('Content-Length', 0)) - post_data = self.rfile.read(content_length).decode('utf-8') - data = json.loads(post_data) - - nickname = data.get('nickname', '').strip() - if not nickname: - self.send_json_response({"success": False, "error": "Nickname is required"}, 400) - return - - # Check if player exists - import asyncio - async def check_player_exists(): - player = await self.database.get_player(nickname) - return player - - player = asyncio.run(check_player_exists()) - if not player: - self.send_json_response({"success": False, "error": "Player not found. You need to register first by using !start in IRC."}, 404) - return - - # Generate PIN using PinAuthenticationService - async def generate_login_pin(): - from src.pin_authentication import PinAuthenticationService - pin_service = PinAuthenticationService(self.database, self.bot) - - result = await pin_service.request_verification( - player_id=player['id'], - nickname=nickname, - action_type="web_login", - action_data={"login_time": datetime.now().isoformat()}, - expiration_minutes=10, - message_template="""🔐 Web Login PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN on the login page to access your profile. - -⚠️ Keep this PIN private! Do not share it with anyone.""" - ) - return result - - pin_result = asyncio.run(generate_login_pin()) - - if pin_result["success"]: - self.send_json_response({"success": True, "message": "PIN sent via IRC"}) - else: - self.send_json_response({"success": False, "error": "Failed to generate PIN"}, 500) - - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - except Exception as e: - print(f"Error in player auth: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - def handle_player_verify(self): - """Handle player PIN verification and create session""" - try: - # Parse request data - content_length = int(self.headers.get('Content-Length', 0)) - post_data = self.rfile.read(content_length).decode('utf-8') - data = json.loads(post_data) - - nickname = data.get('nickname', '').strip() - pin = data.get('pin', '').strip() - - if not nickname or not pin: - self.send_json_response({"success": False, "error": "Nickname and PIN are required"}, 400) - return - - if len(pin) != 6 or not pin.isdigit(): - self.send_json_response({"success": False, "error": "PIN must be 6 digits"}, 400) - return - - # Get player - import asyncio - async def verify_login_pin(): - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Verify PIN using PinAuthenticationService - from src.pin_authentication import PinAuthenticationService - pin_service = PinAuthenticationService(self.database, self.bot) - - result = await pin_service.verify_and_execute( - player_id=player['id'], - pin_code=pin, - action_type="web_login" - ) - - if result["success"]: - return {"success": True, "player": player} - else: - return result - - result = asyncio.run(verify_login_pin()) - - if result["success"]: - # Create player session - session_token = self.create_player_session(nickname) - - # Set cookie - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Set-Cookie', f'player_session={session_token}; Path=/; HttpOnly; Max-Age=86400') # 24 hours - self.add_security_headers() - self.end_headers() - - response_data = {"success": True, "message": "Login successful", "nickname": nickname} - self.wfile.write(json.dumps(response_data).encode('utf-8')) - else: - self.send_json_response({"success": False, "error": result.get("error", "Invalid PIN")}, 401) - - except json.JSONDecodeError: - self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) - except Exception as e: - print(f"Error in player verify: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - def handle_player_logout(self): - """Handle player logout""" - try: - # Remove player session - self.logout_player_session() - - # Clear cookie - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Set-Cookie', 'player_session=; Path=/; HttpOnly; Max-Age=0') # Clear cookie - self.add_security_headers() - self.end_headers() - - response_data = {"success": True, "message": "Logged out successfully"} - self.wfile.write(json.dumps(response_data).encode('utf-8')) - - except Exception as e: - print(f"Error in player logout: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - def check_admin_session(self): - """Check if user has valid admin session""" - # Get cookie - cookie_header = self.headers.get('Cookie', '') - session_token = None - - for cookie in cookie_header.split(';'): - if cookie.strip().startswith('admin_session='): - session_token = cookie.strip()[14:] - break - - if not session_token: - return None - - # Check if session is valid - import time - session = self.admin_sessions.get(session_token) - - if session and session['expires'] > time.time(): - # Extend session - session['expires'] = time.time() + (60 * 60) - return session['nickname'] - - # Invalid or expired session - if session_token in self.admin_sessions: - del self.admin_sessions[session_token] - - return None - - def check_player_session(self, required_nickname=None): - """Check if user has valid player session - - Args: - required_nickname: If provided, checks if the session matches this specific player - - Returns: - str: Player nickname if session is valid, None otherwise - """ - # Get cookie - cookie_header = self.headers.get('Cookie', '') - session_token = None - - for cookie in cookie_header.split(';'): - if cookie.strip().startswith('player_session='): - session_token = cookie.strip()[15:] - break - - if not session_token: - return None - - # Check if session is valid - import time - session = self.player_sessions.get(session_token) - - if session and session['expires'] > time.time(): - # Extend session - session['expires'] = time.time() + (60 * 60 * 24) # 24 hour session for players - - # If a specific nickname is required, check if it matches - if required_nickname and session['nickname'].lower() != required_nickname.lower(): - return None - - return session['nickname'] - - # Invalid or expired session - if session_token in self.player_sessions: - del self.player_sessions[session_token] - - return None - - def create_player_session(self, nickname): - """Create a new player session - - Args: - nickname: Player's nickname - - Returns: - str: Session token - """ - import hashlib - import time - session_token = hashlib.sha256(f"{nickname}:player:{time.time()}".encode()).hexdigest() - - # Store session - self.player_sessions[session_token] = { - 'nickname': nickname, - 'expires': time.time() + (60 * 60 * 24) # 24 hour session - } - - return session_token - - def logout_player_session(self, nickname=None): - """Logout player session(s) - - Args: - nickname: If provided, logout only sessions for this player - """ - if nickname: - # Remove sessions for specific player - to_remove = [] - for token, session in self.player_sessions.items(): - if session['nickname'].lower() == nickname.lower(): - to_remove.append(token) - for token in to_remove: - del self.player_sessions[token] - else: - # Get current session from cookie and remove it - cookie_header = self.headers.get('Cookie', '') - for cookie in cookie_header.split(';'): - if cookie.strip().startswith('player_session='): - session_token = cookie.strip()[15:] - if session_token in self.player_sessions: - del self.player_sessions[session_token] - break - - def redirect_to_login(self, message=None): - """Redirect user to login page with optional message""" - login_url = "/login" - if message: - # You could encode the message in URL params if needed - # For now, just redirect to login - pass - - self.send_response(302) - self.send_header('Location', login_url) - self.add_security_headers() - self.end_headers() - - def serve_admin_dashboard(self): - """Serve the admin dashboard page""" - # Check admin session - admin_user = self.check_admin_session() - if not admin_user: - # Redirect to login - self.send_response(302) - self.send_header('Location', '/admin') - self.add_security_headers() - self.end_headers() - return - - # Get system statistics - import asyncio - stats = asyncio.run(self._get_admin_stats_async()) - - content = f""" -
-

🎮 Admin Control Panel

-

Welcome, {admin_user}!

-
- - -
-

📊 System Statistics

-
-
-

👥 Total Players

-
{stats['total_players']}
-
-
-

🐾 Total Pets

-
{stats['total_pets']}
-
-
-

⚔️ Active Battles

-
{stats['active_battles']}
-
-
-

💾 Database Size

-
{stats['db_size']}
-
-
-
- - -
-

👥 Player Management

-
-

Search Player

- - -
-
-
- - -
-

🔧 System Controls

-
-
-

Database Management

- - -
-
- -
-

IRC Management

- - - -
-
-
-
- - -
-

🌍 Game Management

-
-
-

Weather Control

- - - -
-
- -
-

Rate Limiting

- - - -
-
-
-
- - + + + + ← 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
+
+
+
+
+ +
+
🐾 Pet Collection
+
+ + + + + + + + + + + + + + {pets_html} + +
StatusNameSpeciesTypeLevelHPStats
+
+
+ +
+
🏆 Achievements
+
+ {achievements_html} +
+
+ +
+
🎒 Inventory
+
+ {inventory_html} +
+
+ +""" - - """ - - html = self.get_page_template("Admin Dashboard", content, "") self.send_response(200) self.send_header('Content-type', 'text/html') - self.add_security_headers() self.end_headers() self.wfile.write(html.encode()) - def _get_location_options(self, locations): - """Generate HTML options for locations""" - options = "" - for location in locations: - options += f'' - return options - - async def _get_admin_stats_async(self): - """Get admin dashboard statistics""" - import aiosqlite - import os - - stats = { - 'total_players': 0, - 'total_pets': 0, - 'active_battles': 0, - 'db_size': '0 MB', - 'total_achievements': 0, - 'total_badges': 0, - 'total_items': 0, - 'locations': [] - } - - try: - async with aiosqlite.connect(self.database.db_path) as db: - # Get player count - cursor = await db.execute("SELECT COUNT(*) FROM players") - stats['total_players'] = (await cursor.fetchone())[0] - - # Get pet count - cursor = await db.execute("SELECT COUNT(*) FROM pets") - stats['total_pets'] = (await cursor.fetchone())[0] - - # Get active battles (if table exists) - try: - cursor = await db.execute("SELECT COUNT(*) FROM active_battles") - stats['active_battles'] = (await cursor.fetchone())[0] - except: - stats['active_battles'] = 0 - - # Get achievement count - try: - cursor = await db.execute("SELECT COUNT(*) FROM player_achievements") - stats['total_achievements'] = (await cursor.fetchone())[0] - except: - stats['total_achievements'] = 0 - - # Get badge count (if table exists) - try: - cursor = await db.execute("SELECT COUNT(*) FROM player_badges") - stats['total_badges'] = (await cursor.fetchone())[0] - except: - stats['total_badges'] = 0 - - # Get item count (if table exists) - try: - cursor = await db.execute("SELECT COUNT(*) FROM player_items") - stats['total_items'] = (await cursor.fetchone())[0] - except: - stats['total_items'] = 0 - - # Get locations - cursor = await db.execute("SELECT id, name FROM locations ORDER BY name") - locations = await cursor.fetchall() - stats['locations'] = [{'id': loc[0], 'name': loc[1]} for loc in locations] - - # Get database size - if os.path.exists(self.database.db_path): - size_bytes = os.path.getsize(self.database.db_path) - stats['db_size'] = f"{size_bytes / 1024 / 1024:.2f} MB" - - except Exception as e: - print(f"Error getting admin stats: {e}") - - return stats - - def handle_admin_api(self, endpoint): - """Handle admin API requests""" - print(f"Admin API request: {endpoint}") - - # Check admin session - admin_user = self.check_admin_session() - if not admin_user: - print(f"Unauthorized admin API request for endpoint: {endpoint}") - self.send_json_response({"success": False, "error": "Unauthorized"}, 401) - return - - print(f"Authorized admin API request from {admin_user} for endpoint: {endpoint}") - - # Get POST data if it's a POST request - content_length = int(self.headers.get('Content-Length', 0)) - post_data = {} - - if content_length > 0 and self.command == 'POST': - try: - import json - post_data = json.loads(self.rfile.read(content_length).decode('utf-8')) - except: - pass - - # Parse query parameters for GET requests - query_params = {} - if '?' in endpoint: - endpoint, query_string = endpoint.split('?', 1) - for param in query_string.split('&'): - if '=' in param: - key, value = param.split('=', 1) - query_params[key] = value - - # Route to appropriate handler - if endpoint.startswith('player/'): - # Handle player endpoints - support both info and updates - player_path = endpoint[7:] # Remove 'player/' prefix - - if player_path.endswith('/update'): - # Player update endpoint - player_name = player_path[:-7] # Remove '/update' suffix - if self.command == 'POST': - self.handle_admin_player_update(player_name, post_data) - else: - self.send_json_response({"success": False, "error": "Method not allowed"}, 405) - else: - # Player info endpoint - self.handle_admin_player_get(player_path) - elif endpoint == 'backup': - if self.command == 'POST': - self.handle_admin_backup_create() - else: - self.send_json_response({"success": False, "error": "Method not allowed"}, 405) - elif endpoint == 'backups': - self.handle_admin_backups_list() - elif endpoint == 'broadcast': - self.handle_admin_broadcast(post_data) - elif endpoint == 'irc-status': - print(f"IRC status endpoint hit!") - # Add a simple test first - try: - print(f"Calling handle_admin_irc_status...") - self.handle_admin_irc_status() - print(f"handle_admin_irc_status completed") - except Exception as e: - print(f"Exception in IRC status handler: {e}") - import traceback - traceback.print_exc() - # Send a simple fallback response - self.send_json_response({ - "success": False, - "error": "IRC status handler failed", - "details": str(e) - }, 500) - elif endpoint == 'weather': - self.handle_admin_weather_set(post_data) - elif endpoint == 'rate-stats': - self.handle_admin_rate_stats(query_params) - elif endpoint == 'rate-reset': - self.handle_admin_rate_reset(post_data) - elif endpoint == 'test': - # Simple test endpoint - self.send_json_response({ - "success": True, - "message": "Test endpoint working", - "timestamp": str(datetime.now()) - }) - else: - self.send_json_response({"success": False, "error": "Unknown endpoint"}, 404) - - def handle_admin_player_search(self, data): - """Search for a player""" - nickname = data.get('nickname', '').strip() - if not nickname: - self.send_json_response({"success": False, "error": "No nickname provided"}) - return - - import asyncio - player = asyncio.run(self.database.get_player(nickname)) - - if player: - # Get additional stats - pet_count = asyncio.run(self._get_player_pet_count_async(player['id'])) - - self.send_json_response({ - "success": True, - "player": { - "nickname": player['nickname'], - "level": player['level'], - "money": player['money'], - "pet_count": pet_count, - "experience": player['experience'] - } - }) - else: - self.send_json_response({"success": False, "error": "Player not found"}) - - async def _get_player_pet_count_async(self, player_id): - """Get player's pet count""" - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute("SELECT COUNT(*) FROM pets WHERE player_id = ?", (player_id,)) - return (await cursor.fetchone())[0] - - def handle_admin_backup_create(self): - """Create a database backup""" - if self.bot and hasattr(self.bot, 'backup_manager'): - import asyncio - result = asyncio.run(self.bot.backup_manager.create_backup("manual", "Admin web interface")) - - if result['success']: - self.send_json_response({ - "success": True, - "message": f"Backup created: {result['filename']}" - }) - else: - self.send_json_response({ - "success": False, - "error": result.get('error', 'Backup failed') - }) - else: - self.send_json_response({ - "success": False, - "error": "Backup system not available" - }) - - def handle_admin_weather_set(self, data): - """Set weather for a location""" - location = data.get('location', '').strip() - weather = data.get('weather', '').strip() - - if not location or not weather: - self.send_json_response({"success": False, "error": "Missing location or weather"}) - return - - # Execute weather change using database directly - try: - import asyncio - result = asyncio.run(self._set_weather_for_location_async(location, weather)) - - if result.get("success"): - self.send_json_response({ - "success": True, - "message": result.get("message", f"Weather set to {weather} in {location}") - }) - else: - self.send_json_response({ - "success": False, - "error": result.get("error", "Failed to set weather") - }) - - except Exception as e: - print(f"Error setting weather: {e}") - self.send_json_response({ - "success": False, - "error": f"Weather system error: {str(e)}" - }) - - async def _set_weather_for_location_async(self, location, weather): - """Async helper to set weather for location""" - try: - import json - import random - - # Load weather patterns - try: - with open("config/weather_patterns.json", "r") as f: - weather_data = json.load(f) - except FileNotFoundError: - return { - "success": False, - "error": "Weather configuration file not found" - } - - # Validate weather type - weather_types = list(weather_data["weather_types"].keys()) - if weather not in weather_types: - return { - "success": False, - "error": f"Invalid weather type. Valid types: {', '.join(weather_types)}" - } - - weather_config = weather_data["weather_types"][weather] - - # Calculate duration (3 hours = 180 minutes) - duration_range = weather_config.get("duration_minutes", [90, 180]) - duration = random.randint(duration_range[0], duration_range[1]) - - end_time = datetime.now() + timedelta(minutes=duration) - - # Set weather for the location - result = await self.database.set_weather_for_location( - location, weather, end_time.isoformat(), - weather_config.get("spawn_modifier", 1.0), - ",".join(weather_config.get("affected_types", [])) - ) - - if result.get("success"): - # Announce weather change if it actually changed and bot is available - if result.get("changed") and self.bot and hasattr(self.bot, 'game_engine'): - await self.bot.game_engine.announce_weather_change( - location, result.get("previous_weather"), weather, "web" - ) - - return { - "success": True, - "message": f"Weather set to {weather} in {location} for {duration} minutes" - } - else: - return { - "success": False, - "error": result.get("error", "Failed to set weather") - } - - except Exception as e: - print(f"Error in _set_weather_for_location_async: {e}") - return { - "success": False, - "error": str(e) - } - - def handle_admin_announce(self, data): - """Send announcement to IRC""" - message = data.get('message', '').strip() - - if not message: - self.send_json_response({"success": False, "error": "No message provided"}) - return - - if self.bot and hasattr(self.bot, 'send_message_sync'): - try: - # Send to main channel - self.bot.send_message_sync("#petz", f"📢 ANNOUNCEMENT: {message}") - self.send_json_response({"success": True, "message": "Announcement sent"}) - except Exception as e: - self.send_json_response({"success": False, "error": str(e)}) - else: - self.send_json_response({"success": False, "error": "IRC not available"}) - - def handle_admin_monitor(self, monitor_type): - """Get monitoring data""" - # TODO: Implement real monitoring data - self.send_json_response({ - "success": True, - "type": monitor_type, - "data": [] - }) - - def handle_admin_player_get(self, nickname): - """Get player information""" - if not nickname: - self.send_json_response({"success": False, "error": "No nickname provided"}, 400) - return - - try: - import asyncio - result = asyncio.run(self._get_player_info_async(nickname)) - - if result["success"]: - self.send_json_response(result) - else: - self.send_json_response(result, 404) - - except Exception as e: - print(f"Error in handle_admin_player_get: {e}") - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - def handle_admin_player_update(self, nickname, data): - """Update player information""" - if not nickname: - self.send_json_response({"success": False, "error": "No nickname provided"}, 400) - return - - if not data: - self.send_json_response({"success": False, "error": "No update data provided"}, 400) - return - - try: - import asyncio - result = asyncio.run(self._update_player_async(nickname, data)) - - if result["success"]: - self.send_json_response(result) - else: - self.send_json_response(result, 400) - - except Exception as e: - print(f"Error in handle_admin_player_update: {e}") - import traceback - traceback.print_exc() - self.send_json_response({"success": False, "error": "Internal server error"}, 500) - - async def _update_player_async(self, nickname, data): - """Update player information asynchronously""" - try: - # Validate input data - allowed_fields = ['level', 'experience', 'money'] - updates = {} - - for field in allowed_fields: - if field in data: - value = data[field] - - # Validate each field - if field == 'level': - if not isinstance(value, int) or value < 1 or value > 100: - return {"success": False, "error": "Level must be between 1 and 100"} - updates[field] = value - elif field == 'experience': - if not isinstance(value, int) or value < 0: - return {"success": False, "error": "Experience cannot be negative"} - updates[field] = value - elif field == 'money': - if not isinstance(value, int) or value < 0: - return {"success": False, "error": "Money cannot be negative"} - updates[field] = value - - if not updates: - return {"success": False, "error": "No valid fields to update"} - - # Check if player exists - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Update player data - success = await self.database.update_player_admin(player["id"], updates) - - if success: - return { - "success": True, - "message": f"Updated {', '.join(updates.keys())} for player {nickname}", - "updated_fields": list(updates.keys()) - } - else: - return {"success": False, "error": "Failed to update player data"} - - except Exception as e: - print(f"Error updating player: {e}") - return {"success": False, "error": str(e)} - - async def _get_player_info_async(self, nickname): - """Get player information asynchronously""" - try: - player = await self.database.get_player(nickname) - if not player: - return {"success": False, "error": "Player not found"} - - # Get additional stats - pets = await self.database.get_player_pets(player["id"]) - - # Get location name if current_location_id is set - location_name = "Unknown" - if player.get("current_location_id"): - try: - location_data = await self.database.get_location_by_id(player["current_location_id"]) - if location_data: - location_name = location_data["name"] - else: - location_name = f"Location ID {player['current_location_id']}" - except Exception as loc_error: - print(f"Error resolving location: {loc_error}") - location_name = f"Location ID {player['current_location_id']}" - - # Get team composition - team_info = await self.database.get_team_composition(player["id"]) - - return { - "success": True, - "player": { - "nickname": player["nickname"], - "level": player["level"], - "experience": player["experience"], - "money": player["money"], - "current_location": location_name, - "pet_count": len(pets), - "active_pets": team_info.get("active_pets", 0), - "total_pets": team_info.get("total_pets", 0) - } - } - except Exception as e: - print(f"Error getting player info: {e}") - return {"success": False, "error": str(e)} - - def handle_admin_backups_list(self): - """List available backups""" - try: - import os - backup_dir = "backups" - - if not os.path.exists(backup_dir): - self.send_json_response({ - "success": True, - "backups": [] - }) - return - - backups = [] - for filename in os.listdir(backup_dir): - if filename.endswith('.gz'): - filepath = os.path.join(backup_dir, filename) - size = os.path.getsize(filepath) - # Convert size to human readable format - if size < 1024: - size_str = f"{size} B" - elif size < 1024 * 1024: - size_str = f"{size / 1024:.1f} KB" - else: - size_str = f"{size / (1024 * 1024):.1f} MB" - - backups.append({ - "name": filename, - "size": size_str - }) - - # Sort by name (newest first) - backups.sort(key=lambda x: x['name'], reverse=True) - - self.send_json_response({ - "success": True, - "backups": backups[:10] # Show only last 10 backups - }) - - except Exception as e: - print(f"Error listing backups: {e}") - self.send_json_response({"success": False, "error": str(e)}, 500) - - def handle_admin_broadcast(self, data): - """Send IRC broadcast message""" - message = data.get('message', '').strip() - if not message: - self.send_json_response({"success": False, "error": "No message provided"}, 400) - return - - try: - # Send to IRC channel via bot - if self.bot and hasattr(self.bot, 'send_message_sync'): - from config import IRC_CONFIG - channel = IRC_CONFIG.get("channel", "#petz") - self.bot.send_message_sync(channel, f"📢 Admin Announcement: {message}") - - self.send_json_response({ - "success": True, - "message": "Broadcast sent successfully" - }) - else: - self.send_json_response({"success": False, "error": "IRC bot not available"}, 500) - - except Exception as e: - print(f"Error sending broadcast: {e}") - self.send_json_response({"success": False, "error": str(e)}, 500) - - def handle_admin_irc_status(self): - """Get comprehensive IRC connection status and activity""" - try: - print(f"IRC status request - checking bot availability...") - bot = self.bot - print(f"Bot instance: {bot}") - - if bot and hasattr(bot, 'connection_manager') and bot.connection_manager: - connection_manager = bot.connection_manager - print(f"Connection manager: {connection_manager}") - - if hasattr(connection_manager, 'get_connection_stats'): - try: - stats = connection_manager.get_connection_stats() - print(f"Got comprehensive connection stats") - - # Return comprehensive IRC status - response_data = { - "success": True, - "irc_status": stats - } - print(f"Sending comprehensive IRC response") - self.send_json_response(response_data) - - except Exception as stats_error: - print(f"Error getting connection stats: {stats_error}") - import traceback - traceback.print_exc() - self.send_json_response({ - "success": False, - "error": f"Failed to get connection stats: {str(stats_error)}" - }, 500) - else: - print("Connection manager has no get_connection_stats method") - self.send_json_response({ - "success": False, - "error": "Connection manager missing get_connection_stats method" - }, 500) - else: - print("No bot instance or connection manager available") - self.send_json_response({ - "success": True, - "irc_status": { - "connected": False, - "state": "disconnected", - "error": "Bot instance or connection manager not available" - } - }) - - except Exception as e: - print(f"Error getting IRC status: {e}") - import traceback - traceback.print_exc() - try: - self.send_json_response({"success": False, "error": str(e)}, 500) - except Exception as json_error: - print(f"Failed to send JSON error response: {json_error}") - # Send a basic HTTP error response if JSON fails - self.send_error(500, f"IRC status error: {str(e)}") - - def handle_admin_rate_stats(self, query_params): - """Get rate limiting statistics""" - try: - username = query_params.get('user', '').strip() - - if self.bot and hasattr(self.bot, 'rate_limiter') and self.bot.rate_limiter: - if username: - # Get stats for specific user - user_stats = self.bot.rate_limiter.get_user_stats(username) - self.send_json_response({ - "success": True, - "stats": { - "violations": user_stats.get("violations", 0), - "banned": user_stats.get("banned", False), - "last_violation": user_stats.get("last_violation", "Never") - } - }) - else: - # Get global stats - global_stats = self.bot.rate_limiter.get_global_stats() - self.send_json_response({ - "success": True, - "stats": { - "total_users": global_stats.get("total_users", 0), - "active_bans": global_stats.get("active_bans", 0), - "total_violations": global_stats.get("total_violations", 0) - } - }) - else: - self.send_json_response({ - "success": True, - "stats": { - "total_users": 0, - "active_bans": 0, - "total_violations": 0 - } - }) - - except Exception as e: - print(f"Error getting rate stats: {e}") - self.send_json_response({"success": False, "error": str(e)}, 500) - - def handle_admin_rate_reset(self, data): - """Reset rate limiting for user or globally""" - try: - username = data.get('user', '').strip() if data.get('user') else None - - if self.bot and hasattr(self.bot, 'rate_limiter') and self.bot.rate_limiter: - if username: - # Reset for specific user - success = self.bot.rate_limiter.reset_user(username) - if success: - self.send_json_response({ - "success": True, - "message": f"Rate limits reset for user: {username}" - }) - else: - self.send_json_response({"success": False, "error": f"User {username} not found"}, 404) - else: - # Global reset - self.bot.rate_limiter.reset_all() - self.send_json_response({ - "success": True, - "message": "All rate limits reset successfully" - }) - else: - self.send_json_response({"success": False, "error": "Rate limiter not available"}, 500) - - except Exception as e: - print(f"Error resetting rate limits: {e}") - self.send_json_response({"success": False, "error": str(e)}, 500) - - # ================================================================ - # NEW TEAM BUILDER METHODS - Separated Team Management - # ================================================================ - - def serve_team_selection_hub(self, nickname): - """Serve the team selection hub showing all teams with swap options.""" - try: - # Get database and bot from server - database = self.server.database if hasattr(self.server, 'database') else None - bot = self.server.bot if hasattr(self.server, 'bot') else None - - if not database: - self.send_error(500, "Database not available") - return - - # Get team management service - if not hasattr(self, 'team_service'): - from src.team_management import TeamManagementService - from src.pin_authentication import PinAuthenticationService - pin_service = PinAuthenticationService(database, bot) - self.team_service = TeamManagementService(database, pin_service) - - # Get team overview - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - player = loop.run_until_complete(database.get_player(nickname)) - if not player: - self.send_error(404, "Player not found") - return - - team_overview = loop.run_until_complete(self.team_service.get_team_overview(player["id"])) - if not team_overview["success"]: - self.send_error(500, f"Failed to load teams: {team_overview['error']}") - return - - teams = team_overview["teams"] - finally: - loop.close() - - # Generate team hub HTML - content = self.generate_team_hub_content(nickname, teams) - full_page = self.get_page_template(f"Team Management - {nickname}", content, "teambuilder") - - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.add_security_headers() - self.end_headers() - self.wfile.write(full_page.encode('utf-8')) - - except Exception as e: - print(f"Error serving team selection hub: {e}") - self.send_error(500, "Internal server error") - - def serve_individual_team_editor(self, nickname, team_identifier): - """Serve individual team editor page.""" - try: - # Get database and bot from server - database = self.server.database if hasattr(self.server, 'database') else None - bot = self.server.bot if hasattr(self.server, 'bot') else None - - if not database: - self.send_error(500, "Database not available") - return - - # Get team management service - if not hasattr(self, 'team_service'): - from src.team_management import TeamManagementService - from src.pin_authentication import PinAuthenticationService - pin_service = PinAuthenticationService(database, bot) - self.team_service = TeamManagementService(database, pin_service) - - # Get team data - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - player = loop.run_until_complete(database.get_player(nickname)) - if not player: - self.send_error(404, "Player not found") - return - - team_data = loop.run_until_complete( - self.team_service.get_individual_team_data(player["id"], team_identifier) - ) - if not team_data["success"]: - self.send_error(500, f"Failed to load team: {team_data['error']}") - return - - # Get player's pets for the editor - player_pets = loop.run_until_complete(database.get_player_pets(player["id"])) - finally: - loop.close() - - # Generate individual team editor HTML - content = self.generate_individual_team_editor_content(nickname, team_identifier, team_data, player_pets) - full_page = self.get_page_template(f"{team_data['team_name']} - {nickname}", content, "teambuilder") - - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.add_security_headers() - self.end_headers() - self.wfile.write(full_page.encode('utf-8')) - - except Exception as e: - print(f"Error serving individual team editor: {e}") - self.send_error(500, "Internal server error") - - def generate_team_hub_content(self, nickname, teams): - """Generate HTML content for team selection hub.""" - return f''' -
-
-

🏆 Team Management Hub

-

Manage your teams and swap configurations with PIN verification

-
- -
-

⚡ Current Battle Team

-
- {self._generate_active_team_display(teams.get('active', {}), nickname)} -
-
- -
-

💾 Saved Team Configurations

-
- {self._generate_team_preview(teams.get('slot_1', {}), '1')} - {self._generate_team_preview(teams.get('slot_2', {}), '2')} - {self._generate_team_preview(teams.get('slot_3', {}), '3')} -
-
- - -
- - - ''' - - def _generate_team_preview(self, team_data, team_identifier): - """Generate preview for a single team.""" - team_name = team_data.get('name', f'Team {team_identifier}') - pet_count = team_data.get('count', 0) - is_active = team_data.get('is_active', False) - - if is_active: - actions = f''' - - ✏️ Edit Active Team - - ''' - else: - actions = f''' - - ✏️ Edit Team {team_identifier} - - - - ''' - - status = "🏆 ACTIVE TEAM" if is_active else f"💾 Saved Team" - - return f''' -
-

{team_name}

-
{status}
-
🐾 {pet_count} pets
- -
- {actions} -
-
- ''' - - def _generate_active_team_display(self, active_team_data, nickname): - """Generate detailed display for active team with individual pet cards.""" - pets_dict = active_team_data.get('pets', {}) - pet_count = active_team_data.get('count', 0) - - # Convert dictionary format to list for consistent processing - pets = [] - if isinstance(pets_dict, dict): - # Active team format: {"1": {pet_data}, "2": {pet_data}} - for position, pet_data in pets_dict.items(): - if pet_data: - pet_data['team_order'] = int(position) - pets.append(pet_data) - elif isinstance(pets_dict, list): - # Saved team format: [{pet_data}, {pet_data}] - pets = pets_dict - - if not pets or pet_count == 0: - return f''' -
-

🏆 Active Team

-
No pets in active team
- -
- ''' - - # Generate individual pet cards for active team - pet_cards = [] - for pet in pets: - # Handle both active team format and saved team format - name = pet.get('nickname') or pet.get('name') or pet.get('species_name', 'Unknown') - level = pet.get('level', 1) - hp = pet.get('hp', 0) - max_hp = pet.get('max_hp', 0) - attack = pet.get('attack', 0) - defense = pet.get('defense', 0) - speed = pet.get('speed', 0) - happiness = pet.get('happiness', 50) - species_name = pet.get('species_name', 'Unknown') - - # Handle type field variations between active and saved teams - type1 = (pet.get('type1') or pet.get('type_primary') or - pet.get('type1', 'Normal')) - type2 = (pet.get('type2') or pet.get('type_secondary') or - pet.get('type2')) - - team_order = pet.get('team_order', 0) - - # Calculate HP percentage for health bar - hp_percent = (hp / max_hp) * 100 if max_hp > 0 else 0 - hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336" - - # Happiness emoji - if happiness >= 80: - happiness_emoji = "😊" - elif happiness >= 60: - happiness_emoji = "🙂" - elif happiness >= 40: - happiness_emoji = "😐" - elif happiness >= 20: - happiness_emoji = "😕" - else: - happiness_emoji = "😢" - - # Type display - type_display = type1 - if type2: - type_display += f"/{type2}" - - pet_card = f''' -
-
-

#{team_order} {name}

-
Lv.{level}
-
-
{species_name}
-
{type_display}
- -
-
- HP - {hp}/{max_hp} -
-
-
-
-
- -
-
- ATK - {attack} -
-
- DEF - {defense} -
-
- SPD - {speed} -
-
- -
- {happiness_emoji} - Happiness: {happiness}/100 -
-
- ''' - pet_cards.append(pet_card) - - pets_html = "".join(pet_cards) - - return f''' -
-
-

🏆 Active Battle Team ({pet_count} pets)

- - ✏️ Edit Active Team - -
-
- {pets_html} -
-
- - - - - ''' - - def generate_individual_team_editor_content(self, nickname, team_identifier, team_data, player_pets): - """Generate HTML content for individual team editor.""" - team_name = team_data.get('team_name', 'Unknown Team') - is_active_team = team_data.get('is_active_team', False) - team_pets = team_data.get('pets', []) - - # Separate pets into team and storage - team_pet_ids = [p['id'] for p in team_pets] - storage_pets = [p for p in player_pets if p['id'] not in team_pet_ids] - - # Helper function to create detailed pet card - def make_pet_card(pet, in_team=False): - name = pet.get('nickname') or pet.get('species_name', 'Unknown') - pet_id = pet.get('id', 0) - level = pet.get('level', 1) - species = pet.get('species_name', 'Unknown') - - # Type info - type_str = pet.get('type1', 'Normal') - if pet.get('type2'): - type_str += f"/{pet['type2']}" - - # HP calculation - hp = pet.get('hp', 0) - max_hp = pet.get('max_hp', 1) - hp_percent = (hp / max_hp * 100) if max_hp > 0 else 0 - hp_color = "#4CAF50" if hp_percent > 60 else "#FF9800" if hp_percent > 25 else "#f44336" - - # Get pet moves - moves = pet.get('moves', []) - moves_html = '' - if moves: - moves_html = '
Moves: ' + ', '.join([m.get('name', 'Unknown') for m in moves[:4]]) + '
' - - return f""" -
-
-

{pet.get('emoji', '🐾')} {name}

-
Lv.{level}
-
-
{species}
-
{type_str}
- -
-
- HP - {hp}/{max_hp} -
-
-
-
-
- -
-
- ATK - {pet.get('attack', 0)} -
-
- DEF - {pet.get('defense', 0)} -
-
- SPD - {pet.get('speed', 0)} -
-
- - {moves_html} - -
- {'😊' if pet.get('happiness', 50) > 70 else '😐' if pet.get('happiness', 50) > 40 else '😞'} - Happiness: {pet.get('happiness', 50)}/100 -
-
- """ - - # Create team slots (6 slots) - team_slots_html = '' - for i in range(1, 7): - # Find pet in this slot - slot_pet = None - for pet in team_pets: - if pet.get('team_order') == i or (i == 1 and not any(p.get('team_order') == 1 for p in team_pets) and team_pets and pet == team_pets[0]): - slot_pet = pet - break - - if slot_pet: - team_slots_html += f""" -
-
#{i}
- {make_pet_card(slot_pet, True)} -
- """ - else: - team_slots_html += f""" -
-
#{i}
-
- - Drop pet here -
-
- """ - - # Create storage pet cards - storage_cards_html = ''.join([make_pet_card(pet, False) for pet in storage_pets]) - - if not storage_cards_html: - storage_cards_html = '
No pets in storage. All your pets are in teams!
' - - return f''' -
-
-

✏️ {team_name}

-

{"⚡ Active battle team" if is_active_team else f"💾 Saved team configuration (Slot {team_identifier})"}

- ← Back to Hub -
- -
-
-

🏆 Team Composition

-
- {team_slots_html} -
-
- - -
- -
- -
-

🐾 Pet Storage ({len(storage_pets)} available)

-
- {storage_cards_html} -
-
-
-
- - - - - ''' - + def log_message(self, format, *args): + """Override to reduce logging noise""" + pass class PetBotWebServer: - """Standalone web server for PetBot""" - - def __init__(self, database=None, port=8080, bot=None): - self.database = database or Database() + def __init__(self, database, port=8080): + self.database = database self.port = port - self.bot = bot self.server = None - # Initialize PIN authentication service - self.pin_service = PinAuthenticationService(self.database, self.bot) - # Add custom message template for admin authentication - self.pin_service.add_message_template("admin_auth", """🔐 Admin Panel Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN at the admin login page to access the control panel. - -⚠️ This is an administrative access PIN. Keep it private!""") - - # Add custom message template for pet renames with shorter expiration - self.pin_service.add_message_template("pet_rename", """🐾 Pet Rename Verification PIN: {pin_code} - -This PIN will expire in {expiration_minutes} minutes. -Enter this PIN on the pet management page to confirm your pet rename. - -⚠️ Keep this PIN private! Do not share it with anyone.""") - def run(self): - """Start the web server""" + """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}") 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 - self.server.pin_service = self.pin_service - - 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('') - - try: - self.server.serve_forever() - except KeyboardInterrupt: - print('\n🛑 Server stopped') - finally: - self.server.server_close() + self.server.serve_forever() def start_in_thread(self): - """Start the web server in a separate thread""" - import threading - - def run_server(): - self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler) - self.server.database = self.database - self.server.bot = self.bot - self.server.pin_service = self.pin_service - - try: - self.server.serve_forever() - except Exception as e: - print(f"Web server error: {e}") - finally: - self.server.server_close() - - self.server_thread = threading.Thread(target=run_server, daemon=True) - self.server_thread.start() - - def stop(self): - """Stop the web server""" - if self.server: - print('🛑 Stopping web server...') - self.server.shutdown() - self.server.server_close() - + """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}") + return thread def run_standalone(): - """Run the web server in standalone mode""" - import sys + """Run the web server standalone for testing""" + print("🐾 PetBot Web Server (Standalone Mode)") + print("=" * 40) - port = 8080 - if len(sys.argv) > 1: - try: - port = int(sys.argv[1]) - except ValueError: - print('Usage: python webserver.py [port]') - sys.exit(1) + # Initialize database + database = Database() + # Note: In standalone mode, we can't easily run async init + # This is mainly for testing the web routes - server = PetBotWebServer(port=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('📱 Example Player Profile:') - print(' http://petz.rdx4.com/player/megasconed') - print('') - print('⚙️ Press Ctrl+C to stop the server') - print('') + # Start web server + server = PetBotWebServer(database) + print("🚀 Starting web server...") + print("📝 Available routes:") + print(" http://localhost:8080/ - Game Hub") + print(" http://localhost:8080/help - Command Help") + print(" http://localhost:8080/players - Player List") + print(" http://localhost:8080/leaderboard - Leaderboard") + print(" http://localhost:8080/locations - Locations") + print("") + print("Press Ctrl+C to stop") try: server.run() except KeyboardInterrupt: - print('\n🛑 Shutting down web server...') - server.stop() + print("\n✅ Web server stopped") - -if __name__ == '__main__': - run_standalone() +if __name__ == "__main__": + run_standalone() \ No newline at end of file