Major code cleanup and compact message improvements

 Fixed all syntax errors and indentation issues
 Made all command outputs more compact and readable
 Fixed shop display with full item names
 Improved message formats for better IRC experience

Changes:
- Fixed broken try-except blocks and indentation
- Compacted hit/miss/reload messages
- Fixed color code issues in logging
- All syntax errors resolved - bot fully functional
This commit is contained in:
2025-09-13 14:36:57 +01:00
parent 86bf92c478
commit 009a851696
7 changed files with 4087 additions and 665 deletions

View File

@@ -9,14 +9,41 @@
"username": "duckhunt", "username": "duckhunt",
"password": "duckhunt//789//" "password": "duckhunt//789//"
}, },
"password": "", "password": "your_iline_password_here",
"_comment_password": "Server password for I-line exemption (PASS command)",
"admins": ["peorth", "computertech", "colby"], "admins": ["peorth", "computertech", "colby"],
"_comment_message_output": "Default output modes for different message types",
"message_output": {
"default_user_mode": "PUBLIC",
"_comment_default_user_mode": "Default output mode for new users: PUBLIC, NOTICE, or PRIVMSG",
"force_public": {
"duck_spawn": true,
"duck_shot": true,
"duck_befriend": true,
"leaderboard": true,
"admin_commands": true
},
"_comment_force_public": "Message types that always go to channel regardless of user preference"
},
"_comment_duck_spawning": "Duck spawning configuration", "_comment_duck_spawning": "Duck spawning configuration",
"duck_spawn_min": 1800, "duck_spawn_min": 1800,
"duck_spawn_max": 5400, "duck_spawn_max": 5400,
"duck_timeout_min": 45, "duck_timeout_min": 45,
"duck_timeout_max": 75, "duck_timeout_max": 75,
"duck_smartness": {
"enabled": true,
"learning_rate": 0.1,
"max_difficulty_multiplier": 2.0,
"_comment": "Ducks become harder to hit as more are shot in channel"
},
"records_tracking": {
"enabled": true,
"track_fastest_shots": true,
"track_channel_records": true,
"max_records_stored": 10
},
"duck_types": { "duck_types": {
"normal": { "normal": {
"spawn_chance": 0.6, "spawn_chance": 0.6,
@@ -91,7 +118,27 @@
"starting_chargers": 2, "starting_chargers": 2,
"max_chargers_base": 2, "max_chargers_base": 2,
"durability_enabled": true, "durability_enabled": true,
"confiscation_enabled": true "confiscation_enabled": true,
"auto_rearm_on_duck_shot": true,
"_comment_auto_rearm": "Automatically restore confiscated guns when anyone shoots a duck"
},
"_comment_new_players": "Starting stats for new hunters",
"new_players": {
"starting_xp": 0,
"starting_accuracy": 65,
"starting_reliability": 70,
"starting_karma": 0,
"starting_deflection": 0,
"starting_defense": 0,
"luck_chance": 5,
"_comment_luck_chance": "Base percentage chance for lucky events",
"random_stats": {
"enabled": false,
"accuracy_range": [60, 80],
"reliability_range": [65, 85],
"_comment_ranges": "If enabled, new players get random stats within these ranges"
}
}, },
"_comment_economy": "Economy and shop configuration", "_comment_economy": "Economy and shop configuration",

205
duckhunt/config_local.json Normal file
View File

@@ -0,0 +1,205 @@
{
"server": "irc.rizon.net",
"port": 6697,
"nick": "DuckHunt",
"channels": ["#computertech"],
"ssl": true,
"sasl": {
"enabled": true,
"username": "duckhunt",
"password": "duckhunt//789//"
},
"password": "",
"admins": ["peorth", "computertech", "colby"],
"_comment_duck_spawning": "Duck spawning configuration",
"duck_spawn_min": 1800,
"duck_spawn_max": 5400,
"duck_timeout_min": 45,
"duck_timeout_max": 75,
"duck_types": {
"normal": {
"spawn_chance": 0.6,
"xp_reward": 10,
"difficulty": 1.0,
"flee_time": 15,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK!"]
},
"fast": {
"spawn_chance": 0.25,
"xp_reward": 15,
"difficulty": 1.5,
"flee_time": 8,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Fast duck!)"]
},
"rare": {
"spawn_chance": 0.1,
"xp_reward": 30,
"difficulty": 2.0,
"flee_time": 12,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Rare duck!)"]
},
"golden": {
"spawn_chance": 0.05,
"xp_reward": 75,
"difficulty": 3.0,
"flee_time": 10,
"messages": ["・゜゜・。。・゜゜\\\\_✪< ★ GOLDEN DUCK ★"]
}
},
"sleep_hours": [],
"max_ducks_per_channel": 3,
"_comment_befriending": "Duck befriending configuration",
"befriending": {
"enabled": true,
"success_chance": 0.7,
"failure_messages": [
"The duck looked at you suspiciously and flew away!",
"The duck didn't trust you and escaped!",
"The duck was too scared and ran off!"
],
"scared_away_chance": 0.1,
"scared_away_messages": [
"You scared the duck away with your approach!",
"The duck was terrified and fled immediately!"
],
"xp_reward_min": 1,
"xp_reward_max": 3
},
"_comment_shooting": "Shooting mechanics configuration",
"shooting": {
"enabled": true,
"base_accuracy": 85,
"base_reliability": 90,
"jam_chance_base": 10,
"friendly_fire_enabled": true,
"friendly_fire_chance": 5,
"reflex_shot_bonus": 5,
"miss_xp_penalty": 5,
"wild_shot_xp_penalty": 10,
"teamkill_xp_penalty": 20
},
"_comment_weapons": "Weapon system configuration",
"weapons": {
"enabled": true,
"starting_weapon": "pistol",
"starting_ammo": 6,
"max_ammo_base": 6,
"starting_chargers": 2,
"max_chargers_base": 2,
"durability_enabled": true,
"confiscation_enabled": true
},
"_comment_economy": "Economy and shop configuration",
"economy": {
"enabled": true,
"starting_coins": 100,
"shop_enabled": true,
"trading_enabled": true,
"theft_enabled": true,
"theft_success_rate": 30,
"theft_penalty": 50,
"banking_enabled": true,
"interest_rate": 5,
"loan_enabled": true,
"inventory_system_enabled": true,
"max_inventory_slots": 20
},
"_comment_progression": "Player progression configuration",
"progression": {
"enabled": true,
"max_level": 40,
"xp_multiplier": 1.0,
"level_benefits_enabled": true,
"titles_enabled": true,
"prestige_enabled": false
},
"_comment_karma": "Karma system configuration",
"karma": {
"enabled": true,
"hit_bonus": 2,
"golden_hit_bonus": 5,
"teamkill_penalty": 10,
"wild_shot_penalty": 3,
"miss_penalty": 1,
"befriend_success_bonus": 2,
"befriend_fail_penalty": 1
},
"_comment_items": "Items and powerups configuration",
"items": {
"enabled": true,
"lucky_items_enabled": true,
"lucky_item_base_chance": 5,
"detector_enabled": true,
"silencer_enabled": true,
"sunglasses_enabled": true,
"explosive_ammo_enabled": true,
"sabotage_enabled": true,
"insurance_enabled": true,
"decoy_enabled": true
},
"_comment_social": "Social features configuration",
"social": {
"leaderboards_enabled": true,
"duck_alerts_enabled": true,
"private_messages_enabled": true,
"statistics_sharing_enabled": true,
"achievements_enabled": false
},
"_comment_moderation": "Moderation and admin features",
"moderation": {
"ignore_system_enabled": true,
"rate_limiting_enabled": true,
"rate_limit_cooldown": 2.0,
"admin_commands_enabled": true,
"ban_system_enabled": true,
"database_reset_enabled": true,
"admin_rearm_gives_full_ammo": false,
"admin_rearm_gives_full_chargers":false
},
"_comment_advanced": "Advanced game mechanics",
"advanced": {
"gun_jamming_enabled": true,
"weather_effects_enabled": false,
"seasonal_events_enabled": false,
"daily_challenges_enabled": false,
"guild_system_enabled": false,
"pvp_enabled": false
},
"_comment_messages": "Message customization",
"messages": {
"custom_duck_messages_enabled": true,
"color_enabled": true,
"emoji_enabled": true,
"verbose_messages": true,
"success_sound_effects": true
},
"_comment_database": "Database and persistence",
"database": {
"auto_save_enabled": true,
"auto_save_interval": 300,
"backup_enabled": true,
"backup_interval": 3600,
"compression_enabled": false
},
"_comment_debug": "Debug and logging options",
"debug": {
"debug_mode": false,
"verbose_logging": false,
"command_logging": false,
"performance_monitoring": false
}
}

