Clean up codebase and add documentation
- Remove all comments from Python source files for cleaner code - Add comprehensive README.md with installation and usage instructions - Add .gitignore to exclude runtime files and sensitive configuration - Preserve all functionality while improving code readability
This commit is contained in:
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
duckhunt.log*
|
||||||
|
|
||||||
|
# Database and Runtime Data
|
||||||
|
duckhunt.json
|
||||||
|
duckhunt.db
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Configuration (sensitive)
|
||||||
|
config.json
|
||||||
|
config_local.json
|
||||||
|
config_backup.json
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
backup/
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# DuckHunt IRC Bot
|
||||||
|
|
||||||
|
A competitive IRC game bot implementing the classic DuckHunt mechanics with modern features. Players compete to shoot ducks that spawn in channels, managing ammunition, accuracy, and collecting items in a persistent progression system.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core Gameplay
|
||||||
|
- **Duck Spawning**: Ducks appear randomly in configured channels with ASCII art
|
||||||
|
- **Shooting Mechanics**: Players use `!bang` to shoot ducks with limited ammunition
|
||||||
|
- **Accuracy System**: Hit chances based on player skill that improves with successful shots
|
||||||
|
- **Gun Jamming**: Weapons can jam and require reloading based on reliability stats
|
||||||
|
- **Wild Shots**: Shooting without targets results in gun confiscation
|
||||||
|
|
||||||
|
### Progression System
|
||||||
|
- **Experience Points**: Earned from successful duck kills and befriending
|
||||||
|
- **Level System**: 40 levels with titles and increasing XP requirements
|
||||||
|
- **Statistics Tracking**: Comprehensive stats including accuracy, best times, and shot counts
|
||||||
|
- **Leaderboards**: Top player rankings and personal statistics
|
||||||
|
|
||||||
|
### Item System
|
||||||
|
- **Shop**: 8 different items available for purchase with earned money
|
||||||
|
- **Inventory**: Persistent item storage with quantity tracking
|
||||||
|
- **Item Effects**: Consumable and permanent items affecting gameplay
|
||||||
|
- **Competitive Drops**: Items drop to the ground for any player to grab with `!snatch`
|
||||||
|
|
||||||
|
### Gun Mechanics
|
||||||
|
- **Ammunition Management**: Limited shots per magazine with reloading required
|
||||||
|
- **Charger System**: Multiple magazines with reload mechanics
|
||||||
|
- **Gun Confiscation**: Administrative punishment system for wild shooting
|
||||||
|
- **Reliability**: Weapon condition affecting jam probability
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Python 3.7 or higher
|
||||||
|
- asyncio support
|
||||||
|
- SSL/TLS support for secure IRC connections
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install Python dependencies (none required beyond standard library)
|
||||||
|
3. Copy and configure `config.json`
|
||||||
|
4. Run the bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 duckhunt.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The bot uses `config.json` for all configuration. Key sections include:
|
||||||
|
|
||||||
|
### IRC Connection
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": "irc.example.net",
|
||||||
|
"port": 6697,
|
||||||
|
"nick": "DuckHunt",
|
||||||
|
"channels": ["#games"],
|
||||||
|
"ssl": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SASL Authentication
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sasl": {
|
||||||
|
"enabled": true,
|
||||||
|
"username": "bot_username",
|
||||||
|
"password": "bot_password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game Settings
|
||||||
|
- Duck spawn intervals and timing
|
||||||
|
- Sleep hours when ducks don't spawn
|
||||||
|
- Duck type probabilities and rewards
|
||||||
|
- Shop item prices and effects
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Player Commands
|
||||||
|
- `!bang` - Shoot at a duck
|
||||||
|
- `!reload` - Reload your weapon
|
||||||
|
- `!bef` / `!befriend` - Attempt to befriend a duck instead of shooting
|
||||||
|
- `!shop` - View available items for purchase
|
||||||
|
- `!duckstats` - View your personal statistics
|
||||||
|
- `!topduck` - View the leaderboard
|
||||||
|
- `!snatch` - Grab items dropped by other players
|
||||||
|
- `!use <item_number> [target]` - Use an item from inventory
|
||||||
|
- `!sell <item_number>` - Sell an item for half price
|
||||||
|
|
||||||
|
### Admin Commands
|
||||||
|
- `!rearm [player]` - Restore confiscated guns
|
||||||
|
- `!disarm <player>` - Confiscate a player's gun
|
||||||
|
- `!ducklaunch` - Force spawn a duck
|
||||||
|
- `!reset <player> [confirm]` - Reset player statistics
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Modular Design
|
||||||
|
- `duckhuntbot.py` - Main bot class and IRC handling
|
||||||
|
- `game.py` - Duck spawning and game mechanics
|
||||||
|
- `db.py` - Player data persistence and management
|
||||||
|
- `utils.py` - Input validation and IRC message parsing
|
||||||
|
- `sasl.py` - SASL authentication implementation
|
||||||
|
- `logging_utils.py` - Enhanced logging with rotation
|
||||||
|
|
||||||
|
### Database
|
||||||
|
Player data is stored in JSON format with automatic backups. The system handles:
|
||||||
|
- Player statistics and progression
|
||||||
|
- Inventory and item management
|
||||||
|
- Configuration and preferences
|
||||||
|
- Historical data and records
|
||||||
|
|
||||||
|
### Concurrency
|
||||||
|
Built on Python's asyncio framework for handling:
|
||||||
|
- IRC message processing
|
||||||
|
- Duck spawning timers
|
||||||
|
- Background cleanup tasks
|
||||||
|
- Multiple simultaneous players
|
||||||
|
|
||||||
|
## Duck Types
|
||||||
|
|
||||||
|
- **Normal Duck**: Standard rewards and difficulty
|
||||||
|
- **Fast Duck**: Higher XP but harder to hit
|
||||||
|
- **Rare Duck**: Bonus rewards and special drops
|
||||||
|
- **Boss Duck**: Challenging encounters with significant rewards
|
||||||
|
|
||||||
|
## Item Types
|
||||||
|
|
||||||
|
1. **Extra Shots** - Temporary ammunition boost
|
||||||
|
2. **Faster Reload** - Reduced reload time
|
||||||
|
3. **Accuracy Charm** - Permanent accuracy improvement
|
||||||
|
4. **Lucky Charm** - Increased rare duck encounters
|
||||||
|
5. **Friendship Bracelet** - Better befriending success rates
|
||||||
|
6. **Duck Caller** - Faster duck spawning
|
||||||
|
7. **Camouflage** - Temporary stealth mode
|
||||||
|
8. **Energy Drink** - Energy restoration
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The codebase follows clean architecture principles with:
|
||||||
|
- Separation of concerns between IRC, game logic, and data persistence
|
||||||
|
- Comprehensive error handling and logging
|
||||||
|
- Input validation and sanitization
|
||||||
|
- Graceful shutdown handling
|
||||||
|
- Signal-based process management
|
||||||
|
|
||||||
|
### Adding Features
|
||||||
|
New features can be added by:
|
||||||
|
1. Extending the command processing in `duckhuntbot.py`
|
||||||
|
2. Adding game mechanics to `game.py`
|
||||||
|
3. Updating data structures in `db.py`
|
||||||
|
4. Configuring behavior in `config.json`
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
DuckHunt IRC Bot - Main Entry Point
|
DuckHunt IRC Bot - Main Entry Point
|
||||||
"""
|
"""
|
||||||
@@ -8,7 +7,6 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Add src directory to path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from src.duckhuntbot import DuckHuntBot
|
from src.duckhuntbot import DuckHuntBot
|
||||||
@@ -17,15 +15,12 @@ from src.duckhuntbot import DuckHuntBot
|
|||||||
def main():
|
def main():
|
||||||
"""Main entry point for DuckHunt Bot"""
|
"""Main entry point for DuckHunt Bot"""
|
||||||
try:
|
try:
|
||||||
# Load configuration
|
|
||||||
with open('config.json') as f:
|
with open('config.json') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
# Create and run bot
|
|
||||||
bot = DuckHuntBot(config)
|
bot = DuckHuntBot(config)
|
||||||
bot.logger.info("🦆 Starting DuckHunt Bot...")
|
bot.logger.info("🦆 Starting DuckHunt Bot...")
|
||||||
|
|
||||||
# Run the bot
|
|
||||||
asyncio.run(bot.run())
|
asyncio.run(bot.run())
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
src/db.py
26
src/db.py
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Database functionality for DuckHunt Bot
|
Database functionality for DuckHunt Bot
|
||||||
"""
|
"""
|
||||||
@@ -21,7 +20,6 @@ class DuckDB:
|
|||||||
|
|
||||||
def get_config(self, path, default=None):
|
def get_config(self, path, default=None):
|
||||||
"""Helper method to get config values (needs to be set by bot)"""
|
"""Helper method to get config values (needs to be set by bot)"""
|
||||||
# This will be set by the main bot class
|
|
||||||
if hasattr(self, '_config_getter'):
|
if hasattr(self, '_config_getter'):
|
||||||
return self._config_getter(path, default)
|
return self._config_getter(path, default)
|
||||||
return default
|
return default
|
||||||
@@ -48,7 +46,6 @@ class DuckDB:
|
|||||||
def save_database(self):
|
def save_database(self):
|
||||||
"""Save all player data to JSON file with error handling"""
|
"""Save all player data to JSON file with error handling"""
|
||||||
try:
|
try:
|
||||||
# Atomic write to prevent corruption
|
|
||||||
temp_file = f"{self.db_file}.tmp"
|
temp_file = f"{self.db_file}.tmp"
|
||||||
data = {
|
data = {
|
||||||
'players': self.players,
|
'players': self.players,
|
||||||
@@ -57,7 +54,6 @@ class DuckDB:
|
|||||||
with open(temp_file, 'w') as f:
|
with open(temp_file, 'w') as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
# Atomic rename to replace old file
|
|
||||||
os.replace(temp_file, self.db_file)
|
os.replace(temp_file, self.db_file)
|
||||||
|
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -92,7 +88,6 @@ class DuckDB:
|
|||||||
|
|
||||||
if nick in self.players:
|
if nick in self.players:
|
||||||
player = self.players[nick]
|
player = self.players[nick]
|
||||||
# Ensure backward compatibility by adding missing fields
|
|
||||||
self._ensure_player_fields(player)
|
self._ensure_player_fields(player)
|
||||||
return player
|
return player
|
||||||
else:
|
else:
|
||||||
@@ -118,7 +113,6 @@ class DuckDB:
|
|||||||
'level': 1,
|
'level': 1,
|
||||||
'inventory': {},
|
'inventory': {},
|
||||||
'ignored_users': [],
|
'ignored_users': [],
|
||||||
# Gun mechanics (eggdrop style)
|
|
||||||
'jammed': False,
|
'jammed': False,
|
||||||
'jammed_count': 0,
|
'jammed_count': 0,
|
||||||
'total_ammo_used': 0,
|
'total_ammo_used': 0,
|
||||||
@@ -137,24 +131,23 @@ class DuckDB:
|
|||||||
def _ensure_player_fields(self, player):
|
def _ensure_player_fields(self, player):
|
||||||
"""Ensure player has all required fields for backward compatibility"""
|
"""Ensure player has all required fields for backward compatibility"""
|
||||||
required_fields = {
|
required_fields = {
|
||||||
'shots': player.get('ammo', 6), # Map old 'ammo' to 'shots'
|
'shots': player.get('ammo', 6),
|
||||||
'max_shots': player.get('max_ammo', 6), # Map old 'max_ammo' to 'max_shots'
|
'max_shots': player.get('max_ammo', 6),
|
||||||
'chargers': player.get('chargers', 2), # Map old 'chargers' (magazines)
|
'chargers': player.get('chargers', 2),
|
||||||
'max_chargers': player.get('max_chargers', 2), # Map old 'max_chargers'
|
'max_chargers': player.get('max_chargers', 2),
|
||||||
'reload_time': 5.0,
|
'reload_time': 5.0,
|
||||||
'ducks_shot': player.get('caught', 0), # Map old 'caught' to 'ducks_shot'
|
'ducks_shot': player.get('caught', 0),
|
||||||
'ducks_befriended': player.get('befriended', 0), # Use existing befriended count
|
'ducks_befriended': player.get('befriended', 0),
|
||||||
'accuracy_bonus': 0,
|
'accuracy_bonus': 0,
|
||||||
'xp_bonus': 0,
|
'xp_bonus': 0,
|
||||||
'charm_bonus': 0,
|
'charm_bonus': 0,
|
||||||
'exp': player.get('xp', 0), # Map old 'xp' to 'exp'
|
'exp': player.get('xp', 0),
|
||||||
'money': player.get('coins', 100), # Map old 'coins' to 'money'
|
'money': player.get('coins', 100),
|
||||||
'last_hunt': 0,
|
'last_hunt': 0,
|
||||||
'last_reload': 0,
|
'last_reload': 0,
|
||||||
'level': 1,
|
'level': 1,
|
||||||
'inventory': {},
|
'inventory': {},
|
||||||
'ignored_users': [],
|
'ignored_users': [],
|
||||||
# Gun mechanics (eggdrop style)
|
|
||||||
'jammed': False,
|
'jammed': False,
|
||||||
'jammed_count': player.get('jammed_count', 0),
|
'jammed_count': player.get('jammed_count', 0),
|
||||||
'total_ammo_used': player.get('total_ammo_used', 0),
|
'total_ammo_used': player.get('total_ammo_used', 0),
|
||||||
@@ -174,12 +167,11 @@ class DuckDB:
|
|||||||
"""Save player data - batch saves for performance"""
|
"""Save player data - batch saves for performance"""
|
||||||
if not self._save_pending:
|
if not self._save_pending:
|
||||||
self._save_pending = True
|
self._save_pending = True
|
||||||
# Schedule delayed save to batch multiple writes
|
|
||||||
asyncio.create_task(self._delayed_save())
|
asyncio.create_task(self._delayed_save())
|
||||||
|
|
||||||
async def _delayed_save(self):
|
async def _delayed_save(self):
|
||||||
"""Batch save to reduce disk I/O"""
|
"""Batch save to reduce disk I/O"""
|
||||||
await asyncio.sleep(0.5) # Small delay to batch saves
|
await asyncio.sleep(0.5)
|
||||||
try:
|
try:
|
||||||
self.save_database()
|
self.save_database()
|
||||||
self.logger.debug("Database batch save completed")
|
self.logger.debug("Database batch save completed")
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Main DuckHunt IRC Bot
|
Main DuckHunt IRC Bot
|
||||||
"""
|
"""
|
||||||
@@ -33,26 +32,20 @@ class DuckHuntBot:
|
|||||||
self.channels_joined = set()
|
self.channels_joined = set()
|
||||||
self.shutdown_requested = False
|
self.shutdown_requested = False
|
||||||
|
|
||||||
# Initialize subsystems
|
|
||||||
self.db = DuckDB()
|
self.db = DuckDB()
|
||||||
self.db.set_config_getter(self.get_config)
|
self.db.set_config_getter(self.get_config)
|
||||||
self.game = DuckGame(self, self.db)
|
self.game = DuckGame(self, self.db)
|
||||||
|
|
||||||
# Initialize SASL handler
|
|
||||||
self.sasl_handler = SASLHandler(self, config)
|
self.sasl_handler = SASLHandler(self, config)
|
||||||
|
|
||||||
# Admin configuration
|
|
||||||
self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])]
|
self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])]
|
||||||
self.ignored_nicks = set()
|
self.ignored_nicks = set()
|
||||||
|
|
||||||
# Duck spawn timing
|
self.duck_spawn_times = {}
|
||||||
self.duck_spawn_times = {} # Per-channel last spawn times
|
self.channel_records = {}
|
||||||
self.channel_records = {} # Per-channel shooting records
|
|
||||||
|
|
||||||
# Dropped items tracking - competitive snatching system
|
self.dropped_items = {}
|
||||||
self.dropped_items = {} # Per-channel dropped items: {channel: [{'item': item_name, 'timestamp': time, 'dropper': nick}]}
|
|
||||||
|
|
||||||
# Colors for IRC messages
|
|
||||||
self.colors = {
|
self.colors = {
|
||||||
'red': '\x0304',
|
'red': '\x0304',
|
||||||
'green': '\x0303',
|
'green': '\x0303',
|
||||||
@@ -80,12 +73,10 @@ class DuckHuntBot:
|
|||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect to IRC server"""
|
"""Connect to IRC server"""
|
||||||
try:
|
try:
|
||||||
# Setup SSL context if needed
|
|
||||||
ssl_context = None
|
ssl_context = None
|
||||||
if self.config.get('ssl', False):
|
if self.config.get('ssl', False):
|
||||||
ssl_context = ssl.create_default_context()
|
ssl_context = ssl.create_default_context()
|
||||||
|
|
||||||
# Connect to server
|
|
||||||
self.reader, self.writer = await asyncio.open_connection(
|
self.reader, self.writer = await asyncio.open_connection(
|
||||||
self.config['server'],
|
self.config['server'],
|
||||||
self.config['port'],
|
self.config['port'],
|
||||||
@@ -94,11 +85,9 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}")
|
self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}")
|
||||||
|
|
||||||
# Send server password if provided
|
|
||||||
if self.config.get('password'):
|
if self.config.get('password'):
|
||||||
self.send_raw(f"PASS {self.config['password']}")
|
self.send_raw(f"PASS {self.config['password']}")
|
||||||
|
|
||||||
# Register with server
|
|
||||||
await self.register_user()
|
await self.register_user()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -116,7 +105,6 @@ class DuckHuntBot:
|
|||||||
if self.writer and not self.writer.is_closing():
|
if self.writer and not self.writer.is_closing():
|
||||||
try:
|
try:
|
||||||
self.writer.write(f'{msg}\r\n'.encode())
|
self.writer.write(f'{msg}\r\n'.encode())
|
||||||
# No await for drain() - let TCP handle buffering for speed
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error sending message: {e}")
|
self.logger.error(f"Error sending message: {e}")
|
||||||
|
|
||||||
@@ -141,27 +129,23 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
async def send_user_message(self, nick, channel, message, message_type='default'):
|
async def send_user_message(self, nick, channel, message, message_type='default'):
|
||||||
"""Send message to user respecting their output mode preferences"""
|
"""Send message to user respecting their output mode preferences"""
|
||||||
# Get player to check preferences
|
|
||||||
player = self.db.get_player(f"{nick}!user@host")
|
player = self.db.get_player(f"{nick}!user@host")
|
||||||
if not player:
|
if not player:
|
||||||
# Default to public if no player data
|
|
||||||
self.send_message(channel, f"{nick} > {message}")
|
self.send_message(channel, f"{nick} > {message}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check message output configuration
|
|
||||||
force_public_types = self.get_config('message_output.force_public', {}) or {}
|
force_public_types = self.get_config('message_output.force_public', {}) or {}
|
||||||
if force_public_types.get(message_type, False):
|
if force_public_types.get(message_type, False):
|
||||||
self.send_message(channel, f"{nick} > {message}")
|
self.send_message(channel, f"{nick} > {message}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check user preference
|
|
||||||
output_mode = player.get('settings', {}).get('output_mode', 'PUBLIC')
|
output_mode = player.get('settings', {}).get('output_mode', 'PUBLIC')
|
||||||
|
|
||||||
if output_mode == 'NOTICE':
|
if output_mode == 'NOTICE':
|
||||||
self.send_raw(f'NOTICE {nick} :{message}')
|
self.send_raw(f'NOTICE {nick} :{message}')
|
||||||
elif output_mode == 'PRIVMSG':
|
elif output_mode == 'PRIVMSG':
|
||||||
self.send_message(nick, message)
|
self.send_message(nick, message)
|
||||||
else: # PUBLIC or default
|
else:
|
||||||
self.send_message(channel, f"{nick} > {message}")
|
self.send_message(channel, f"{nick} > {message}")
|
||||||
|
|
||||||
async def auto_rearm_confiscated_guns(self, channel, shooter_nick):
|
async def auto_rearm_confiscated_guns(self, channel, shooter_nick):
|
||||||
@@ -169,7 +153,6 @@ class DuckHuntBot:
|
|||||||
if not self.get_config('weapons.auto_rearm_on_duck_shot', True):
|
if not self.get_config('weapons.auto_rearm_on_duck_shot', True):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find players with confiscated guns
|
|
||||||
rearmed_players = []
|
rearmed_players = []
|
||||||
for nick, player in self.db.players.items():
|
for nick, player in self.db.players.items():
|
||||||
if player.get('gun_confiscated', False):
|
if player.get('gun_confiscated', False):
|
||||||
@@ -190,7 +173,6 @@ class DuckHuntBot:
|
|||||||
def signal_handler(signum, frame):
|
def signal_handler(signum, frame):
|
||||||
self.logger.info(f"Received signal {signum}, shutting down...")
|
self.logger.info(f"Received signal {signum}, shutting down...")
|
||||||
self.shutdown_requested = True
|
self.shutdown_requested = True
|
||||||
# Cancel any pending tasks
|
|
||||||
for task in asyncio.all_tasks():
|
for task in asyncio.all_tasks():
|
||||||
if not task.done():
|
if not task.done():
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@@ -203,11 +185,10 @@ class DuckHuntBot:
|
|||||||
async def handle_message(self, prefix, command, params, trailing):
|
async def handle_message(self, prefix, command, params, trailing):
|
||||||
"""Handle incoming IRC messages"""
|
"""Handle incoming IRC messages"""
|
||||||
try:
|
try:
|
||||||
if command == '001': # Welcome message
|
if command == '001':
|
||||||
self.registered = True
|
self.registered = True
|
||||||
self.logger.info("Successfully registered with IRC server")
|
self.logger.info("Successfully registered with IRC server")
|
||||||
|
|
||||||
# Join channels
|
|
||||||
for channel in self.config['channels']:
|
for channel in self.config['channels']:
|
||||||
self.send_raw(f'JOIN {channel}')
|
self.send_raw(f'JOIN {channel}')
|
||||||
|
|
||||||
@@ -222,14 +203,12 @@ class DuckHuntBot:
|
|||||||
target = params[0]
|
target = params[0]
|
||||||
message = trailing
|
message = trailing
|
||||||
|
|
||||||
# Handle commands
|
|
||||||
if message.startswith('!') or target == self.config['nick']:
|
if message.startswith('!') or target == self.config['nick']:
|
||||||
await self.handle_command(prefix, target, message)
|
await self.handle_command(prefix, target, message)
|
||||||
|
|
||||||
elif command == 'PING':
|
elif command == 'PING':
|
||||||
self.send_raw(f'PONG :{trailing}')
|
self.send_raw(f'PONG :{trailing}')
|
||||||
|
|
||||||
# Handle SASL messages
|
|
||||||
elif command == 'CAP':
|
elif command == 'CAP':
|
||||||
await self.sasl_handler.handle_cap_response(params, trailing)
|
await self.sasl_handler.handle_cap_response(params, trailing)
|
||||||
elif command == 'AUTHENTICATE':
|
elif command == 'AUTHENTICATE':
|
||||||
@@ -249,24 +228,19 @@ class DuckHuntBot:
|
|||||||
nick = user.split('!')[0]
|
nick = user.split('!')[0]
|
||||||
nick_lower = nick.lower()
|
nick_lower = nick.lower()
|
||||||
|
|
||||||
# Input validation
|
|
||||||
if not InputValidator.validate_nickname(nick):
|
if not InputValidator.validate_nickname(nick):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if user is ignored
|
|
||||||
if nick_lower in self.ignored_nicks:
|
if nick_lower in self.ignored_nicks:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Sanitize message
|
|
||||||
message = InputValidator.sanitize_message(message)
|
message = InputValidator.sanitize_message(message)
|
||||||
if not message:
|
if not message:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine response target
|
|
||||||
is_private = channel == self.config['nick']
|
is_private = channel == self.config['nick']
|
||||||
response_target = nick if is_private else channel
|
response_target = nick if is_private else channel
|
||||||
|
|
||||||
# Remove ! prefix for public commands
|
|
||||||
if message.startswith('!'):
|
if message.startswith('!'):
|
||||||
cmd_parts = message[1:].split()
|
cmd_parts = message[1:].split()
|
||||||
else:
|
else:
|
||||||
@@ -278,12 +252,10 @@ class DuckHuntBot:
|
|||||||
cmd = cmd_parts[0].lower()
|
cmd = cmd_parts[0].lower()
|
||||||
args = cmd_parts[1:] if len(cmd_parts) > 1 else []
|
args = cmd_parts[1:] if len(cmd_parts) > 1 else []
|
||||||
|
|
||||||
# Get player data
|
|
||||||
player = self.db.get_player(user)
|
player = self.db.get_player(user)
|
||||||
if not player:
|
if not player:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle commands
|
|
||||||
await self.process_command(nick, response_target, cmd, args, player, user)
|
await self.process_command(nick, response_target, cmd, args, player, user)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -291,7 +263,6 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
async def process_command(self, nick, target, cmd, args, player, user):
|
async def process_command(self, nick, target, cmd, args, player, user):
|
||||||
"""Process individual commands"""
|
"""Process individual commands"""
|
||||||
# Game commands
|
|
||||||
if cmd == 'bang':
|
if cmd == 'bang':
|
||||||
await self.handle_bang(nick, target, player)
|
await self.handle_bang(nick, target, player)
|
||||||
elif cmd == 'reload':
|
elif cmd == 'reload':
|
||||||
@@ -329,7 +300,6 @@ class DuckHuntBot:
|
|||||||
await self.handle_topduck(nick, target)
|
await self.handle_topduck(nick, target)
|
||||||
elif cmd == 'snatch':
|
elif cmd == 'snatch':
|
||||||
await self.handle_snatch(nick, target, player)
|
await self.handle_snatch(nick, target, player)
|
||||||
# Admin commands
|
|
||||||
elif cmd == 'rearm' and self.is_admin(user):
|
elif cmd == 'rearm' and self.is_admin(user):
|
||||||
target_nick = args[0] if args else None
|
target_nick = args[0] if args else None
|
||||||
await self.handle_rearm(nick, target, player, target_nick)
|
await self.handle_rearm(nick, target, player, target_nick)
|
||||||
@@ -346,36 +316,29 @@ class DuckHuntBot:
|
|||||||
else:
|
else:
|
||||||
await self.send_user_message(nick, target, "Usage: !reset <player> [confirm]")
|
await self.send_user_message(nick, target, "Usage: !reset <player> [confirm]")
|
||||||
else:
|
else:
|
||||||
# Unknown command
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_bang(self, nick, channel, player):
|
async def handle_bang(self, nick, channel, player):
|
||||||
"""Handle !bang command - shoot at duck (eggdrop style)"""
|
"""Handle !bang command - shoot at duck (eggdrop style)"""
|
||||||
# Check if gun is confiscated
|
|
||||||
if player.get('gun_confiscated', False):
|
if player.get('gun_confiscated', False):
|
||||||
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot shoot.")
|
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot shoot.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if gun is jammed
|
|
||||||
if player.get('jammed', False):
|
if player.get('jammed', False):
|
||||||
message = f"{nick} > Gun jammed! Use !reload"
|
message = f"{nick} > Gun jammed! Use !reload"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if player has ammunition
|
|
||||||
if player['shots'] <= 0:
|
if player['shots'] <= 0:
|
||||||
message = f"{nick} > *click* You're out of ammo! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
message = f"{nick} > *click* You're out of ammo! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if channel has ducks
|
|
||||||
if channel not in self.game.ducks or not self.game.ducks[channel]:
|
if channel not in self.game.ducks or not self.game.ducks[channel]:
|
||||||
# Fire shot anyway (wild shot into air)
|
|
||||||
player['shots'] -= 1
|
player['shots'] -= 1
|
||||||
player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1
|
player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1
|
||||||
player['wild_shots'] = player.get('wild_shots', 0) + 1
|
player['wild_shots'] = player.get('wild_shots', 0) + 1
|
||||||
|
|
||||||
# Check for gun jam after shooting
|
|
||||||
if self.game.gun_jams(player):
|
if self.game.gun_jams(player):
|
||||||
player['jammed'] = True
|
player['jammed'] = True
|
||||||
player['jammed_count'] = player.get('jammed_count', 0) + 1
|
player['jammed_count'] = player.get('jammed_count', 0) + 1
|
||||||
@@ -383,7 +346,6 @@ class DuckHuntBot:
|
|||||||
else:
|
else:
|
||||||
message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}"
|
message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}"
|
||||||
|
|
||||||
# Confiscate gun for wild shooting
|
|
||||||
player['gun_confiscated'] = True
|
player['gun_confiscated'] = True
|
||||||
player['confiscated_count'] = player.get('confiscated_count', 0) + 1
|
player['confiscated_count'] = player.get('confiscated_count', 0) + 1
|
||||||
|
|
||||||
@@ -391,43 +353,35 @@ class DuckHuntBot:
|
|||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get first duck in channel
|
|
||||||
duck = self.game.ducks[channel][0]
|
duck = self.game.ducks[channel][0]
|
||||||
player['shots'] -= 1
|
player['shots'] -= 1
|
||||||
player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1
|
player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1
|
||||||
player['shot_at'] = player.get('shot_at', 0) + 1
|
player['shot_at'] = player.get('shot_at', 0) + 1
|
||||||
|
|
||||||
# Check for gun jam first (before checking hit)
|
|
||||||
if self.game.gun_jams(player):
|
if self.game.gun_jams(player):
|
||||||
player['jammed'] = True
|
player['jammed'] = True
|
||||||
player['jammed_count'] = player.get('jammed_count', 0) + 1
|
player['jammed_count'] = player.get('jammed_count', 0) + 1
|
||||||
message = f"{nick} > *BANG* *click* Gun jammed while shooting! | Ammo: {player['shots']}/{player['max_shots']}"
|
message = f"{nick} > *BANG* *click* Gun jammed while shooting! | Ammo: {player['shots']}/{player['max_shots']}"
|
||||||
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
||||||
else:
|
else:
|
||||||
# Check if duck was hit (based on accuracy)
|
|
||||||
hit_chance = min(0.7 + (player.get('accuracy', 0) * 0.001), 0.95)
|
hit_chance = min(0.7 + (player.get('accuracy', 0) * 0.001), 0.95)
|
||||||
if random.random() < hit_chance:
|
if random.random() < hit_chance:
|
||||||
await self.handle_duck_hit(nick, channel, player, duck)
|
await self.handle_duck_hit(nick, channel, player, duck)
|
||||||
else:
|
else:
|
||||||
await self.handle_duck_miss(nick, channel, player)
|
await self.handle_duck_miss(nick, channel, player)
|
||||||
|
|
||||||
# Save player data
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
async def handle_duck_hit(self, nick, channel, player, duck):
|
async def handle_duck_hit(self, nick, channel, player, duck):
|
||||||
"""Handle successful duck hit (eggdrop style)"""
|
"""Handle successful duck hit (eggdrop style)"""
|
||||||
# Remove duck from channel
|
|
||||||
self.game.ducks[channel].remove(duck)
|
self.game.ducks[channel].remove(duck)
|
||||||
|
|
||||||
# Calculate reaction time if available
|
|
||||||
shot_time = time.time()
|
shot_time = time.time()
|
||||||
reaction_time = shot_time - duck.get('spawn_time', shot_time)
|
reaction_time = shot_time - duck.get('spawn_time', shot_time)
|
||||||
|
|
||||||
# Award points and XP based on duck type
|
|
||||||
points_earned = duck['points']
|
points_earned = duck['points']
|
||||||
xp_earned = duck['xp']
|
xp_earned = duck['xp']
|
||||||
|
|
||||||
# Bonus for quick shots
|
|
||||||
if reaction_time < 2.0:
|
if reaction_time < 2.0:
|
||||||
quick_bonus = int(points_earned * 0.5)
|
quick_bonus = int(points_earned * 0.5)
|
||||||
points_earned += quick_bonus
|
points_earned += quick_bonus
|
||||||
@@ -435,55 +389,43 @@ class DuckHuntBot:
|
|||||||
else:
|
else:
|
||||||
quick_shot_msg = ""
|
quick_shot_msg = ""
|
||||||
|
|
||||||
# Apply XP bonus
|
|
||||||
xp_earned = int(xp_earned * (1 + player.get('xp_bonus', 0) * 0.001))
|
xp_earned = int(xp_earned * (1 + player.get('xp_bonus', 0) * 0.001))
|
||||||
|
|
||||||
# Update player stats
|
|
||||||
player['ducks_shot'] += 1
|
player['ducks_shot'] += 1
|
||||||
player['exp'] += xp_earned
|
player['exp'] += xp_earned
|
||||||
player['money'] += points_earned
|
player['money'] += points_earned
|
||||||
player['last_hunt'] = time.time()
|
player['last_hunt'] = time.time()
|
||||||
|
|
||||||
# Update accuracy (reward hits)
|
|
||||||
current_accuracy = player.get('accuracy', 65)
|
current_accuracy = player.get('accuracy', 65)
|
||||||
player['accuracy'] = min(current_accuracy + 1, 95)
|
player['accuracy'] = min(current_accuracy + 1, 95)
|
||||||
|
|
||||||
# Track best time
|
|
||||||
if 'best_time' not in player or reaction_time < player['best_time']:
|
if 'best_time' not in player or reaction_time < player['best_time']:
|
||||||
player['best_time'] = reaction_time
|
player['best_time'] = reaction_time
|
||||||
|
|
||||||
# Store reflex data
|
|
||||||
player['total_reflex_time'] = player.get('total_reflex_time', 0) + reaction_time
|
player['total_reflex_time'] = player.get('total_reflex_time', 0) + reaction_time
|
||||||
player['reflex_shots'] = player.get('reflex_shots', 0) + 1
|
player['reflex_shots'] = player.get('reflex_shots', 0) + 1
|
||||||
|
|
||||||
# Level up check
|
|
||||||
await self.check_level_up(nick, channel, player)
|
await self.check_level_up(nick, channel, player)
|
||||||
|
|
||||||
# Eggdrop style hit message - exact format match
|
|
||||||
message = f"{nick} > *BANG* you shot down the duck in {reaction_time:.2f} seconds. \\_X< *KWAK* [+{xp_earned} xp] [TOTAL DUCKS: {player['ducks_shot']}]"
|
message = f"{nick} > *BANG* you shot down the duck in {reaction_time:.2f} seconds. \\_X< *KWAK* [+{xp_earned} xp] [TOTAL DUCKS: {player['ducks_shot']}]"
|
||||||
self.send_message(channel, f"{self.colors['green']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['green']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Award items occasionally - drop to ground for snatching
|
if random.random() < 0.1:
|
||||||
if random.random() < 0.1: # 10% chance
|
|
||||||
await self.drop_random_item(nick, channel)
|
await self.drop_random_item(nick, channel)
|
||||||
|
|
||||||
async def handle_duck_miss(self, nick, channel, player):
|
async def handle_duck_miss(self, nick, channel, player):
|
||||||
"""Handle duck miss (eggdrop style)"""
|
"""Handle duck miss (eggdrop style)"""
|
||||||
# Reduce accuracy (penalize misses)
|
|
||||||
current_accuracy = player.get('accuracy', 65)
|
current_accuracy = player.get('accuracy', 65)
|
||||||
player['accuracy'] = max(current_accuracy - 2, 10)
|
player['accuracy'] = max(current_accuracy - 2, 10)
|
||||||
|
|
||||||
# Track misses
|
|
||||||
player['missed'] = player.get('missed', 0) + 1
|
player['missed'] = player.get('missed', 0) + 1
|
||||||
|
|
||||||
# Eggdrop style miss message
|
|
||||||
message = f"{nick} > *BANG* You missed the duck! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
message = f"{nick} > *BANG* You missed the duck! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
||||||
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Chance to scare other ducks with the noise
|
|
||||||
if channel in self.game.ducks and len(self.game.ducks[channel]) > 1:
|
if channel in self.game.ducks and len(self.game.ducks[channel]) > 1:
|
||||||
for other_duck in self.game.ducks[channel][:]:
|
for other_duck in self.game.ducks[channel][:]:
|
||||||
if random.random() < 0.2: # 20% chance each duck gets scared
|
if random.random() < 0.2:
|
||||||
self.game.ducks[channel].remove(other_duck)
|
self.game.ducks[channel].remove(other_duck)
|
||||||
self.send_message(channel, f"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The other ducks fly away, scared by the noise!")
|
self.send_message(channel, f"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The other ducks fly away, scared by the noise!")
|
||||||
|
|
||||||
@@ -491,14 +433,11 @@ class DuckHuntBot:
|
|||||||
"""Handle reload command (eggdrop style) - reload ammo and clear jams"""
|
"""Handle reload command (eggdrop style) - reload ammo and clear jams"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
# Check if gun is confiscated
|
|
||||||
if player.get('gun_confiscated', False):
|
if player.get('gun_confiscated', False):
|
||||||
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot reload.")
|
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot reload.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if gun is jammed
|
|
||||||
if player.get('jammed', False):
|
if player.get('jammed', False):
|
||||||
# Clear the jam
|
|
||||||
player['jammed'] = False
|
player['jammed'] = False
|
||||||
player['last_reload'] = current_time
|
player['last_reload'] = current_time
|
||||||
|
|
||||||
@@ -507,29 +446,25 @@ class DuckHuntBot:
|
|||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if already full
|
|
||||||
if player['shots'] >= player['max_shots']:
|
if player['shots'] >= player['max_shots']:
|
||||||
message = f"{nick} > Gun is already fully loaded! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
message = f"{nick} > Gun is already fully loaded! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if have chargers to reload with
|
|
||||||
if player.get('chargers', 0) <= 0:
|
if player.get('chargers', 0) <= 0:
|
||||||
message = f"{nick} > No chargers left to reload with! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: 0/{player.get('max_chargers', 2)}"
|
message = f"{nick} > No chargers left to reload with! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: 0/{player.get('max_chargers', 2)}"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check reload time cooldown
|
|
||||||
if current_time - player.get('last_reload', 0) < player['reload_time']:
|
if current_time - player.get('last_reload', 0) < player['reload_time']:
|
||||||
remaining = int(player['reload_time'] - (current_time - player.get('last_reload', 0)))
|
remaining = int(player['reload_time'] - (current_time - player.get('last_reload', 0)))
|
||||||
message = f"{nick} > Reload cooldown: {remaining} seconds remaining | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
message = f"{nick} > Reload cooldown: {remaining} seconds remaining | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Perform reload
|
|
||||||
old_shots = player['shots']
|
old_shots = player['shots']
|
||||||
player['shots'] = player['max_shots']
|
player['shots'] = player['max_shots']
|
||||||
player['chargers'] = max(0, player.get('chargers', 2) - 1) # Use one charger
|
player['chargers'] = max(0, player.get('chargers', 2) - 1)
|
||||||
player['last_reload'] = current_time
|
player['last_reload'] = current_time
|
||||||
shots_added = player['shots'] - old_shots
|
shots_added = player['shots'] - old_shots
|
||||||
|
|
||||||
@@ -537,27 +472,21 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Save player data
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
async def handle_befriend(self, nick, channel, player):
|
async def handle_befriend(self, nick, channel, player):
|
||||||
"""Handle !bef command - befriend a duck"""
|
"""Handle !bef command - befriend a duck"""
|
||||||
# Check if channel has ducks
|
|
||||||
if channel not in self.game.ducks or not self.game.ducks[channel]:
|
if channel not in self.game.ducks or not self.game.ducks[channel]:
|
||||||
await self.send_user_message(nick, channel, "There are no ducks to befriend!")
|
await self.send_user_message(nick, channel, "There are no ducks to befriend!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get first duck
|
|
||||||
duck = self.game.ducks[channel][0]
|
duck = self.game.ducks[channel][0]
|
||||||
|
|
||||||
# Befriend success rate (starts at 50%, can be improved with items)
|
|
||||||
befriend_chance = 0.5 + (player.get('charm_bonus', 0) * 0.001)
|
befriend_chance = 0.5 + (player.get('charm_bonus', 0) * 0.001)
|
||||||
|
|
||||||
if random.random() < befriend_chance:
|
if random.random() < befriend_chance:
|
||||||
# Remove duck from channel
|
|
||||||
self.game.ducks[channel].remove(duck)
|
self.game.ducks[channel].remove(duck)
|
||||||
|
|
||||||
# Award XP and friendship bonus
|
|
||||||
xp_earned = duck['xp']
|
xp_earned = duck['xp']
|
||||||
friendship_bonus = duck['points'] // 2
|
friendship_bonus = duck['points'] // 2
|
||||||
|
|
||||||
@@ -565,17 +494,15 @@ class DuckHuntBot:
|
|||||||
player['money'] += friendship_bonus
|
player['money'] += friendship_bonus
|
||||||
player['ducks_befriended'] += 1
|
player['ducks_befriended'] += 1
|
||||||
|
|
||||||
# Level up check
|
|
||||||
await self.check_level_up(nick, channel, player)
|
await self.check_level_up(nick, channel, player)
|
||||||
|
|
||||||
# Random positive effect
|
|
||||||
effects = [
|
effects = [
|
||||||
("luck", 10, "You feel lucky!"),
|
("luck", 10, "You feel lucky!"),
|
||||||
("charm_bonus", 5, "The duck teaches you about friendship!"),
|
("charm_bonus", 5, "The duck teaches you about friendship!"),
|
||||||
("accuracy_bonus", 3, "The duck gives you aiming tips!")
|
("accuracy_bonus", 3, "The duck gives you aiming tips!")
|
||||||
]
|
]
|
||||||
|
|
||||||
if random.random() < 0.3: # 30% chance for bonus effect
|
if random.random() < 0.3:
|
||||||
effect, amount, message = random.choice(effects)
|
effect, amount, message = random.choice(effects)
|
||||||
player[effect] = player.get(effect, 0) + amount
|
player[effect] = player.get(effect, 0) + amount
|
||||||
bonus_msg = f" {message}"
|
bonus_msg = f" {message}"
|
||||||
@@ -586,11 +513,9 @@ class DuckHuntBot:
|
|||||||
f"+{friendship_bonus} coins, +{xp_earned} XP.{bonus_msg}")
|
f"+{friendship_bonus} coins, +{xp_earned} XP.{bonus_msg}")
|
||||||
self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Award items occasionally
|
if random.random() < 0.15:
|
||||||
if random.random() < 0.15: # 15% chance (higher than shooting)
|
|
||||||
await self.award_random_item(nick, channel, player)
|
await self.award_random_item(nick, channel, player)
|
||||||
else:
|
else:
|
||||||
# Befriend failed
|
|
||||||
miss_messages = [
|
miss_messages = [
|
||||||
f"The {duck['type']} duck doesn't trust you yet!",
|
f"The {duck['type']} duck doesn't trust you yet!",
|
||||||
f"The {duck['type']} duck flies away from you!",
|
f"The {duck['type']} duck flies away from you!",
|
||||||
@@ -601,10 +526,8 @@ class DuckHuntBot:
|
|||||||
message = f"{nick} {random.choice(miss_messages)}"
|
message = f"{nick} {random.choice(miss_messages)}"
|
||||||
self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Small penalty to charm
|
|
||||||
player['charm_bonus'] = max(player.get('charm_bonus', 0) - 1, -50)
|
player['charm_bonus'] = max(player.get('charm_bonus', 0) - 1, -50)
|
||||||
|
|
||||||
# Save player data
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
async def handle_shop(self, nick, channel, player):
|
async def handle_shop(self, nick, channel, player):
|
||||||
@@ -634,7 +557,6 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "Invalid item ID!")
|
await self.send_user_message(nick, channel, "Invalid item ID!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if player has the item
|
|
||||||
if 'inventory' not in player:
|
if 'inventory' not in player:
|
||||||
player['inventory'] = {}
|
player['inventory'] = {}
|
||||||
|
|
||||||
@@ -643,7 +565,6 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "You don't have that item!")
|
await self.send_user_message(nick, channel, "You don't have that item!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get item info from built-in shop items
|
|
||||||
shop_items = {
|
shop_items = {
|
||||||
1: {'name': 'Extra Shots', 'price': 50},
|
1: {'name': 'Extra Shots', 'price': 50},
|
||||||
2: {'name': 'Faster Reload', 'price': 100},
|
2: {'name': 'Faster Reload', 'price': 100},
|
||||||
@@ -659,7 +580,6 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "Invalid item!")
|
await self.send_user_message(nick, channel, "Invalid item!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove item and add money (50% of original price)
|
|
||||||
player['inventory'][item_key] -= 1
|
player['inventory'][item_key] -= 1
|
||||||
if player['inventory'][item_key] <= 0:
|
if player['inventory'][item_key] <= 0:
|
||||||
del player['inventory'][item_key]
|
del player['inventory'][item_key]
|
||||||
@@ -670,7 +590,6 @@ class DuckHuntBot:
|
|||||||
message = f"Sold {item_info['name']} for ${sell_price}!"
|
message = f"Sold {item_info['name']} for ${sell_price}!"
|
||||||
await self.send_user_message(nick, channel, message)
|
await self.send_user_message(nick, channel, message)
|
||||||
|
|
||||||
# Save player data
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
async def handle_use(self, nick, channel, item_id, player, target_nick=None):
|
async def handle_use(self, nick, channel, item_id, player, target_nick=None):
|
||||||
@@ -681,11 +600,9 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "Invalid item ID!")
|
await self.send_user_message(nick, channel, "Invalid item ID!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if it's a shop purchase or inventory use
|
|
||||||
if 'inventory' not in player:
|
if 'inventory' not in player:
|
||||||
player['inventory'] = {}
|
player['inventory'] = {}
|
||||||
|
|
||||||
# Get item info from built-in shop items
|
|
||||||
shop_items = {
|
shop_items = {
|
||||||
1: {'name': 'Extra Shots', 'price': 50, 'consumable': True},
|
1: {'name': 'Extra Shots', 'price': 50, 'consumable': True},
|
||||||
2: {'name': 'Faster Reload', 'price': 100, 'consumable': True},
|
2: {'name': 'Faster Reload', 'price': 100, 'consumable': True},
|
||||||
@@ -704,22 +621,17 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "Invalid item ID!")
|
await self.send_user_message(nick, channel, "Invalid item ID!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if player owns the item
|
|
||||||
if item_key in player['inventory'] and player['inventory'][item_key] > 0:
|
if item_key in player['inventory'] and player['inventory'][item_key] > 0:
|
||||||
# Use owned item
|
|
||||||
await self.use_item_effect(player, item_id, nick, channel, target_nick)
|
await self.use_item_effect(player, item_id, nick, channel, target_nick)
|
||||||
player['inventory'][item_key] -= 1
|
player['inventory'][item_key] -= 1
|
||||||
if player['inventory'][item_key] <= 0:
|
if player['inventory'][item_key] <= 0:
|
||||||
del player['inventory'][item_key]
|
del player['inventory'][item_key]
|
||||||
else:
|
else:
|
||||||
# Try to buy item
|
|
||||||
if player['money'] >= item_info['price']:
|
if player['money'] >= item_info['price']:
|
||||||
if item_info.get('consumable', True):
|
if item_info.get('consumable', True):
|
||||||
# Buy and immediately use consumable item
|
|
||||||
player['money'] -= item_info['price']
|
player['money'] -= item_info['price']
|
||||||
await self.use_item_effect(player, item_id, nick, channel, target_nick)
|
await self.use_item_effect(player, item_id, nick, channel, target_nick)
|
||||||
else:
|
else:
|
||||||
# Buy permanent item and add to inventory
|
|
||||||
player['money'] -= item_info['price']
|
player['money'] -= item_info['price']
|
||||||
player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1
|
player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1
|
||||||
await self.send_user_message(nick, channel, f"Purchased {item_info['name']}!")
|
await self.send_user_message(nick, channel, f"Purchased {item_info['name']}!")
|
||||||
@@ -728,12 +640,10 @@ class DuckHuntBot:
|
|||||||
f"Not enough money! Need ${item_info['price']}, you have ${player['money']}")
|
f"Not enough money! Need ${item_info['price']}, you have ${player['money']}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save player data
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
|
|
||||||
async def handle_topduck(self, nick, channel):
|
async def handle_topduck(self, nick, channel):
|
||||||
"""Handle topduck command - show leaderboard"""
|
"""Handle topduck command - show leaderboard"""
|
||||||
# Sort players by ducks shot
|
|
||||||
sorted_players = sorted(
|
sorted_players = sorted(
|
||||||
[(name, data) for name, data in self.db.players.items()],
|
[(name, data) for name, data in self.db.players.items()],
|
||||||
key=lambda x: x[1]['ducks_shot'],
|
key=lambda x: x[1]['ducks_shot'],
|
||||||
@@ -744,7 +654,6 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, "No players found!")
|
await self.send_user_message(nick, channel, "No players found!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show top 5
|
|
||||||
await self.send_user_message(nick, channel, "=== TOP DUCK HUNTERS ===")
|
await self.send_user_message(nick, channel, "=== TOP DUCK HUNTERS ===")
|
||||||
for i, (name, data) in enumerate(sorted_players[:5], 1):
|
for i, (name, data) in enumerate(sorted_players[:5], 1):
|
||||||
stats = f"{i}. {name}: {data['ducks_shot']} ducks (Level {data['level']})"
|
stats = f"{i}. {name}: {data['ducks_shot']} ducks (Level {data['level']})"
|
||||||
@@ -754,59 +663,49 @@ class DuckHuntBot:
|
|||||||
"""Handle snatch command - grab dropped items competitively"""
|
"""Handle snatch command - grab dropped items competitively"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Check if there are any dropped items in this channel
|
|
||||||
if channel not in self.dropped_items or not self.dropped_items[channel]:
|
if channel not in self.dropped_items or not self.dropped_items[channel]:
|
||||||
await self.send_user_message(nick, channel, f"{nick} > There are no items to snatch!")
|
await self.send_user_message(nick, channel, f"{nick} > There are no items to snatch!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the oldest dropped item (first come, first served)
|
|
||||||
item = self.dropped_items[channel].pop(0)
|
item = self.dropped_items[channel].pop(0)
|
||||||
|
|
||||||
# Check if item has expired (60 seconds timeout)
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - item['timestamp'] > 60:
|
if current_time - item['timestamp'] > 60:
|
||||||
await self.send_user_message(nick, channel, f"{nick} > The item has disappeared!")
|
await self.send_user_message(nick, channel, f"{nick} > The item has disappeared!")
|
||||||
# Clean up any other expired items while we're at it
|
|
||||||
self.dropped_items[channel] = [
|
self.dropped_items[channel] = [
|
||||||
i for i in self.dropped_items[channel]
|
i for i in self.dropped_items[channel]
|
||||||
if current_time - i['timestamp'] <= 60
|
if current_time - i['timestamp'] <= 60
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialize player inventory if needed
|
|
||||||
if 'inventory' not in player:
|
if 'inventory' not in player:
|
||||||
player['inventory'] = {}
|
player['inventory'] = {}
|
||||||
|
|
||||||
# Add item to player's inventory
|
|
||||||
item_key = item['item_id']
|
item_key = item['item_id']
|
||||||
player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1
|
player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1
|
||||||
|
|
||||||
# Success message - eggdrop style
|
|
||||||
message = f"{nick} snatched a {item['item_name']}! ⚡"
|
message = f"{nick} snatched a {item['item_name']}! ⚡"
|
||||||
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
async def handle_rearm(self, nick, channel, player, target_nick=None):
|
async def handle_rearm(self, nick, channel, player, target_nick=None):
|
||||||
"""Handle rearm command - restore confiscated guns"""
|
"""Handle rearm command - restore confiscated guns"""
|
||||||
if target_nick:
|
if target_nick:
|
||||||
# Rearm another player (admin only)
|
|
||||||
target_player = self.db.get_player(target_nick.lower())
|
target_player = self.db.get_player(target_nick.lower())
|
||||||
if target_player:
|
if target_player:
|
||||||
target_player['gun_confiscated'] = False
|
target_player['gun_confiscated'] = False
|
||||||
target_player['shots'] = target_player['max_shots']
|
target_player['shots'] = target_player['max_shots']
|
||||||
target_player['chargers'] = target_player.get('max_chargers', 2)
|
target_player['chargers'] = target_player.get('max_chargers', 2)
|
||||||
target_player['jammed'] = False
|
target_player['jammed'] = False
|
||||||
target_player['last_reload'] = 0 # Reset reload timer
|
target_player['last_reload'] = 0
|
||||||
message = f"{nick} returned {target_nick}'s confiscated gun! | Ammo: {target_player['shots']}/{target_player['max_shots']} | Chargers: {target_player['chargers']}/{target_player.get('max_chargers', 2)}"
|
message = f"{nick} returned {target_nick}'s confiscated gun! | Ammo: {target_player['shots']}/{target_player['max_shots']} | Chargers: {target_player['chargers']}/{target_player.get('max_chargers', 2)}"
|
||||||
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}")
|
||||||
else:
|
else:
|
||||||
await self.send_user_message(nick, channel, "Player not found!")
|
await self.send_user_message(nick, channel, "Player not found!")
|
||||||
else:
|
else:
|
||||||
# Check if gun is confiscated
|
|
||||||
if not player.get('gun_confiscated', False):
|
if not player.get('gun_confiscated', False):
|
||||||
await self.send_user_message(nick, channel, f"{nick} > Your gun is not confiscated!")
|
await self.send_user_message(nick, channel, f"{nick} > Your gun is not confiscated!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Rearm self (automatic gun return system or admin)
|
|
||||||
if self.is_admin(nick):
|
if self.is_admin(nick):
|
||||||
player['gun_confiscated'] = False
|
player['gun_confiscated'] = False
|
||||||
player['shots'] = player['max_shots']
|
player['shots'] = player['max_shots']
|
||||||
@@ -819,8 +718,6 @@ class DuckHuntBot:
|
|||||||
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! Wait for an admin or automatic return.")
|
await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! Wait for an admin or automatic return.")
|
||||||
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
# Save database
|
|
||||||
self.db.save_database()
|
|
||||||
|
|
||||||
async def handle_disarm(self, nick, channel, target_nick):
|
async def handle_disarm(self, nick, channel, target_nick):
|
||||||
"""Handle disarm command (admin only)"""
|
"""Handle disarm command (admin only)"""
|
||||||
@@ -830,7 +727,6 @@ class DuckHuntBot:
|
|||||||
message = f"Admin {nick} disarmed {target_nick}!"
|
message = f"Admin {nick} disarmed {target_nick}!"
|
||||||
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
# Save database
|
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
else:
|
else:
|
||||||
await self.send_user_message(nick, channel, "Player not found!")
|
await self.send_user_message(nick, channel, "Player not found!")
|
||||||
@@ -856,9 +752,7 @@ class DuckHuntBot:
|
|||||||
)
|
)
|
||||||
await self.send_user_message(nick, channel, stats_msg)
|
await self.send_user_message(nick, channel, stats_msg)
|
||||||
|
|
||||||
# Show inventory if player has items
|
|
||||||
if 'inventory' in player and player['inventory']:
|
if 'inventory' in player and player['inventory']:
|
||||||
# Get item names
|
|
||||||
shop_items = {
|
shop_items = {
|
||||||
1: 'Extra Shots', 2: 'Faster Reload', 3: 'Accuracy Charm', 4: 'Lucky Charm',
|
1: 'Extra Shots', 2: 'Faster Reload', 3: 'Accuracy Charm', 4: 'Lucky Charm',
|
||||||
5: 'Friendship Bracelet', 6: 'Duck Caller', 7: 'Camouflage', 8: 'Energy Drink',
|
5: 'Friendship Bracelet', 6: 'Duck Caller', 7: 'Camouflage', 8: 'Energy Drink',
|
||||||
@@ -950,9 +844,8 @@ class DuckHuntBot:
|
|||||||
if new_level > current_level:
|
if new_level > current_level:
|
||||||
player['level'] = new_level
|
player['level'] = new_level
|
||||||
|
|
||||||
# Award level-up bonuses
|
player['max_shots'] = min(player['max_shots'] + 1, 10)
|
||||||
player['max_shots'] = min(player['max_shots'] + 1, 10) # Max 10 shots
|
player['reload_time'] = max(player['reload_time'] - 0.5, 2.0)
|
||||||
player['reload_time'] = max(player['reload_time'] - 0.5, 2.0) # Min 2 seconds
|
|
||||||
|
|
||||||
message = (f"🎉 {nick} leveled up to level {new_level}! "
|
message = (f"🎉 {nick} leveled up to level {new_level}! "
|
||||||
f"Max shots: {player['max_shots']}, "
|
f"Max shots: {player['max_shots']}, "
|
||||||
@@ -961,7 +854,6 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
def calculate_level(self, exp):
|
def calculate_level(self, exp):
|
||||||
"""Calculate level from experience points"""
|
"""Calculate level from experience points"""
|
||||||
# Exponential level curve: level = floor(sqrt(exp / 100))
|
|
||||||
import math
|
import math
|
||||||
return int(math.sqrt(exp / 100)) + 1
|
return int(math.sqrt(exp / 100)) + 1
|
||||||
|
|
||||||
@@ -973,7 +865,6 @@ class DuckHuntBot:
|
|||||||
"""Drop a random item to the ground for competitive snatching"""
|
"""Drop a random item to the ground for competitive snatching"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Simple random item IDs
|
|
||||||
item_ids = [1, 2, 3, 4, 5, 6, 7, 8]
|
item_ids = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
item_id = random.choice(item_ids)
|
item_id = random.choice(item_ids)
|
||||||
item_key = str(item_id)
|
item_key = str(item_id)
|
||||||
@@ -986,11 +877,9 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
item_name = item_names.get(item_key, f'Item {item_id}')
|
item_name = item_names.get(item_key, f'Item {item_id}')
|
||||||
|
|
||||||
# Initialize channel dropped items if needed
|
|
||||||
if channel not in self.dropped_items:
|
if channel not in self.dropped_items:
|
||||||
self.dropped_items[channel] = []
|
self.dropped_items[channel] = []
|
||||||
|
|
||||||
# Add item to dropped items
|
|
||||||
dropped_item = {
|
dropped_item = {
|
||||||
'item_id': item_key,
|
'item_id': item_key,
|
||||||
'item_name': item_name,
|
'item_name': item_name,
|
||||||
@@ -999,7 +888,6 @@ class DuckHuntBot:
|
|||||||
}
|
}
|
||||||
self.dropped_items[channel].append(dropped_item)
|
self.dropped_items[channel].append(dropped_item)
|
||||||
|
|
||||||
# Announce the drop - eggdrop style
|
|
||||||
message = f"🎁 A {item_name} has been dropped! Type !snatch to grab it!"
|
message = f"🎁 A {item_name} has been dropped! Type !snatch to grab it!"
|
||||||
self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}")
|
self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}")
|
||||||
|
|
||||||
@@ -1008,7 +896,6 @@ class DuckHuntBot:
|
|||||||
if 'inventory' not in player:
|
if 'inventory' not in player:
|
||||||
player['inventory'] = {}
|
player['inventory'] = {}
|
||||||
|
|
||||||
# Simple random item IDs
|
|
||||||
item_ids = [1, 2, 3, 4, 5, 6, 7, 8]
|
item_ids = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
item_id = random.choice(item_ids)
|
item_id = random.choice(item_ids)
|
||||||
item_key = str(item_id)
|
item_key = str(item_id)
|
||||||
@@ -1028,33 +915,31 @@ class DuckHuntBot:
|
|||||||
async def use_item_effect(self, player, item_id, nick, channel, target_nick=None):
|
async def use_item_effect(self, player, item_id, nick, channel, target_nick=None):
|
||||||
"""Apply item effects"""
|
"""Apply item effects"""
|
||||||
effects = {
|
effects = {
|
||||||
1: "Extra Shots! +3 shots", # Extra shots
|
1: "Extra Shots! +3 shots",
|
||||||
2: "Faster Reload! -1s reload time", # Faster reload
|
2: "Faster Reload! -1s reload time",
|
||||||
3: "Accuracy Charm! +5 accuracy", # Accuracy charm
|
3: "Accuracy Charm! +5 accuracy",
|
||||||
4: "Lucky Charm! +10 luck", # Lucky charm
|
4: "Lucky Charm! +10 luck",
|
||||||
5: "Friendship Bracelet! +5 charm", # Friendship bracelet
|
5: "Friendship Bracelet! +5 charm",
|
||||||
6: "Duck Caller! Next duck spawns faster", # Duck caller
|
6: "Duck Caller! Next duck spawns faster",
|
||||||
7: "Camouflage! Ducks can't see you for 60s", # Camouflage
|
7: "Camouflage! Ducks can't see you for 60s",
|
||||||
8: "Energy Drink! +50 energy" # Energy drink
|
8: "Energy Drink! +50 energy"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply item effects
|
if item_id == 1:
|
||||||
if item_id == 1: # Extra shots
|
|
||||||
player['shots'] = min(player['shots'] + 3, player['max_shots'])
|
player['shots'] = min(player['shots'] + 3, player['max_shots'])
|
||||||
elif item_id == 2: # Faster reload
|
elif item_id == 2:
|
||||||
player['reload_time'] = max(player['reload_time'] - 1, 1)
|
player['reload_time'] = max(player['reload_time'] - 1, 1)
|
||||||
elif item_id == 3: # Accuracy charm
|
elif item_id == 3:
|
||||||
player['accuracy_bonus'] = player.get('accuracy_bonus', 0) + 5
|
player['accuracy_bonus'] = player.get('accuracy_bonus', 0) + 5
|
||||||
elif item_id == 4: # Lucky charm
|
elif item_id == 4:
|
||||||
player['luck'] = player.get('luck', 0) + 10
|
player['luck'] = player.get('luck', 0) + 10
|
||||||
elif item_id == 5: # Friendship bracelet
|
elif item_id == 5:
|
||||||
player['charm_bonus'] = player.get('charm_bonus', 0) + 5
|
player['charm_bonus'] = player.get('charm_bonus', 0) + 5
|
||||||
elif item_id == 6: # Duck caller
|
elif item_id == 6:
|
||||||
# Could implement faster duck spawning here
|
|
||||||
pass
|
pass
|
||||||
elif item_id == 7: # Camouflage
|
elif item_id == 7:
|
||||||
player['camouflaged_until'] = time.time() + 60
|
player['camouflaged_until'] = time.time() + 60
|
||||||
elif item_id == 8: # Energy drink
|
elif item_id == 8:
|
||||||
player['energy'] = player.get('energy', 100) + 50
|
player['energy'] = player.get('energy', 100) + 50
|
||||||
|
|
||||||
effect_msg = effects.get(item_id, "Unknown effect")
|
effect_msg = effects.get(item_id, "Unknown effect")
|
||||||
@@ -1068,43 +953,33 @@ class DuckHuntBot:
|
|||||||
try:
|
try:
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
# Clean up expired items from all channels
|
|
||||||
for channel in list(self.dropped_items.keys()):
|
for channel in list(self.dropped_items.keys()):
|
||||||
if channel in self.dropped_items:
|
if channel in self.dropped_items:
|
||||||
original_count = len(self.dropped_items[channel])
|
original_count = len(self.dropped_items[channel])
|
||||||
|
|
||||||
# Remove items older than 60 seconds
|
|
||||||
self.dropped_items[channel] = [
|
self.dropped_items[channel] = [
|
||||||
item for item in self.dropped_items[channel]
|
item for item in self.dropped_items[channel]
|
||||||
if current_time - item['timestamp'] <= 60
|
if current_time - item['timestamp'] <= 60
|
||||||
]
|
]
|
||||||
|
|
||||||
# Optional: log cleanup if items were removed
|
|
||||||
removed_count = original_count - len(self.dropped_items[channel])
|
removed_count = original_count - len(self.dropped_items[channel])
|
||||||
if removed_count > 0:
|
if removed_count > 0:
|
||||||
self.logger.debug(f"Cleaned up {removed_count} expired items from {channel}")
|
self.logger.debug(f"Cleaned up {removed_count} expired items from {channel}")
|
||||||
|
|
||||||
# Sleep for 30 seconds before next cleanup
|
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error in cleanup_expired_items: {e}")
|
self.logger.error(f"Error in cleanup_expired_items: {e}")
|
||||||
await asyncio.sleep(30) # Continue trying after error
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""Main bot run loop"""
|
"""Main bot run loop"""
|
||||||
tasks = [] # Initialize tasks list early
|
tasks = []
|
||||||
try:
|
try:
|
||||||
# Setup signal handlers
|
|
||||||
self.setup_signal_handlers()
|
self.setup_signal_handlers()
|
||||||
|
|
||||||
# Load database
|
|
||||||
self.db.load_database()
|
self.db.load_database()
|
||||||
|
|
||||||
# Connect to IRC
|
|
||||||
await self.connect()
|
await self.connect()
|
||||||
|
|
||||||
# Start background tasks
|
|
||||||
tasks = [
|
tasks = [
|
||||||
asyncio.create_task(self.message_loop()),
|
asyncio.create_task(self.message_loop()),
|
||||||
asyncio.create_task(self.game.spawn_ducks()),
|
asyncio.create_task(self.game.spawn_ducks()),
|
||||||
@@ -1112,17 +987,15 @@ class DuckHuntBot:
|
|||||||
asyncio.create_task(self.cleanup_expired_items()),
|
asyncio.create_task(self.cleanup_expired_items()),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Wait for shutdown or task completion
|
|
||||||
try:
|
try:
|
||||||
while not self.shutdown_requested:
|
while not self.shutdown_requested:
|
||||||
# Check if any critical task has failed
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
if task.done() and task.exception():
|
if task.done() and task.exception():
|
||||||
self.logger.error(f"Task failed: {task.exception()}")
|
self.logger.error(f"Task failed: {task.exception()}")
|
||||||
self.shutdown_requested = True
|
self.shutdown_requested = True
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(0.1) # Short sleep to allow signal handling
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.logger.info("Main loop cancelled")
|
self.logger.info("Main loop cancelled")
|
||||||
@@ -1134,10 +1007,8 @@ class DuckHuntBot:
|
|||||||
self.logger.error(f"Bot error: {e}")
|
self.logger.error(f"Bot error: {e}")
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# Cleanup
|
|
||||||
self.logger.info("Shutting down bot...")
|
self.logger.info("Shutting down bot...")
|
||||||
|
|
||||||
# Cancel all tasks
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
if not task.done():
|
if not task.done():
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@@ -1148,14 +1019,12 @@ class DuckHuntBot:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error cancelling task: {e}")
|
self.logger.error(f"Error cancelling task: {e}")
|
||||||
|
|
||||||
# Save database
|
|
||||||
try:
|
try:
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
self.logger.info("Database saved")
|
self.logger.info("Database saved")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error saving database: {e}")
|
self.logger.error(f"Error saving database: {e}")
|
||||||
|
|
||||||
# Close connection
|
|
||||||
if self.writer and not self.writer.is_closing():
|
if self.writer and not self.writer.is_closing():
|
||||||
try:
|
try:
|
||||||
self.send_raw("QUIT :Bot shutting down")
|
self.send_raw("QUIT :Bot shutting down")
|
||||||
@@ -1171,7 +1040,6 @@ class DuckHuntBot:
|
|||||||
"""Handle incoming IRC messages"""
|
"""Handle incoming IRC messages"""
|
||||||
while not self.shutdown_requested and self.reader:
|
while not self.shutdown_requested and self.reader:
|
||||||
try:
|
try:
|
||||||
# Use a timeout so we can check shutdown_requested regularly
|
|
||||||
line = await asyncio.wait_for(self.reader.readline(), timeout=1.0)
|
line = await asyncio.wait_for(self.reader.readline(), timeout=1.0)
|
||||||
if not line:
|
if not line:
|
||||||
self.logger.warning("Empty line received, connection may be closed")
|
self.logger.warning("Empty line received, connection may be closed")
|
||||||
@@ -1183,7 +1051,6 @@ class DuckHuntBot:
|
|||||||
await self.handle_message(prefix, command, params, trailing)
|
await self.handle_message(prefix, command, params, trailing)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
# Timeout is expected - just continue to check shutdown flag
|
|
||||||
continue
|
continue
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.logger.info("Message loop cancelled")
|
self.logger.info("Message loop cancelled")
|
||||||
|
|||||||
42
src/game.py
42
src/game.py
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Game mechanics for DuckHunt Bot
|
Game mechanics for DuckHunt Bot
|
||||||
"""
|
"""
|
||||||
@@ -17,10 +16,9 @@ class DuckGame:
|
|||||||
def __init__(self, bot, db):
|
def __init__(self, bot, db):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.db = db
|
self.db = db
|
||||||
self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]}
|
self.ducks = {}
|
||||||
self.logger = logging.getLogger('DuckHuntBot.Game')
|
self.logger = logging.getLogger('DuckHuntBot.Game')
|
||||||
|
|
||||||
# Colors for IRC messages
|
|
||||||
self.colors = {
|
self.colors = {
|
||||||
'red': '\x0304',
|
'red': '\x0304',
|
||||||
'green': '\x0303',
|
'green': '\x0303',
|
||||||
@@ -84,29 +82,25 @@ class DuckGame:
|
|||||||
|
|
||||||
if start_hour <= end_hour:
|
if start_hour <= end_hour:
|
||||||
return start_hour <= current_hour <= end_hour
|
return start_hour <= current_hour <= end_hour
|
||||||
else: # Crosses midnight
|
else:
|
||||||
return current_hour >= start_hour or current_hour <= end_hour
|
return current_hour >= start_hour or current_hour <= end_hour
|
||||||
|
|
||||||
def calculate_gun_reliability(self, player):
|
def calculate_gun_reliability(self, player):
|
||||||
"""Calculate gun reliability with modifiers"""
|
"""Calculate gun reliability with modifiers"""
|
||||||
base_reliability = player.get('reliability', 70)
|
base_reliability = player.get('reliability', 70)
|
||||||
# Add weapon modifiers, items, etc.
|
|
||||||
return min(100, max(0, base_reliability))
|
return min(100, max(0, base_reliability))
|
||||||
|
|
||||||
def gun_jams(self, player):
|
def gun_jams(self, player):
|
||||||
"""Check if gun jams (eggdrop style)"""
|
"""Check if gun jams (eggdrop style)"""
|
||||||
# Base jamming probability is inverse of reliability
|
|
||||||
reliability = player.get('reliability', 70)
|
reliability = player.get('reliability', 70)
|
||||||
jam_chance = max(1, 101 - reliability) # Higher reliability = lower jam chance
|
jam_chance = max(1, 101 - reliability)
|
||||||
|
|
||||||
# Additional factors that increase jam chance
|
|
||||||
if player.get('total_ammo_used', 0) > 100:
|
if player.get('total_ammo_used', 0) > 100:
|
||||||
jam_chance += 2 # Gun gets more prone to jamming with use
|
jam_chance += 2
|
||||||
|
|
||||||
if player.get('jammed_count', 0) > 5:
|
if player.get('jammed_count', 0) > 5:
|
||||||
jam_chance += 1 # Previously jammed guns are more prone to jamming
|
jam_chance += 1
|
||||||
|
|
||||||
# Roll for jam (1-100, jam if roll <= jam_chance)
|
|
||||||
return random.randint(1, 100) <= jam_chance
|
return random.randint(1, 100) <= jam_chance
|
||||||
|
|
||||||
async def scare_other_ducks(self, channel, shot_duck_id):
|
async def scare_other_ducks(self, channel, shot_duck_id):
|
||||||
@@ -114,16 +108,15 @@ class DuckGame:
|
|||||||
if channel not in self.ducks:
|
if channel not in self.ducks:
|
||||||
return
|
return
|
||||||
|
|
||||||
for duck in self.ducks[channel][:]: # Copy list to avoid modification during iteration
|
for duck in self.ducks[channel][:]:
|
||||||
if duck['id'] != shot_duck_id and duck['alive']:
|
if duck['id'] != shot_duck_id and duck['alive']:
|
||||||
# 30% chance to scare away other ducks
|
|
||||||
if random.random() < 0.3:
|
if random.random() < 0.3:
|
||||||
duck['alive'] = False
|
duck['alive'] = False
|
||||||
self.ducks[channel].remove(duck)
|
self.ducks[channel].remove(duck)
|
||||||
|
|
||||||
async def scare_duck_on_miss(self, channel, target_duck):
|
async def scare_duck_on_miss(self, channel, target_duck):
|
||||||
"""Scare duck when someone misses"""
|
"""Scare duck when someone misses"""
|
||||||
if target_duck and random.random() < 0.15: # 15% chance
|
if target_duck and random.random() < 0.15:
|
||||||
target_duck['alive'] = False
|
target_duck['alive'] = False
|
||||||
if channel in self.ducks and target_duck in self.ducks[channel]:
|
if channel in self.ducks and target_duck in self.ducks[channel]:
|
||||||
self.ducks[channel].remove(target_duck)
|
self.ducks[channel].remove(target_duck)
|
||||||
@@ -133,7 +126,7 @@ class DuckGame:
|
|||||||
if not self.get_config('items.enabled', True):
|
if not self.get_config('items.enabled', True):
|
||||||
return
|
return
|
||||||
|
|
||||||
if random.random() < 0.1: # 10% chance
|
if random.random() < 0.1:
|
||||||
items = [
|
items = [
|
||||||
("a mirror", "mirror", "You can now deflect shots!"),
|
("a mirror", "mirror", "You can now deflect shots!"),
|
||||||
("some sand", "sand", "Throw this to blind opponents!"),
|
("some sand", "sand", "Throw this to blind opponents!"),
|
||||||
@@ -172,7 +165,6 @@ class DuckGame:
|
|||||||
self.logger.debug(f"Max ducks already in {channel}")
|
self.logger.debug(f"Max ducks already in {channel}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine duck type
|
|
||||||
if force_golden:
|
if force_golden:
|
||||||
duck_type = "golden"
|
duck_type = "golden"
|
||||||
else:
|
else:
|
||||||
@@ -188,13 +180,11 @@ class DuckGame:
|
|||||||
else:
|
else:
|
||||||
duck_type = "normal"
|
duck_type = "normal"
|
||||||
|
|
||||||
# Get duck configuration
|
|
||||||
duck_config = self.get_config(f'duck_types.{duck_type}', {})
|
duck_config = self.get_config(f'duck_types.{duck_type}', {})
|
||||||
if not duck_config.get('enabled', True):
|
if not duck_config.get('enabled', True):
|
||||||
duck_type = "normal"
|
duck_type = "normal"
|
||||||
duck_config = self.get_config('duck_types.normal', {})
|
duck_config = self.get_config('duck_types.normal', {})
|
||||||
|
|
||||||
# Create duck
|
|
||||||
duck = {
|
duck = {
|
||||||
'id': str(uuid.uuid4())[:8],
|
'id': str(uuid.uuid4())[:8],
|
||||||
'type': duck_type,
|
'type': duck_type,
|
||||||
@@ -206,14 +196,12 @@ class DuckGame:
|
|||||||
|
|
||||||
self.ducks[channel].append(duck)
|
self.ducks[channel].append(duck)
|
||||||
|
|
||||||
# Send spawn message
|
|
||||||
messages = duck_config.get('messages', [self.get_duck_spawn_message()])
|
messages = duck_config.get('messages', [self.get_duck_spawn_message()])
|
||||||
spawn_message = random.choice(messages)
|
spawn_message = random.choice(messages)
|
||||||
|
|
||||||
self.bot.send_message(channel, spawn_message)
|
self.bot.send_message(channel, spawn_message)
|
||||||
self.logger.info(f"Spawned {duck_type} duck in {channel}")
|
self.logger.info(f"Spawned {duck_type} duck in {channel}")
|
||||||
|
|
||||||
# Alert users who have alerts enabled
|
|
||||||
await self.send_duck_alerts(channel, duck_type)
|
await self.send_duck_alerts(channel, duck_type)
|
||||||
|
|
||||||
return duck
|
return duck
|
||||||
@@ -223,8 +211,6 @@ class DuckGame:
|
|||||||
if not self.get_config('social.duck_alerts_enabled', True):
|
if not self.get_config('social.duck_alerts_enabled', True):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Implementation would iterate through players with alerts enabled
|
|
||||||
# For now, just log
|
|
||||||
self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}")
|
self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}")
|
||||||
|
|
||||||
async def spawn_ducks(self):
|
async def spawn_ducks(self):
|
||||||
@@ -232,7 +218,7 @@ class DuckGame:
|
|||||||
while not self.bot.shutdown_requested:
|
while not self.bot.shutdown_requested:
|
||||||
try:
|
try:
|
||||||
if self.is_sleep_time():
|
if self.is_sleep_time():
|
||||||
await asyncio.sleep(300) # Check every 5 minutes during sleep
|
await asyncio.sleep(300)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for channel in self.bot.channels_joined:
|
for channel in self.bot.channels_joined:
|
||||||
@@ -242,7 +228,6 @@ class DuckGame:
|
|||||||
if channel not in self.ducks:
|
if channel not in self.ducks:
|
||||||
self.ducks[channel] = []
|
self.ducks[channel] = []
|
||||||
|
|
||||||
# Clean up dead ducks
|
|
||||||
self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']]
|
self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']]
|
||||||
|
|
||||||
max_ducks = self.get_config('max_ducks_per_channel', 3)
|
max_ducks = self.get_config('max_ducks_per_channel', 3)
|
||||||
@@ -252,10 +237,10 @@ class DuckGame:
|
|||||||
min_spawn_time = self.get_config('duck_spawn_min', 1800)
|
min_spawn_time = self.get_config('duck_spawn_min', 1800)
|
||||||
max_spawn_time = self.get_config('duck_spawn_max', 5400)
|
max_spawn_time = self.get_config('duck_spawn_max', 5400)
|
||||||
|
|
||||||
if random.random() < 0.1: # 10% chance each check
|
if random.random() < 0.1:
|
||||||
await self.spawn_duck_now(channel)
|
await self.spawn_duck_now(channel)
|
||||||
|
|
||||||
await asyncio.sleep(random.randint(60, 300)) # Check every 1-5 minutes
|
await asyncio.sleep(random.randint(60, 300))
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.logger.info("Duck spawning loop cancelled")
|
self.logger.info("Duck spawning loop cancelled")
|
||||||
@@ -277,7 +262,7 @@ class DuckGame:
|
|||||||
if channel not in self.ducks:
|
if channel not in self.ducks:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for duck in self.ducks[channel][:]: # Copy to avoid modification
|
for duck in self.ducks[channel][:]:
|
||||||
if not duck['alive']:
|
if not duck['alive']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -291,7 +276,6 @@ class DuckGame:
|
|||||||
duck['alive'] = False
|
duck['alive'] = False
|
||||||
self.ducks[channel].remove(duck)
|
self.ducks[channel].remove(duck)
|
||||||
|
|
||||||
# Send timeout message (eggdrop style)
|
|
||||||
timeout_messages = [
|
timeout_messages = [
|
||||||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck flew away!",
|
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck flew away!",
|
||||||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *FLAP FLAP FLAP*",
|
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *FLAP FLAP FLAP*",
|
||||||
@@ -301,7 +285,7 @@ class DuckGame:
|
|||||||
self.bot.send_message(channel, random.choice(timeout_messages))
|
self.bot.send_message(channel, random.choice(timeout_messages))
|
||||||
self.logger.debug(f"Duck timed out in {channel}")
|
self.logger.debug(f"Duck timed out in {channel}")
|
||||||
|
|
||||||
await asyncio.sleep(10) # Check every 10 seconds
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.logger.info("Duck timeout checker cancelled")
|
self.logger.info("Duck timeout checker cancelled")
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Logging utilities for DuckHunt Bot
|
Logging utilities for DuckHunt Bot
|
||||||
"""
|
"""
|
||||||
@@ -10,12 +9,12 @@ import logging.handlers
|
|||||||
class DetailedColorFormatter(logging.Formatter):
|
class DetailedColorFormatter(logging.Formatter):
|
||||||
"""Console formatter with color support"""
|
"""Console formatter with color support"""
|
||||||
COLORS = {
|
COLORS = {
|
||||||
'DEBUG': '\033[94m', # Blue
|
'DEBUG': '\033[94m',
|
||||||
'INFO': '\033[92m', # Green
|
'INFO': '\033[92m',
|
||||||
'WARNING': '\033[93m', # Yellow
|
'WARNING': '\033[93m',
|
||||||
'ERROR': '\033[91m', # Red
|
'ERROR': '\033[91m',
|
||||||
'CRITICAL': '\033[95m', # Magenta
|
'CRITICAL': '\033[95m',
|
||||||
'ENDC': '\033[0m' # End color
|
'ENDC': '\033[0m'
|
||||||
}
|
}
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
@@ -36,10 +35,8 @@ def setup_logger(name="DuckHuntBot"):
|
|||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Clear any existing handlers
|
|
||||||
logger.handlers.clear()
|
logger.handlers.clear()
|
||||||
|
|
||||||
# Console handler with colors
|
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setLevel(logging.INFO)
|
console_handler.setLevel(logging.INFO)
|
||||||
console_formatter = DetailedColorFormatter(
|
console_formatter = DetailedColorFormatter(
|
||||||
@@ -48,12 +45,11 @@ def setup_logger(name="DuckHuntBot"):
|
|||||||
console_handler.setFormatter(console_formatter)
|
console_handler.setFormatter(console_formatter)
|
||||||
logger.addHandler(console_handler)
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
# File handler with rotation for detailed logs
|
|
||||||
try:
|
try:
|
||||||
file_handler = logging.handlers.RotatingFileHandler(
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
'duckhunt.log',
|
'duckhunt.log',
|
||||||
maxBytes=10*1024*1024, # 10MB per file
|
maxBytes=10*1024*1024,
|
||||||
backupCount=5 # Keep 5 backup files
|
backupCount=5
|
||||||
)
|
)
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_formatter = DetailedFileFormatter(
|
file_formatter = DetailedFileFormatter(
|
||||||
|
|||||||
12
src/sasl.py
12
src/sasl.py
@@ -55,7 +55,6 @@ class SASLHandler:
|
|||||||
subcommand = params[1]
|
subcommand = params[1]
|
||||||
|
|
||||||
if subcommand == "LS":
|
if subcommand == "LS":
|
||||||
# Server listing capabilities
|
|
||||||
caps = trailing.split() if trailing else []
|
caps = trailing.split() if trailing else []
|
||||||
self.logger.info(f"Server capabilities: {caps}")
|
self.logger.info(f"Server capabilities: {caps}")
|
||||||
if "sasl" in caps:
|
if "sasl" in caps:
|
||||||
@@ -69,7 +68,6 @@ class SASLHandler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
elif subcommand == "ACK":
|
elif subcommand == "ACK":
|
||||||
# Server acknowledged capability request
|
|
||||||
caps = trailing.split() if trailing else []
|
caps = trailing.split() if trailing else []
|
||||||
self.logger.info("SASL capability acknowledged")
|
self.logger.info("SASL capability acknowledged")
|
||||||
if "sasl" in caps:
|
if "sasl" in caps:
|
||||||
@@ -82,7 +80,6 @@ class SASLHandler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
elif subcommand == "NAK":
|
elif subcommand == "NAK":
|
||||||
# Server rejected capability request
|
|
||||||
self.logger.warning("SASL capability rejected")
|
self.logger.warning("SASL capability rejected")
|
||||||
self.bot.send_raw("CAP END")
|
self.bot.send_raw("CAP END")
|
||||||
await self.bot.register_user()
|
await self.bot.register_user()
|
||||||
@@ -96,7 +93,6 @@ class SASLHandler:
|
|||||||
"""
|
"""
|
||||||
self.logger.info("Sending AUTHENTICATE PLAIN")
|
self.logger.info("Sending AUTHENTICATE PLAIN")
|
||||||
self.bot.send_raw('AUTHENTICATE PLAIN')
|
self.bot.send_raw('AUTHENTICATE PLAIN')
|
||||||
# Small delay to ensure proper sequencing
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
async def handle_authenticate_response(self, params):
|
async def handle_authenticate_response(self, params):
|
||||||
@@ -106,7 +102,6 @@ class SASLHandler:
|
|||||||
if params and params[0] == '+':
|
if params and params[0] == '+':
|
||||||
self.logger.info("Server ready for SASL authentication")
|
self.logger.info("Server ready for SASL authentication")
|
||||||
if self.username and self.password:
|
if self.username and self.password:
|
||||||
# Create auth string: username\0username\0password
|
|
||||||
authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}'
|
authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}'
|
||||||
self.logger.debug(f"Auth string length: {len(authpass)} chars")
|
self.logger.debug(f"Auth string length: {len(authpass)} chars")
|
||||||
self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'")
|
self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'")
|
||||||
@@ -125,14 +120,12 @@ class SASLHandler:
|
|||||||
async def handle_sasl_result(self, command, params, trailing):
|
async def handle_sasl_result(self, command, params, trailing):
|
||||||
"""Handle SASL authentication result."""
|
"""Handle SASL authentication result."""
|
||||||
if command == "903":
|
if command == "903":
|
||||||
# SASL success
|
|
||||||
self.logger.info("SASL authentication successful!")
|
self.logger.info("SASL authentication successful!")
|
||||||
self.authenticated = True
|
self.authenticated = True
|
||||||
await self.handle_903()
|
await self.handle_903()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif command == "904":
|
elif command == "904":
|
||||||
# SASL failed
|
|
||||||
self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)")
|
self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)")
|
||||||
self.logger.error(f"Attempted username: {self.username}")
|
self.logger.error(f"Attempted username: {self.username}")
|
||||||
self.logger.error(f"Password length: {len(self.password)} chars")
|
self.logger.error(f"Password length: {len(self.password)} chars")
|
||||||
@@ -145,28 +138,24 @@ class SASLHandler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
elif command == "905":
|
elif command == "905":
|
||||||
# SASL too long
|
|
||||||
self.logger.error("SASL authentication string too long")
|
self.logger.error("SASL authentication string too long")
|
||||||
self.bot.send_raw("CAP END")
|
self.bot.send_raw("CAP END")
|
||||||
await self.bot.register_user()
|
await self.bot.register_user()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif command == "906":
|
elif command == "906":
|
||||||
# SASL aborted
|
|
||||||
self.logger.error("SASL authentication aborted")
|
self.logger.error("SASL authentication aborted")
|
||||||
self.bot.send_raw("CAP END")
|
self.bot.send_raw("CAP END")
|
||||||
await self.bot.register_user()
|
await self.bot.register_user()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif command == "907":
|
elif command == "907":
|
||||||
# Already authenticated
|
|
||||||
self.logger.info("Already authenticated via SASL")
|
self.logger.info("Already authenticated via SASL")
|
||||||
self.authenticated = True
|
self.authenticated = True
|
||||||
await self.handle_903()
|
await self.handle_903()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif command == "908":
|
elif command == "908":
|
||||||
# SASL mechanisms
|
|
||||||
mechanisms = trailing.split() if trailing else []
|
mechanisms = trailing.split() if trailing else []
|
||||||
self.logger.info(f"Available SASL mechanisms: {mechanisms}")
|
self.logger.info(f"Available SASL mechanisms: {mechanisms}")
|
||||||
if "PLAIN" not in mechanisms:
|
if "PLAIN" not in mechanisms:
|
||||||
@@ -182,7 +171,6 @@ class SASLHandler:
|
|||||||
Handles the 903 command by sending a CAP END command and triggering registration.
|
Handles the 903 command by sending a CAP END command and triggering registration.
|
||||||
"""
|
"""
|
||||||
self.bot.send_raw('CAP END')
|
self.bot.send_raw('CAP END')
|
||||||
# Trigger user registration after successful SASL auth
|
|
||||||
await self.bot.register_user()
|
await self.bot.register_user()
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
Utility functions for DuckHunt Bot
|
Utility functions for DuckHunt Bot
|
||||||
"""
|
"""
|
||||||
@@ -15,7 +14,6 @@ class InputValidator:
|
|||||||
"""Validate IRC nickname format"""
|
"""Validate IRC nickname format"""
|
||||||
if not nick or len(nick) > 30:
|
if not nick or len(nick) > 30:
|
||||||
return False
|
return False
|
||||||
# RFC 2812 nickname pattern
|
|
||||||
pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$'
|
pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$'
|
||||||
return bool(re.match(pattern, nick))
|
return bool(re.match(pattern, nick))
|
||||||
|
|
||||||
@@ -44,9 +42,8 @@ class InputValidator:
|
|||||||
"""Sanitize user input message"""
|
"""Sanitize user input message"""
|
||||||
if not message:
|
if not message:
|
||||||
return ""
|
return ""
|
||||||
# Remove control characters and limit length
|
|
||||||
sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n')
|
sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n')
|
||||||
return sanitized[:500] # Limit message length
|
return sanitized[:500]
|
||||||
|
|
||||||
|
|
||||||
def parse_message(line):
|
def parse_message(line):
|
||||||
|
|||||||
Reference in New Issue
Block a user