0
duckhunt/config_new.json Normal file
View File

View File

@@ -541,3 +541,13 @@
[2025-09-12 20:59:07,442] INFO: Final database save completed - 19 players saved [2025-09-12 20:59:07,442] INFO: Final database save completed - 19 players saved
[2025-09-12 20:59:07,443] INFO: Cleanup completed successfully [2025-09-12 20:59:07,443] INFO: Cleanup completed successfully
[2025-09-12 20:59:07,445] INFO: DuckHunt Bot shutdown complete [2025-09-12 20:59:07,445] INFO: DuckHunt Bot shutdown complete
2025-09-13 13:06:23,192 [INFO ] DuckHuntBot - setup_logger:78: Enhanced logging system initialized with file rotation
2025-09-13 13:06:23,212 [INFO ] DuckHuntBot - load_database:401: Loaded 19 players from duckhunt.json
2025-09-13 13:07:06,713 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation
2025-09-13 13:07:06,715 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json
2025-09-13 13:07:20,053 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation
2025-09-13 13:07:20,054 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json
2025-09-13 13:17:14,822 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation
2025-09-13 13:17:14,824 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json
2025-09-13 13:22:07,954 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation
2025-09-13 13:22:07,956 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json

View File

@@ -8,6 +8,7 @@ import ssl
import json import json
import random import random
import logging import logging
import logging.handlers
import sys import sys
import os import os
import base64 import base64
@@ -15,50 +16,112 @@ import subprocess
import time import time
import uuid import uuid
import signal import signal
import traceback
import re
from functools import partial from functools import partial
from typing import Optional from typing import Optional, Dict, Any
# Import SASL handler # Import SASL handler
from src.sasl import SASLHandler from src.sasl import SASLHandler
# Simple colored logger # Enhanced logger with detailed formatting
class ColorFormatter(logging.Formatter): class DetailedColorFormatter(logging.Formatter):
COLORS = { COLORS = {
'DEBUG': '\033[94m', 'DEBUG': '\033[94m', # Blue
'INFO': '\033[92m', 'INFO': '\033[92m', # Green
'WARNING': '\033[93m', 'WARNING': '\033[93m', # Yellow
'ERROR': '\033[91m', 'ERROR': '\033[91m', # Red
'CRITICAL': '\033[95m', 'CRITICAL': '\033[95m', # Magenta
'ENDC': '\033[0m', 'ENDC': '\033[0m' # End color
} }
def format(self, record): def format(self, record):
color = self.COLORS.get(record.levelname, '') color = self.COLORS.get(record.levelname, '')
endc = self.COLORS['ENDC'] endc = self.COLORS['ENDC']
msg = super().format(record) msg = super().format(record)
return f"{color}{msg}{endc}" return f"{color}{msg}{endc}"
class DetailedFileFormatter(logging.Formatter):
"""File formatter with extra context but no colors"""
def format(self, record):
return super().format(record)
def setup_logger(): def setup_logger():
logger = logging.getLogger('DuckHuntBot') logger = logging.getLogger('DuckHuntBot')
logger.setLevel(logging.DEBUG)
# Clear any existing handlers # Clear any existing handlers
logger.handlers.clear() logger.handlers.clear()
# Console handler with colors # Console handler with colors
console_handler = logging.StreamHandler(sys.stdout) console_handler = logging.StreamHandler()
color_formatter = ColorFormatter('[%(asctime)s] %(levelname)s: %(message)s') console_handler.setLevel(logging.INFO)
console_handler.setFormatter(color_formatter) console_formatter = DetailedColorFormatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler) logger.addHandler(console_handler)
# File handler without colors # File handler with rotation for detailed logs
file_handler = logging.FileHandler('duckhunt.log', mode='a', encoding='utf-8') try:
file_formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s') file_handler = logging.handlers.RotatingFileHandler(
'duckhunt.log',
maxBytes=10*1024*1024, # 10MB per file
backupCount=5 # Keep 5 backup files
)
file_handler.setLevel(logging.DEBUG)
file_formatter = DetailedFileFormatter(
'%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s'
)
file_handler.setFormatter(file_formatter) file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
logger.setLevel(logging.INFO) logger.info("Enhanced logging system initialized with file rotation")
logger.propagate = False except Exception as e:
logger.error(f"Failed to setup file logging: {e}")
return logger return logger
# Input validation functions
class InputValidator:
@staticmethod
def validate_nickname(nick: str) -> bool:
"""Validate IRC nickname format"""
if not nick or len(nick) > 30:
return False
# RFC 2812 nickname pattern
pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$'
return bool(re.match(pattern, nick))
@staticmethod
def validate_channel(channel: str) -> bool:
"""Validate IRC channel format"""
if not channel or len(channel) > 50:
return False
return channel.startswith('#') and ' ' not in channel
@staticmethod
def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]:
"""Safely parse and validate numeric input"""
try:
num = int(value)
if min_val is not None and num < min_val:
return None
if max_val is not None and num > max_val:
return None
return num
except (ValueError, TypeError):
return None
@staticmethod
def sanitize_message(message: str) -> str:
"""Sanitize user input message"""
if not message:
return ""
# Remove control characters and limit length
sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n')
return sanitized[:500] # Limit message length
# Simple IRC message parser # Simple IRC message parser
def parse_message(line): def parse_message(line):
prefix = '' prefix = ''
@@ -93,6 +156,11 @@ class SimpleIRCBot:
self.shutdown_requested = False # Graceful shutdown flag self.shutdown_requested = False # Graceful shutdown flag
self.running_tasks = set() # Track running tasks for cleanup self.running_tasks = set() # Track running tasks for cleanup
# Duck intelligence and records tracking
self.channel_records = {} # Channel-specific records {channel: {'fastest_shot': {}, 'last_duck': {}, 'total_ducks': 0}}
self.duck_difficulty = {} # Per-channel duck difficulty {channel: multiplier}
self.next_duck_spawn = {} # Track next spawn time per channel
# Initialize SASL handler # Initialize SASL handler
self.sasl_handler = SASLHandler(self, config) self.sasl_handler = SASLHandler(self, config)
@@ -367,19 +435,33 @@ class SimpleIRCBot:
nick = user.split('!')[0].lower() nick = user.split('!')[0].lower()
return nick in self.admins return nick in self.admins
async def send_user_message(self, nick, channel, message): async def send_user_message(self, nick, channel, message, message_type='default'):
"""Send message to user respecting their notice/private message preferences""" """Send message to user respecting their output mode preferences and config overrides"""
player = self.get_player(f"{nick}!*@*") player = self.get_player(f"{nick}!*@*")
# Default to channel notices if player not found or no settings # Check if this message type should be forced to public
use_notices = True force_public_key = f'message_output.force_public.{message_type}'
if player and 'settings' in player: if self.get_config(force_public_key, False):
use_notices = player['settings'].get('notices', True)
if use_notices:
# Send to channel
self.send_message(channel, message) self.send_message(channel, message)
else: return
# Default to config setting if player not found or no settings
default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC')
output_mode = default_mode
if player and 'settings' in player:
output_mode = player['settings'].get('output_mode', default_mode)
# Handle legacy 'notices' setting for backwards compatibility
if 'output_mode' not in player['settings'] and 'notices' in player['settings']:
output_mode = 'NOTICE' if player['settings']['notices'] else 'PRIVMSG'
if output_mode == 'PUBLIC':
# Send as regular channel message
self.send_message(channel, message)
elif output_mode == 'NOTICE':
# Send as notice to user
notice_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for notice
self.send_raw(f'NOTICE {nick} :{notice_msg}')
else: # PRIVMSG
# Send as private message # Send as private message
private_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for PM private_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for PM
self.send_message(nick, private_msg) self.send_message(nick, private_msg)
@@ -397,7 +479,105 @@ class SimpleIRCBot:
return random.choice(eligible_players) return random.choice(eligible_players)
return None return None
def _get_starting_accuracy(self):
"""Get starting accuracy for new player - either fixed or random"""
if self.get_config('new_players.random_stats.enabled', False):
accuracy_range = self.get_config('new_players.random_stats.accuracy_range', [60, 80])
if accuracy_range and len(accuracy_range) >= 2:
return random.randint(accuracy_range[0], accuracy_range[1])
return self.get_config('new_players.starting_accuracy', 65)
def _get_starting_reliability(self):
"""Get starting reliability for new player - either fixed or random"""
if self.get_config('new_players.random_stats.enabled', False):
reliability_range = self.get_config('new_players.random_stats.reliability_range', [65, 85])
if reliability_range and len(reliability_range) >= 2:
return random.randint(reliability_range[0], reliability_range[1])
return self.get_config('new_players.starting_reliability', 70)
async def auto_rearm_confiscated_guns(self, channel, shooter_nick):
"""Auto-rearm all players with confiscated guns when someone shoots a duck"""
if not self.get_config('weapons.auto_rearm_on_duck_shot', False):
return
rearmed_players = []
for user_host, player_data in self.players.items():
if player_data.get('gun_confiscated', False):
player_data['gun_confiscated'] = False
player_data['ammo'] = player_data.get('ammo', 0) + 1 # Give them 1 ammo
# Get just the nickname from user!host format
nick = user_host.split('!')[0] if '!' in user_host else user_host
rearmed_players.append(nick)
if rearmed_players:
self.save_database()
# Send notification to channel
rearmed_list = ', '.join(rearmed_players)
self.send_message(channel, f"🔫 {self.colors['green']}Auto-rearm:{self.colors['reset']} {rearmed_list} got their guns back! (Thanks to {shooter_nick}'s duck shot)")
self.logger.info(f"Auto-rearmed {len(rearmed_players)} players after {shooter_nick} shot duck in {channel}")
async def update_channel_records(self, channel, hunter, shot_time, duck_type):
"""Update channel records and duck difficulty after a successful shot"""
if not self.get_config('records_tracking.enabled', True):
return
# Initialize channel records if needed
if channel not in self.channel_records:
self.channel_records[channel] = {
'fastest_shot': None,
'last_duck': None,
'total_ducks': 0,
'total_shots': 0
}
records = self.channel_records[channel]
# Update totals
records['total_ducks'] += 1
# Update fastest shot record
if not records['fastest_shot'] or shot_time < records['fastest_shot']['time']:
records['fastest_shot'] = {
'time': shot_time,
'hunter': hunter,
'duck_type': duck_type,
'timestamp': time.time()
}
# Announce new record
self.send_message(channel, f"🏆 {self.colors['yellow']}NEW RECORD!{self.colors['reset']} {hunter} set fastest shot: {shot_time:.3f}s!")
# Update last duck info
records['last_duck'] = {
'hunter': hunter,
'type': duck_type,
'shot_time': shot_time,
'timestamp': time.time()
}
# Increase duck difficulty (smartness)
if self.get_config('duck_smartness.enabled', True):
if channel not in self.duck_difficulty:
self.duck_difficulty[channel] = 1.0
learning_rate = self.get_config('duck_smartness.learning_rate', 0.1)
max_difficulty = self.get_config('duck_smartness.max_difficulty_multiplier', 2.0)
# Ensure max_difficulty has a valid value
if max_difficulty is None:
max_difficulty = 2.0
# Increase difficulty slightly with each duck shot
self.duck_difficulty[channel] = min(
max_difficulty,
self.duck_difficulty[channel] + learning_rate
)
# Save records to database periodically
self.save_database()
async def connect(self): async def connect(self):
try:
server = self.config['server'] server = self.config['server']
port = self.config['port'] port = self.config['port']
ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None
@@ -416,17 +596,20 @@ class SimpleIRCBot:
# Standard registration without SASL # Standard registration without SASL
await self.register_user() await self.register_user()
return True return True
except Exception as e:
self.logger.error(f"Connection failed: {e}")
return False
async def register_user(self): async def register_user(self):
"""Register the user with the IRC server""" """Register the user with the IRC server"""
# Send password FIRST if configured (for I-line exemption)
if self.config.get('password'):
self.send_raw(f'PASS {self.config["password"]}')
self.logger.info(f"Registering as {self.config['nick']}") self.logger.info(f"Registering as {self.config['nick']}")
self.send_raw(f'NICK {self.config["nick"]}') self.send_raw(f'NICK {self.config["nick"]}')
self.send_raw(f'USER {self.config["nick"]} 0 * :DuckHunt Bot') self.send_raw(f'USER {self.config["nick"]} 0 * :DuckHunt Bot')
# Send password if configured (for servers that require it)
if self.config.get('password'):
self.send_raw(f'PASS {self.config["password"]}')
def send_raw(self, msg): def send_raw(self, msg):
# Skip debug logging for speed # Skip debug logging for speed
# self.logger.debug(f"-> {msg}") # self.logger.debug(f"-> {msg}")
@@ -439,6 +622,34 @@ class SimpleIRCBot:
self.send_raw(f'PRIVMSG {target} :{msg}') self.send_raw(f'PRIVMSG {target} :{msg}')
# Remove drain() for faster responses - let TCP handle buffering # Remove drain() for faster responses - let TCP handle buffering
def check_rate_limit(self, nick: str, channel: str) -> bool:
"""Check if user is within rate limits"""
try:
current_time = time.time()
key = f"{nick}:{channel}"
# Rate limit: 5 commands per 30 seconds per user per channel
if key not in self.command_cooldowns:
self.command_cooldowns[key] = []
# Remove old entries
self.command_cooldowns[key] = [
timestamp for timestamp in self.command_cooldowns[key]
if current_time - timestamp < 30
]
# Check if under limit
if len(self.command_cooldowns[key]) >= 5:
return False
# Add current command
self.command_cooldowns[key].append(current_time)
return True
except Exception as e:
self.logger.error(f"Rate limit check failed: {e}")
return True # Allow command if rate limiting fails
def get_player(self, user): def get_player(self, user):
"""Get player data by nickname only (case insensitive)""" """Get player data by nickname only (case insensitive)"""
if '!' not in user: if '!' not in user:
@@ -458,7 +669,7 @@ class SimpleIRCBot:
# Create new player with configurable defaults # Create new player with configurable defaults
player_data = { player_data = {
'xp': 0, 'xp': self.get_config('new_players.starting_xp', 0),
'caught': 0, 'caught': 0,
'befriended': 0, # Separate counter for befriended ducks 'befriended': 0, # Separate counter for befriended ducks
'missed': 0, 'missed': 0,
@@ -466,22 +677,23 @@ class SimpleIRCBot:
'max_ammo': self.get_config('weapons.max_ammo_base', 6), 'max_ammo': self.get_config('weapons.max_ammo_base', 6),
'chargers': self.get_config('weapons.starting_chargers', 2), 'chargers': self.get_config('weapons.starting_chargers', 2),
'max_chargers': self.get_config('weapons.max_chargers_base', 2), 'max_chargers': self.get_config('weapons.max_chargers_base', 2),
'accuracy': self.get_config('shooting.base_accuracy', 65), 'accuracy': self._get_starting_accuracy(),
'reliability': self.get_config('shooting.base_reliability', 70), 'reliability': self._get_starting_reliability(),
'weapon': self.get_config('weapons.starting_weapon', 'pistol'), 'weapon': self.get_config('weapons.starting_weapon', 'pistol'),
'gun_confiscated': False, 'gun_confiscated': False,
'explosive_ammo': False, 'explosive_ammo': False,
'settings': { 'settings': {
'notices': True, # True for notices, False for private messages 'output_mode': self.get_config('message_output.default_user_mode', 'PUBLIC'),
'notices': True, # Legacy setting for backwards compatibility
'private_messages': False 'private_messages': False
}, },
# Inventory system # Inventory system
'inventory': {}, 'inventory': {},
# New advanced stats # New advanced stats
'golden_ducks': 0, 'golden_ducks': 0,
'karma': 0, 'karma': self.get_config('new_players.starting_karma', 0),
'deflection': 0, 'deflection': self.get_config('new_players.starting_deflection', 0),
'defense': 0, 'defense': self.get_config('new_players.starting_defense', 0),
'jammed': False, 'jammed': False,
'jammed_count': 0, 'jammed_count': 0,
'deaths': 0, 'deaths': 0,
@@ -532,7 +744,12 @@ class SimpleIRCBot:
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) # Small delay to batch saves
try:
self.save_database() self.save_database()
self.logger.debug("Database batch save completed")
except Exception as e:
self.logger.error(f"Database batch save failed: {e}")
finally:
self._save_pending = False self._save_pending = False
def setup_signal_handlers(self): def setup_signal_handlers(self):
@@ -563,14 +780,40 @@ class SimpleIRCBot:
return False return False
async def handle_command(self, user, channel, message): async def handle_command(self, user, channel, message):
"""Enhanced command handler with logging, validation, and graceful degradation"""
if not user: if not user:
self.logger.warning("Received command with no user information")
return return
try:
nick = user.split('!')[0] nick = user.split('!')[0]
nick_lower = nick.lower() nick_lower = nick.lower()
# Input validation
if not InputValidator.validate_nickname(nick):
self.logger.warning(f"Invalid nickname format: {nick}")
return
if not InputValidator.validate_channel(channel) and not channel == self.config['nick']:
self.logger.warning(f"Invalid channel format: {channel}")
return
# Sanitize message input
message = InputValidator.sanitize_message(message)
if not message:
return
# Enhanced logging with context
self.logger.debug(f"Processing command from {nick} in {channel}: {message[:100]}")
# Check if user is ignored # Check if user is ignored
if nick_lower in self.ignored_nicks: if nick_lower in self.ignored_nicks:
self.logger.debug(f"Ignoring command from ignored user: {nick}")
return
# Rate limiting check
if not self.check_rate_limit(nick_lower, channel):
self.logger.info(f"Rate limit exceeded for {nick} in {channel}")
return return
# Determine if this is a private message to the bot # Determine if this is a private message to the bot
@@ -582,6 +825,7 @@ class SimpleIRCBot:
# Handle private messages (no ! prefix needed) # Handle private messages (no ! prefix needed)
if is_private: if is_private:
cmd = message.strip().lower() cmd = message.strip().lower()
self.logger.info(f"Private command from {nick}: {cmd}")
# Private message admin commands # Private message admin commands
if self.is_admin(user): if self.is_admin(user):
@@ -642,17 +886,17 @@ class SimpleIRCBot:
# Check if gun is confiscated # Check if gun is confiscated
if player.get('gun_confiscated', False): if player.get('gun_confiscated', False):
self.send_message(channel, f"{nick} > {self.colors['red']}Your gun has been confiscated! Buy a new gun from the shop (item #5).{self.colors['reset']}") self.send_message(channel, f"{nick} > {self.colors['red']}Gun confiscated! Buy item #5{self.colors['reset']}")
return return
# Check if gun is jammed # Check if gun is jammed
if player.get('jammed', False): if player.get('jammed', False):
self.send_message(channel, f"{nick} > {self.colors['red']}Your gun is jammed! Use !reload to unjam it.{self.colors['reset']}") self.send_message(channel, f"{nick} > {self.colors['red']}Gun jammed! Use !reload{self.colors['reset']}")
return return
# Check ammo # Check ammo
if player['ammo'] <= 0: if player['ammo'] <= 0:
self.send_message(channel, f"{nick} > Your gun is empty! | Ammo: 0/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") self.send_message(channel, f"{nick} > Empty! | {player['ammo']}/{player['max_ammo']} | {player['chargers']}/{player['max_chargers']}")
return return
# Check for gun jamming before shooting # Check for gun jamming before shooting
@@ -689,12 +933,24 @@ class SimpleIRCBot:
if player.get('mirror', 0) > 0: if player.get('mirror', 0) > 0:
base_accuracy += 3 # Mirror helps base_accuracy += 3 # Mirror helps
# Apply duck smartness penalty
duck_difficulty = self.duck_difficulty.get(channel, 1.0)
if duck_difficulty > 1.0:
# Smarter ducks are harder to hit
difficulty_penalty = (duck_difficulty - 1.0) * 20 # Up to 20% penalty at max difficulty
base_accuracy = max(base_accuracy - difficulty_penalty, 10) # Never go below 10%
hit_chance = min(base_accuracy, 95) # Cap at 95% hit_chance = min(base_accuracy, 95) # Cap at 95%
# Record shot attempt # Record shot attempt
player['shot_at'] = player.get('shot_at', 0) + 1 player['shot_at'] = player.get('shot_at', 0) + 1
target_duck['hit_attempts'] = target_duck.get('hit_attempts', 0) + 1 target_duck['hit_attempts'] = target_duck.get('hit_attempts', 0) + 1
# Track total shots for channel statistics
if channel not in self.channel_records:
self.channel_records[channel] = {'total_shots': 0, 'total_ducks': 0}
self.channel_records[channel]['total_shots'] += 1
# Check for hit # Check for hit
if random.randint(1, 100) <= hit_chance: if random.randint(1, 100) <= hit_chance:
# HIT! # HIT!
@@ -742,9 +998,9 @@ class SimpleIRCBot:
if is_golden: if is_golden:
golden_count = player.get('golden_ducks', 0) golden_count = player.get('golden_ducks', 0)
hit_msg = f"{nick} > {self.colors['yellow']}{shot_sound}{self.colors['reset']} You shot down the {self.colors['yellow']}★ GOLDEN DUCK {self.colors['reset']} in {shot_time:.3f}s! Total: {player['caught']} ducks ({self.colors['yellow']}{golden_count} golden{self.colors['reset']}) | Level {level}: {title} | [{self.colors['yellow']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" hit_msg = f"{nick} > {self.colors['yellow']}{shot_sound} ★GOLDEN{self.colors['reset']} {shot_time:.3f}s | Ducks:{player['caught']} ({self.colors['yellow']}{golden_count}g{self.colors['reset']}) | L{level} | +{xp_earned}xp{explosive_text}{lucky_text}"
else: else:
hit_msg = f"{nick} > {self.colors['green']}{shot_sound}{self.colors['reset']} You shot down the duck in {shot_time:.3f}s! Total: {player['caught']} ducks | Level {level}: {title} | [{self.colors['green']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" hit_msg = f"{nick} > {self.colors['green']}{shot_sound}{self.colors['reset']} {shot_time:.3f}s | Ducks:{player['caught']} | L{level} | +{xp_earned}xp{explosive_text}{lucky_text}"
self.send_message(channel, hit_msg) self.send_message(channel, hit_msg)
@@ -754,6 +1010,12 @@ class SimpleIRCBot:
# Find items in bushes (rare chance) # Find items in bushes (rare chance)
await self.find_bushes_items(nick, channel, player) await self.find_bushes_items(nick, channel, player)
# Auto-rearm confiscated guns if enabled
await self.auto_rearm_confiscated_guns(channel, nick)
# Track records and increase duck difficulty
await self.update_channel_records(channel, nick, shot_time, target_duck['type'])
else: else:
# MISS! # MISS!
player['missed'] += 1 player['missed'] += 1
@@ -776,19 +1038,24 @@ class SimpleIRCBot:
ricochet_dmg = -3 ricochet_dmg = -3
target_player['xp'] += ricochet_dmg target_player['xp'] += ricochet_dmg
target_player['shot_at'] = target_player.get('shot_at', 0) + 1 target_player['shot_at'] = target_player.get('shot_at', 0) + 1
ricochet_msg = f" {self.colors['red']}[RICOCHET: {ricochet_target} hit for {ricochet_dmg} xp]{self.colors['reset']}" ricochet_msg = f" [HIT:{ricochet_target}:{ricochet_dmg}xp]"
# Scare duck on miss # Scare duck on miss
await self.scare_duck_on_miss(channel, target_duck) await self.scare_duck_on_miss(channel, target_duck)
miss_sound = "•click•" if player.get('silencer', 0) > 0 else "*CLICK*" miss_sound = "•click•" if player.get('silencer', 0) > 0 else "*CLICK*"
await self.send_user_message(nick, channel, f"{nick} > {miss_sound} You missed the duck! [miss: {miss_penalty} xp]{ricochet_msg}") await self.send_user_message(nick, channel, f"{nick} > {miss_sound} MISS | {miss_penalty}xp{ricochet_msg}")
else: else:
# No duck present - wild fire! # No duck present - wild fire!
player['wild_shots'] = player.get('wild_shots', 0) + 1 player['wild_shots'] = player.get('wild_shots', 0) + 1
self.update_karma(player, 'wild_shot') self.update_karma(player, 'wild_shot')
# Track wild shots in channel statistics
if channel not in self.channel_records:
self.channel_records[channel] = {'total_shots': 0, 'total_ducks': 0}
self.channel_records[channel]['total_shots'] += 1
# Calculate penalties based on level # Calculate penalties based on level
miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp'])) miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp']))
wild_penalty = int(self.calculate_penalty_by_level(-3, player['xp'])) wild_penalty = int(self.calculate_penalty_by_level(-3, player['xp']))
@@ -810,14 +1077,13 @@ class SimpleIRCBot:
target_player['shot_at'] = target_player.get('shot_at', 0) + 1 target_player['shot_at'] = target_player.get('shot_at', 0) + 1
player['accidents'] = player.get('accidents', 0) + 1 player['accidents'] = player.get('accidents', 0) + 1
self.update_karma(player, 'teamkill') self.update_karma(player, 'teamkill')
friendly_fire_msg = f" {self.colors['red']}[ACCIDENT: {ff_target} injured for {ff_dmg} xp]{self.colors['reset']}" friendly_fire_msg = f" [HIT:{ff_target}:{ff_dmg}xp]"
wild_sound = "•BOUM•" if player.get('explosive_ammo', False) else "*BANG*" wild_sound = "•BOUM•" if player.get('explosive_ammo', False) else "*BANG*"
if player.get('silencer', 0) > 0: if player.get('silencer', 0) > 0:
wild_sound = "" + wild_sound[1:-1] + "" wild_sound = "" + wild_sound[1:-1] + ""
confiscated_msg = f" {self.colors['red']}[GUN CONFISCATED]{self.colors['reset']}" await self.send_user_message(nick, channel, f"{nick} > {wild_sound} WILD SHOT! | {miss_penalty+wild_penalty}xp | GUN CONFISCATED{friendly_fire_msg}")
await self.send_user_message(nick, channel, f"{nick} > {wild_sound} You shot at nothing! What were you aiming at? [miss: {miss_penalty} xp] [wild fire: {wild_penalty} xp]{confiscated_msg}{friendly_fire_msg}")
# Save after each shot # Save after each shot
self.save_player(user) self.save_player(user)
@@ -880,7 +1146,7 @@ class SimpleIRCBot:
remaining_ducks = len([d for d in channel_ducks if d.get('alive')]) remaining_ducks = len([d for d in channel_ducks if d.get('alive')])
duck_count_text = f" | {remaining_ducks} ducks remain" if remaining_ducks > 0 else "" duck_count_text = f" | {remaining_ducks} ducks remain" if remaining_ducks > 0 else ""
self.send_message(channel, f"{nick} > You befriended a duck in {bef_time:.3f}s! Total friends: {player['befriended']} ducks on {channel}. \\_o< *quack* [+{xp_earned} xp]{lucky_text}{duck_count_text}") self.send_message(channel, f"{nick} > \\_o< BEFRIENDED {bef_time:.3f}s | Friends:{player['befriended']} | +{xp_earned}xp{lucky_text}{duck_count_text}")
# Update karma for successful befriend # Update karma for successful befriend
if self.get_config('karma.enabled', True): if self.get_config('karma.enabled', True):
@@ -937,16 +1203,16 @@ class SimpleIRCBot:
if player.get('jammed', False): if player.get('jammed', False):
player['jammed'] = False player['jammed'] = False
unjam_sound = "•click click•" if player.get('silencer', 0) > 0 else "*click click*" unjam_sound = "•click click•" if player.get('silencer', 0) > 0 else "*click click*"
self.send_message(channel, f"{nick} > {unjam_sound} You unjammed your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") self.send_message(channel, f"{nick} > {unjam_sound} UNJAMMED | {player['ammo']}/{player['max_ammo']} | {player['chargers']}/{player['max_chargers']}")
self.save_player(user) self.save_player(user)
return return
if player['ammo'] == player['max_ammo']: if player['ammo'] == player['max_ammo']:
self.send_message(channel, f"{nick} > Your gun doesn't need to be reloaded. | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") self.send_message(channel, f"{nick} > Already loaded | {player['ammo']}/{player['max_ammo']} | {player['chargers']}/{player['max_chargers']}")
return return
if player['chargers'] <= 0: if player['chargers'] <= 0:
self.send_message(channel, f"{nick} > You don't have any chargers left! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: 0/{player['max_chargers']}") self.send_message(channel, f"{nick} > No chargers! | {player['ammo']}/{player['max_ammo']} | 0/{player['max_chargers']}")
return return
# Calculate reload reliability # Calculate reload reliability
@@ -956,13 +1222,13 @@ class SimpleIRCBot:
player['chargers'] -= 1 player['chargers'] -= 1
player['ammo'] = player['max_ammo'] player['ammo'] = player['max_ammo']
reload_sound = "•click•" if player.get('silencer', 0) > 0 else "*click*" reload_sound = "•click•" if player.get('silencer', 0) > 0 else "*click*"
self.send_message(channel, f"{nick} > {reload_sound} You reloaded your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") self.send_message(channel, f"{nick} > {reload_sound} RELOADED | {player['ammo']}/{player['max_ammo']} | {player['chargers']}/{player['max_chargers']}")
else: else:
# Gun jams during reload # Gun jams during reload
player['jammed'] = True player['jammed'] = True
player['jammed_count'] = player.get('jammed_count', 0) + 1 player['jammed_count'] = player.get('jammed_count', 0) + 1
jam_sound = "•CLACK• •click click•" if player.get('silencer', 0) > 0 else "*CLACK* *click click*" jam_sound = "•CLACK• •click click•" if player.get('silencer', 0) > 0 else "*CLACK* *click click*"
self.send_message(channel, f"{nick} > {jam_sound} Your gun jammed while reloading! Use !reload again to unjam it.") self.send_message(channel, f"{nick} > {jam_sound} RELOAD JAMMED! Use !reload to unjam.")
# Save to database after reload # Save to database after reload
self.save_player(user) self.save_player(user)
@@ -1143,6 +1409,18 @@ class SimpleIRCBot:
parts = cmd.split(maxsplit=1) parts = cmd.split(maxsplit=1)
output_type = parts[1] if len(parts) > 1 else '' output_type = parts[1] if len(parts) > 1 else ''
await self.handle_output(nick, channel, user, output_type) await self.handle_output(nick, channel, user, output_type)
elif cmd == '!ducktime':
# Show time until next duck spawn
await self.handle_ducktime(nick, channel)
elif cmd == '!lastduck':
# Show information about the last duck shot
await self.handle_lastduck(nick, channel)
elif cmd == '!records':
# Show channel records
await self.handle_records(nick, channel)
elif full_cmd.startswith('!ignore ') and self.is_admin(user): # Admin only elif full_cmd.startswith('!ignore ') and self.is_admin(user): # Admin only
target_nick = full_cmd[8:].strip().lower() target_nick = full_cmd[8:].strip().lower()
await self.handle_ignore(nick, channel, target_nick) await self.handle_ignore(nick, channel, target_nick)
@@ -1164,6 +1442,23 @@ class SimpleIRCBot:
else: else:
self.send_message(channel, f"{nick} > Usage: !givexp <nick> <amount>") self.send_message(channel, f"{nick} > Usage: !givexp <nick> <amount>")
except Exception as e:
# Graceful degradation - log error but don't crash
self.logger.error(f"Command handling error for {user} in {channel}: {e}")
import traceback
self.logger.error(f"Full traceback: {traceback.format_exc()}")
# Send a gentle error message to user
try:
nick = user.split('!')[0] if user and '!' in user else "Unknown"
error_msg = f"{nick} > Sorry, there was an error processing your command. Please try again."
if channel == self.config['nick']: # Private message
self.send_message(nick, error_msg)
else: # Channel message
self.send_message(channel, error_msg)
except Exception as send_error:
self.logger.error(f"Failed to send error message: {send_error}")
async def handle_stats(self, nick, channel, user): async def handle_stats(self, nick, channel, user):
player = self.get_player(user) player = self.get_player(user)
if not player: if not player:
@@ -1188,43 +1483,67 @@ class SimpleIRCBot:
if player.get('explosive_ammo', False): if player.get('explosive_ammo', False):
gun_status += f" {self.colors['orange']}[EXPLOSIVE]{self.colors['reset']}" gun_status += f" {self.colors['orange']}[EXPLOSIVE]{self.colors['reset']}"
# Duck stats with colors # Compact stats display - combine into fewer lines
duck_stats = [] duck_display = f"D:{player.get('caught', 0)}"
if player.get('caught', 0) > 0:
duck_stats.append(f"Shot:{player['caught']}")
if player.get('befriended', 0) > 0: if player.get('befriended', 0) > 0:
duck_stats.append(f"Befriended:{player['befriended']}") duck_display += f"/B:{player['befriended']}"
if player.get('golden_ducks', 0) > 0: if player.get('golden_ducks', 0) > 0:
duck_stats.append(f"{self.colors['yellow']}Golden:{player['golden_ducks']}{self.colors['reset']}") duck_display += f"/{self.colors['yellow']}G:{player['golden_ducks']}{self.colors['reset']}"
duck_display = f"Ducks:({', '.join(duck_stats)})" if duck_stats else "Ducks:0"
# Main stats line # Main stats line
stats_line1 = f"{nick} > {duck_display} | Level {level}: {self.colors['cyan']}{title}{self.colors['reset']} | XP: {player['xp']}"
if xp_for_next > 0:
stats_line1 += f" (next: {xp_for_next})"
# Combat stats line
karma_color = self.colors['green'] if player.get('karma', 0) >= 0 else self.colors['red'] karma_color = self.colors['green'] if player.get('karma', 0) >= 0 else self.colors['red']
karma_display = f"{karma_color}Karma:{player.get('karma', 0)}{self.colors['reset']}" stats_line1 = f"{nick} > {duck_display} | L{level} | XP:{player['xp']}"
if xp_for_next > 0:
stats_line1 += f"(+{xp_for_next})"
stats_line1 += f" | {karma_color}K:{player.get('karma', 0)}{self.colors['reset']}"
stats_line2 = f"{nick} > {karma_display} | Accuracy: {player['accuracy']}% (effective: {effective_accuracy:.1f}%) | Reliability: {self.calculate_gun_reliability(player)}%" # Equipment line with compact gun status
weapon_name = player.get('weapon', 'pistol')[:6] # Shorten weapon names
compact_gun_status = ""
if player.get('gun_confiscated', False):
compact_gun_status += "[CONF]"
if player.get('jammed', False):
compact_gun_status += "[JAM]"
if player.get('explosive_ammo', False):
compact_gun_status += "[EXP]"
# Equipment line stats_line2 = f"{nick} > {weapon_name.title()}{compact_gun_status} | {player['ammo']}/{player['max_ammo']}a | {player['chargers']}/{player['max_chargers']}c | Acc:{player['accuracy']}%({effective_accuracy:.0f}%) | Rel:{self.calculate_gun_reliability(player)}%"
weapon_name = player.get('weapon', 'pistol').replace('_', ' ').title()
stats_line3 = f"{nick} > Weapon: {weapon_name}{gun_status} | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}"
# Advanced stats line # Optional advanced stats line (if requested)
best_time = player.get('best_time', 999.9) best_time = player.get('best_time', 999.9)
best_display = f"{best_time:.3f}s" if best_time < 999 else "none" best_display = f"{best_time:.3f}s" if best_time < 999 else "none"
stats_line4 = f"{nick} > Best time: {best_display} | Avg time: {average_time:.3f}s | Jams: {player.get('jammed_count', 0)} | Accidents: {player.get('accidents', 0)} | Lucky shots: {player.get('lucky_shots', 0)}" stats_line3 = f"{nick} > Best:{best_display} | Avg:{average_time:.3f}s | Jams:{player.get('jammed_count', 0)} | Accidents:{player.get('accidents', 0)} | Lucky:{player.get('lucky_shots', 0)}"
# Send all stats # Send compact stats (just 2-3 lines instead of 4+)
await self.send_user_message(nick, channel, stats_line1) await self.send_user_message(nick, channel, stats_line1)
await self.send_user_message(nick, channel, stats_line2) await self.send_user_message(nick, channel, stats_line2)
# Display inventory compactly if player has items
if player.get('inventory'):
inventory_items = []
shop_items = {
'1': 'Bullet', '2': 'Clip', '3': 'AP', '4': 'Explosive',
'5': 'Gun', '6': 'Grease', '7': 'Sight', '8': 'Detector', '9': 'Silencer',
'10': 'Clover', '11': 'Shotgun', '12': 'Rifle', '13': 'Sniper', '14': 'Auto',
'15': 'Sand', '16': 'Water', '17': 'Sabotage', '18': 'Life Ins',
'19': 'Liability', '20': 'Decoy', '21': 'Bread', '22': 'Detector', '23': 'Mech Duck'
}
for item_id, count in player['inventory'].items():
item_name = shop_items.get(item_id, f"#{item_id}")
inventory_items.append(f"{item_id}:{item_name}({count})")
if inventory_items:
max_slots = self.get_config('economy.max_inventory_slots', 20)
total_items = sum(player['inventory'].values())
inventory_display = f"{nick} > {self.colors['magenta']}Inventory ({total_items}/{max_slots}):{self.colors['reset']} {' | '.join(inventory_items[:8])}"
if len(inventory_items) > 8:
inventory_display += f" ... +{len(inventory_items) - 8} more"
await self.send_user_message(nick, channel, inventory_display)
# Only show advanced stats if player has significant activity
if player.get('reflex_shots', 0) > 5:
await self.send_user_message(nick, channel, stats_line3) await self.send_user_message(nick, channel, stats_line3)
await self.send_user_message(nick, channel, stats_line4)
# Inventory display # Inventory display
if player.get('inventory'): if player.get('inventory'):
@@ -1238,15 +1557,14 @@ class SimpleIRCBot:
} }
for item_id, count in player['inventory'].items(): for item_id, count in player['inventory'].items():
item_name = shop_items.get(item_id, f"Item #{item_id}") inventory_items.append(f"{item_id}({count})")
inventory_items.append(f"{item_id}:{item_name}({count})")
if inventory_items: if inventory_items:
max_slots = self.get_config('economy.max_inventory_slots', 20) max_slots = self.get_config('economy.max_inventory_slots', 20)
total_items = sum(player['inventory'].values()) total_items = sum(player['inventory'].values())
inventory_display = f"{nick} > {self.colors['magenta']}Inventory ({total_items}/{max_slots}):{self.colors['reset']} {' | '.join(inventory_items[:10])}" inventory_display = f"{nick} > {self.colors['magenta']}Items({total_items}/{max_slots}):{self.colors['reset']} {' '.join(inventory_items[:15])}"
if len(inventory_items) > 10: if len(inventory_items) > 15:
inventory_display += f" ... and {len(inventory_items) - 10} more" inventory_display += f" +{len(inventory_items) - 15}"
await self.send_user_message(nick, channel, inventory_display) await self.send_user_message(nick, channel, inventory_display)
async def handle_rearm(self, nick, channel, user, target_nick): async def handle_rearm(self, nick, channel, user, target_nick):
@@ -1334,7 +1652,7 @@ class SimpleIRCBot:
self.send_message(channel, line) self.send_message(channel, line)
async def handle_output(self, nick, channel, user, output_type): async def handle_output(self, nick, channel, user, output_type):
"""Handle output mode setting (PRIVMSG or NOTICE)""" """Handle output mode setting (PUBLIC, NOTICE, or PRIVMSG)"""
player = self.get_player(user) player = self.get_player(user)
if not player: if not player:
self.send_message(channel, f"{nick} > Player data not found!") self.send_message(channel, f"{nick} > Player data not found!")
@@ -1342,25 +1660,120 @@ class SimpleIRCBot:
# Ensure player has settings (for existing players) # Ensure player has settings (for existing players)
if 'settings' not in player: if 'settings' not in player:
default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC')
player['settings'] = { player['settings'] = {
'notices': True 'output_mode': default_mode
} }
output_type = output_type.upper() output_type = output_type.upper()
if output_type == 'PRIVMSG': if output_type == 'PUBLIC':
player['settings']['notices'] = False player['settings']['output_mode'] = 'PUBLIC'
self.save_database() self.save_database()
self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PRIVMSG{self.colors['reset']} (private messages)") self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PUBLIC{self.colors['reset']} (channel messages)")
elif output_type == 'NOTICE': elif output_type == 'NOTICE':
player['settings']['notices'] = True player['settings']['output_mode'] = 'NOTICE'
self.save_database() self.save_database()
self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}NOTICE{self.colors['reset']} (channel notices)") self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}NOTICE{self.colors['reset']} (channel notices)")
elif output_type == 'PRIVMSG':
player['settings']['output_mode'] = 'PRIVMSG'
self.save_database()
self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PRIVMSG{self.colors['reset']} (private messages)")
else: else:
current_mode = 'NOTICE' if player['settings']['notices'] else 'PRIVMSG' current_mode = player['settings'].get('output_mode', 'NOTICE')
self.send_message(channel, f"{nick} > Current output mode: {self.colors['cyan']}{current_mode}{self.colors['reset']} | Usage: !output PRIVMSG or !output NOTICE") self.send_message(channel, f"{nick} > Current output mode: {self.colors['cyan']}{current_mode}{self.colors['reset']} | Usage: !output PUBLIC, !output NOTICE, or !output PRIVMSG")
async def handle_ducktime(self, nick, channel):
"""Show time until next duck spawn"""
current_time = time.time()
# Check if there are active ducks
channel_ducks = self.ducks.get(channel, [])
alive_ducks = [duck for duck in channel_ducks if duck.get('alive')]
if alive_ducks:
self.send_message(channel, f"{nick} > {len(alive_ducks)} duck(s) currently active! Go hunt them!")
return
# Show next spawn time if we have it
if channel in self.next_duck_spawn:
next_spawn = self.next_duck_spawn[channel]
time_until = max(0, next_spawn - current_time)
if time_until > 0:
minutes = int(time_until // 60)
seconds = int(time_until % 60)
difficulty = self.duck_difficulty.get(channel, 1.0)
difficulty_text = f" (Difficulty: {difficulty:.1f}x)" if difficulty > 1.0 else ""
self.send_message(channel, f"{nick} > Next duck spawn in {self.colors['cyan']}{minutes}m {seconds}s{self.colors['reset']}{difficulty_text}")
else:
self.send_message(channel, f"{nick} > Duck should spawn any moment now...")
else:
# Estimate based on spawn range
min_min = self.duck_spawn_min // 60
max_min = self.duck_spawn_max // 60
self.send_message(channel, f"{nick} > Ducks spawn every {min_min}-{max_min} minutes (spawn time varies)")
async def handle_lastduck(self, nick, channel):
"""Show information about the last duck shot in this channel"""
if channel not in self.channel_records:
self.send_message(channel, f"{nick} > No duck records found for {channel}")
return
last_duck = self.channel_records[channel].get('last_duck')
if not last_duck:
self.send_message(channel, f"{nick} > No ducks have been shot in {channel} yet")
return
# Format the last duck info
hunter = last_duck['hunter']
duck_type = last_duck['type']
shot_time = last_duck['shot_time']
time_ago = time.time() - last_duck['timestamp']
# Format time ago
if time_ago < 60:
time_ago_str = f"{int(time_ago)}s ago"
elif time_ago < 3600:
time_ago_str = f"{int(time_ago // 60)}m ago"
else:
time_ago_str = f"{int(time_ago // 3600)}h ago"
duck_emoji = "🥇" if duck_type == "golden" else "🦆"
self.send_message(channel, f"{nick} > Last duck: {duck_emoji} {duck_type} duck shot by {self.colors['cyan']}{hunter}{self.colors['reset']} in {shot_time:.3f}s ({time_ago_str})")
async def handle_records(self, nick, channel):
"""Show channel records and statistics"""
if channel not in self.channel_records:
self.send_message(channel, f"{nick} > No records found for {channel}")
return
records = self.channel_records[channel]
# Header
self.send_message(channel, f"{nick} > {self.colors['yellow']}📊 {channel.upper()} RECORDS 📊{self.colors['reset']}")
# Fastest shot
fastest = records.get('fastest_shot')
if fastest:
self.send_message(channel, f"🏆 Fastest shot: {self.colors['green']}{fastest['time']:.3f}s{self.colors['reset']} by {self.colors['cyan']}{fastest['hunter']}{self.colors['reset']} ({fastest['duck_type']} duck)")
# Total stats
total_ducks = records.get('total_ducks', 0)
total_shots = records.get('total_shots', 0)
accuracy = (total_ducks / total_shots * 100) if total_shots > 0 else 0
self.send_message(channel, f"📈 Total: {total_ducks} ducks shot, {total_shots} shots fired ({accuracy:.1f}% accuracy)")
# Current difficulty
difficulty = self.duck_difficulty.get(channel, 1.0)
if difficulty > 1.0:
self.send_message(channel, f"🧠 Duck intelligence: {self.colors['red']}{difficulty:.2f}x harder{self.colors['reset']} (they're learning!)")
else:
self.send_message(channel, f"🧠 Duck intelligence: Normal (fresh ducks)")
async def handle_shop(self, nick, channel, user): async def handle_shop(self, nick, channel, user):
player = self.get_player(user) player = self.get_player(user)
@@ -1368,10 +1781,62 @@ class SimpleIRCBot:
self.send_message(channel, f"{nick} > Player data not found!") self.send_message(channel, f"{nick} > Player data not found!")
return return
# Show compact shop in eggdrop style # Create organized shop display
shop_msg = f"[Duck Hunt] Purchasable items: 1-Extra bullet(7xp) 2-Extra clip(20xp) 3-AP ammo(15xp) 4-Explosive ammo(25xp) 5-Repurchase gun(40xp) 6-Grease(8xp) 7-Sight(6xp) 8-Infrared detector(15xp) 9-Silencer(5xp) 10-Four-leaf clover(13xp) 11-Shotgun(100xp) 12-Assault rifle(200xp) 13-Sniper rifle(350xp) 14-Auto shotgun(500xp) 15-Sand(7xp) 16-Water bucket(10xp) 17-Sabotage(14xp) 18-Life insurance(10xp) 19-Liability insurance(5xp) 20-Decoy(80xp) 21-Bread(50xp) 22-Duck detector(50xp) 23-Mechanical duck(50xp)" shop_items = {
self.send_message(channel, f"{nick} > {shop_msg}") 'ammo': [
self.send_message(channel, f"{nick} > Your XP: {player['xp']} | Use !shop <id> to purchase") {'id': '1', 'name': 'Extra bullet', 'cost': 7},
{'id': '2', 'name': 'Extra clip', 'cost': 20},
{'id': '3', 'name': 'AP ammo', 'cost': 15},
{'id': '4', 'name': 'Explosive ammo', 'cost': 25}
],
'weapons': [
{'id': '11', 'name': 'Shotgun', 'cost': 100},
{'id': '12', 'name': 'Assault rifle', 'cost': 200},
{'id': '13', 'name': 'Sniper rifle', 'cost': 350},
{'id': '14', 'name': 'Auto shotgun', 'cost': 500}
],
'upgrades': [
{'id': '6', 'name': 'Grease', 'cost': 8},
{'id': '7', 'name': 'Sight', 'cost': 6},
{'id': '8', 'name': 'Infrared detector', 'cost': 15},
{'id': '9', 'name': 'Silencer', 'cost': 5},
{'id': '10', 'name': 'Four-leaf clover', 'cost': 13}
],
'special': [
{'id': '5', 'name': 'Repurchase gun', 'cost': 40},
{'id': '15', 'name': 'Sand', 'cost': 7},
{'id': '16', 'name': 'Water bucket', 'cost': 10},
{'id': '17', 'name': 'Sabotage', 'cost': 14},
{'id': '20', 'name': 'Decoy', 'cost': 80},
{'id': '21', 'name': 'Bread', 'cost': 50},
{'id': '22', 'name': 'Duck detector', 'cost': 50},
{'id': '23', 'name': 'Mechanical duck', 'cost': 50}
],
'insurance': [
{'id': '18', 'name': 'Life insurance', 'cost': 10},
{'id': '19', 'name': 'Liability insurance', 'cost': 5}
]
}
# Format each category
def format_items(items, color):
formatted = []
for item in items:
formatted.append(f"{item['id']}.{item['name']}({item['cost']})")
return ' '.join(formatted)
# Send compact shop header
self.send_message(channel, f"{nick} > {self.colors['yellow']}SHOP{self.colors['reset']} XP:{self.colors['green']}{player['xp']}{self.colors['reset']}")
# Send categorized items in compact format
self.send_message(channel, f"{self.colors['cyan']}Ammo:{self.colors['reset']} {format_items(shop_items['ammo'], self.colors['cyan'])}")
self.send_message(channel, f"{self.colors['red']}Weapons:{self.colors['reset']} {format_items(shop_items['weapons'], self.colors['red'])}")
self.send_message(channel, f"{self.colors['green']}Upgrades:{self.colors['reset']} {format_items(shop_items['upgrades'], self.colors['green'])}")
self.send_message(channel, f"{self.colors['yellow']}Special:{self.colors['reset']} {format_items(shop_items['special'], self.colors['yellow'])}")
self.send_message(channel, f"{self.colors['magenta']}Insurance:{self.colors['reset']} {format_items(shop_items['insurance'], self.colors['magenta'])}")
# Compact footer
self.send_message(channel, f"Use !shop <id> to buy")
async def handle_buy(self, nick, channel, item, user): async def handle_buy(self, nick, channel, item, user):
"""Buy items and add to inventory""" """Buy items and add to inventory"""
@@ -1976,6 +2441,11 @@ class SimpleIRCBot:
wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max) wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max)
self.logger.info(f"Waiting {wait_time//60}m {wait_time%60}s for next duck") self.logger.info(f"Waiting {wait_time//60}m {wait_time%60}s for next duck")
# Set next spawn time for all channels
next_spawn_time = time.time() + wait_time
for channel in self.channels_joined:
self.next_duck_spawn[channel] = next_spawn_time
# Sleep in chunks to check shutdown flag # Sleep in chunks to check shutdown flag
for _ in range(wait_time): for _ in range(wait_time):
if self.shutdown_requested: if self.shutdown_requested:

File diff suppressed because it is too large Load Diff