Restructure config.json with nested hierarchy and dot notation access

- Reorganized config.json into logical sections: connection, duck_spawning, duck_types, player_defaults, gameplay, features, limits
- Enhanced get_config() method to support dot notation (e.g., 'duck_types.normal.xp')
- Added comprehensive configurable parameters for all game mechanics
- Updated player creation to use configurable starting values
- Added individual timeout settings per duck type
- Made XP rewards, accuracy mechanics, and game limits fully configurable
- Fixed syntax errors in duck_spawn_loop function
This commit is contained in:
2025-09-24 20:26:49 +01:00
parent 6ca624bd2f
commit 74f3afdf4b
18 changed files with 306 additions and 189 deletions

74
.gitignore vendored
View File

@@ -1,74 +0,0 @@
# 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_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/

Binary file not shown.

View File

@@ -1,30 +1,77 @@
{
"server": "irc.rizon.net",
"port": 6697,
"nick": "DickHunt",
"channels": ["#ct"],
"ssl": true,
"connection": {
"server": "irc.rizon.net",
"port": 6697,
"nick": "DickHunt",
"channels": ["#ct"],
"ssl": true,
"password": "your_iline_password_here",
"max_retries": 3,
"retry_delay": 5,
"timeout": 30
},
"sasl": {
"enabled": false,
"username": "duckhunt",
"password": "duckhunt//789//"
},
"password": "your_iline_password_here",
"admins": ["peorth", "computertech", "colby"],
"duck_spawn_min": 10,
"duck_spawn_max": 30,
"duck_timeout": 60,
"befriend_success_rate": 75,
"rearm_on_duck_shot": true,
"duck_spawning": {
"spawn_min": 10,
"spawn_max": 30,
"timeout": 60,
"rearm_on_duck_shot": true
},
"golden_duck_chance": 0.15,
"golden_duck_min_hp": 3,
"golden_duck_max_hp": 5,
"golden_duck_xp": 15,
"duck_types": {
"normal": {
"xp": 10,
"timeout": 60
},
"golden": {
"chance": 0.15,
"min_hp": 3,
"max_hp": 5,
"xp": 15,
"timeout": 50
},
"fast": {
"chance": 0.25,
"timeout": 20,
"xp": 20
}
},
"fast_duck_chance": 0.25,
"fast_duck_timeout": 30,
"fast_duck_xp": 12
"player_defaults": {
"accuracy": 75,
"magazines": 3,
"bullets_per_magazine": 6,
"jam_chance": 5,
"xp": 0
},
"gameplay": {
"befriend_success_rate": 75,
"befriend_xp": 5,
"accuracy_gain_on_hit": 1,
"accuracy_loss_on_miss": 2,
"min_accuracy": 10,
"max_accuracy": 100,
"min_befriend_success_rate": 5,
"max_befriend_success_rate": 95
},
"features": {
"shop_enabled": true,
"inventory_enabled": true,
"auto_rearm_enabled": true
},
"limits": {
"max_inventory_items": 20,
"max_temp_effects": 5
}
}

View File

@@ -640,3 +640,14 @@
2025-09-23 20:12:39,664 [INFO ] DuckHuntBot - run:535: 💾 Database saved
2025-09-23 20:12:39,766 [INFO ] DuckHuntBot - _close_connection:559: 🔌 IRC connection closed
2025-09-23 20:12:39,766 [INFO ] DuckHuntBot - run:542: ✅ Bot shutdown complete
2025-09-24 16:45:23,678 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-24 16:45:23,679 [INFO ] DuckHuntBot.DB - load_database:47: Loaded 1 players from duckhunt.json
2025-09-24 16:45:23,681 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-24 16:45:23,682 [INFO ] DuckHuntBot.Shop - load_items:30: Loaded 4 shop items from /home/colby/duckhunt/shop.json
2025-09-24 16:45:23,683 [INFO ] DuckHuntBot.Levels - load_levels:28: Loaded 8 levels from /home/colby/duckhunt/levels.json
2025-09-24 18:56:53,426 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-24 18:56:53,428 [INFO ] DuckHuntBot.DB - load_database:47: Loaded 1 players from duckhunt.json
2025-09-24 18:56:53,431 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-24 18:56:53,434 [INFO ] DuckHuntBot.Shop - load_items:30: Loaded 5 shop items from /home/colby/duckhunt/shop.json
2025-09-24 18:56:53,436 [INFO ] DuckHuntBot.Levels - load_levels:28: Loaded 8 levels from /home/colby/duckhunt/levels.json
2025-09-24 18:56:53,439 [INFO ] DuckHuntBot.DB - load_database:47: Loaded 1 players from duckhunt.json

View File

@@ -8,8 +8,9 @@
"name": "Duck Novice",
"min_xp": 0,
"max_xp": 49,
"befriend_success_rate": 85,
"accuracy_modifier": 5,
"befriend_success_rate": 95,
"accuracy_modifier": 25,
"jam_chance": 0,
"duck_spawn_speed_modifier": 1.0,
"magazines": 3,
"bullets_per_magazine": 6,
@@ -19,8 +20,9 @@
"name": "Pond Visitor",
"min_xp": 50,
"max_xp": 149,
"befriend_success_rate": 80,
"accuracy_modifier": 0,
"befriend_success_rate": 85,
"accuracy_modifier": 15,
"jam_chance": 1,
"duck_spawn_speed_modifier": 1.0,
"magazines": 3,
"bullets_per_magazine": 6,
@@ -30,10 +32,11 @@
"name": "Duck Hunter",
"min_xp": 150,
"max_xp": 299,
"befriend_success_rate": 75,
"accuracy_modifier": -5,
"befriend_success_rate": 80,
"accuracy_modifier": 5,
"jam_chance": 2,
"duck_spawn_speed_modifier": 0.9,
"magazines": 2,
"magazines": 3,
"bullets_per_magazine": 6,
"description": "Your reputation precedes you, ducks are more cautious"
},
@@ -41,8 +44,9 @@
"name": "Wetland Stalker",
"min_xp": 300,
"max_xp": 599,
"befriend_success_rate": 70,
"accuracy_modifier": -10,
"befriend_success_rate": 75,
"accuracy_modifier": -5,
"jam_chance": 3,
"duck_spawn_speed_modifier": 0.8,
"magazines": 2,
"bullets_per_magazine": 6,
@@ -52,8 +56,9 @@
"name": "Apex Predator",
"min_xp": 600,
"max_xp": 999,
"befriend_success_rate": 65,
"befriend_success_rate": 70,
"accuracy_modifier": -15,
"jam_chance": 4,
"duck_spawn_speed_modifier": 0.7,
"magazines": 2,
"bullets_per_magazine": 6,
@@ -63,8 +68,9 @@
"name": "Duck Whisperer",
"min_xp": 1000,
"max_xp": 1999,
"befriend_success_rate": 60,
"befriend_success_rate": 65,
"accuracy_modifier": -20,
"jam_chance": 5,
"duck_spawn_speed_modifier": 0.6,
"magazines": 1,
"bullets_per_magazine": 6,
@@ -74,8 +80,9 @@
"name": "Legendary Hunter",
"min_xp": 2000,
"max_xp": 4999,
"befriend_success_rate": 55,
"befriend_success_rate": 60,
"accuracy_modifier": -25,
"jam_chance": 6,
"duck_spawn_speed_modifier": 0.5,
"magazines": 1,
"bullets_per_magazine": 6,
@@ -85,8 +92,9 @@
"name": "Duck Deity",
"min_xp": 5000,
"max_xp": 999999,
"befriend_success_rate": 50,
"befriend_success_rate": 55,
"accuracy_modifier": -30,
"jam_chance": 8,
"duck_spawn_speed_modifier": 0.4,
"magazines": 1,
"bullets_per_magazine": 6,

View File

@@ -1,51 +1,52 @@
{
"duck_spawn": [
"・゜゜・。。・゜゜\\_O< {bold}QUACK!{reset}",
"・゜゜・。。・゜゜\\_o< {light_grey}quack~{reset}",
"・゜゜・。。・゜゜\\_O> {bold}*flap flap*{reset}"
"・゜゜・。。・゜゜\\_O< QUACK!",
"・゜゜・。。・゜゜\\_o< quack~",
"・゜゜・。。・゜゜\\_O> *flap flap*"
],
"duck_flies_away": "The {bold}duck{reset} flies away. ·°'`'°-.,¸¸.·°'`",
"fast_duck_flies_away": "The {cyan}fast duck{reset} quickly flies away! ·°'`'°-.,¸¸.·°'`",
"golden_duck_flies_away": "The {yellow}{bold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`",
"bang_hit": "{nick} > {green}*BANG*{reset} You shot the duck! [{green}+{xp_gained} xp{reset}] [Total ducks: {bold}{ducks_shot}{reset}]",
"bang_hit_golden": "{nick} > {green}*BANG*{reset} You shot a {yellow}{bold}GOLDEN DUCK{reset}! [{yellow}{hp_remaining} HP remaining{reset}] [{green}+{xp_gained} xp{reset}]",
"bang_hit_golden_killed": "{nick} > {green}*BANG*{reset} You killed the {yellow}{bold}GOLDEN DUCK{reset}! [{green}+{xp_gained} xp{reset}] [Total ducks: {bold}{ducks_shot}{reset}]",
"bang_hit_fast": "{nick} > {green}*BANG*{reset} You shot a {cyan}{bold}FAST DUCK{reset}! [{green}+{xp_gained} xp{reset}] [Total ducks: {bold}{ducks_shot}{reset}]",
"bang_miss": "{nick} > {red}*BANG*{reset} You missed the {cyan}duck{reset}!",
"bang_no_duck": "{nick} > {red}*BANG*{reset} What did you shoot at? There is {red}no duck{reset} in the area... [{red}GUN CONFISCATED{reset}]",
"bang_no_ammo": "{nick} > {orange}*click*{reset} You're out of ammo! Use {blue}!reload{reset}",
"bang_gun_jammed": "{nick} > {red}*click*{reset} Your gun jammed! [{red}AMMO WASTED{reset}]",
"bang_not_armed": "{nick} > You are {red}not armed{reset}.",
"bef_success": "{nick} > {orange}*befriend*{reset} You befriended the duck! [{green}+{xp_gained} xp{reset}] [Ducks befriended: {bold}{ducks_befriended}{reset}]",
"bef_failed": "{nick} > {pink}*gentle approach*{reset} The {cyan}duck{reset} doesn't trust you and {yellow}flies away{reset}...",
"bef_no_duck": "{nick} > {pink}*gentle approach*{reset} There is {red}no duck{reset} to befriend in the area...",
"bef_duck_shot": "{nick} > {pink}*gentle approach*{reset} The {cyan}duck{reset} is {red}already dead{reset}! You can't befriend it now...",
"reload_success": "{nick} > {orange}*click*{reset} New magazine loaded! [Ammo: {green}{ammo}{reset}/{green}{max_ammo}{reset}] [Spare magazines: {blue}{chargers}{reset}]",
"reload_already_loaded": "{nick} > Your gun is {green}already loaded{reset}!",
"reload_no_chargers": "{nick} > You're out of {red}spare magazines{reset}!",
"reload_not_armed": "{nick} > You are {red}not armed{reset}.",
"shop_display": "DuckHunt Shop: {items} | You have {green}{xp} XP{reset}",
"shop_item_format": "({blue}{id}{reset}) {cyan}{name}{reset} - {green}{price} XP{reset}",
"help_header": "{blue}DuckHunt Commands:{reset}",
"help_user_commands": "{blue}!bang{reset} - Shoot at ducks | {blue}!bef{reset} - Befriend ducks | {blue}!reload{reset} - Reload your gun | {blue}!shop{reset} - View/buy from shop | {blue}!duckstats{reset} - View your stats and items | {blue}!use{reset} - Use inventory items",
"help_help_command": "{blue}!duckhelp{reset} - Show this help",
"help_admin_commands": "{red}Admin:{reset} {blue}!rearm <player>{reset} | {blue}!disarm <player>{reset} | {blue}!ignore <player>{reset} | {blue}!unignore <player>{reset} | {blue}!ducklaunch{reset}",
"admin_rearm_player": "[{red}ADMIN{reset}] {cyan}{target}{reset} has been rearmed by {blue}{admin}{reset}",
"admin_rearm_all": "[{red}ADMIN{reset}] All players have been rearmed by {blue}{admin}{reset}",
"admin_rearm_self": "[{red}ADMIN{reset}] {blue}{admin}{reset} has rearmed themselves",
"admin_disarm": "[{red}ADMIN{reset}] {cyan}{target}{reset} has been disarmed by {blue}{admin}{reset}",
"admin_ignore": "[{red}ADMIN{reset}] {cyan}{target}{reset} is now ignored by {blue}{admin}{reset}",
"admin_unignore": "[{red}ADMIN{reset}] {cyan}{target}{reset} is no longer ignored by {blue}{admin}{reset}",
"admin_ducklaunch": "[{red}ADMIN{reset}] A {cyan}duck{reset} has been launched by {blue}{admin}{reset}",
"admin_ducklaunch_not_enabled": "[{red}ADMIN{reset}] This channel is {red}not enabled{reset} for duckhunt",
"usage_rearm": "Usage: {blue}!rearm <player>{reset}",
"usage_disarm": "Usage: {blue}!disarm <player>{reset}",
"usage_ignore": "Usage: {blue}!ignore <player>{reset}",
"usage_unignore": "Usage: {blue}!unignore <player>{reset}",
"shop_buy_success": "{nick} > You bought {cyan}{item_name}{reset}! [-{red}{price} XP{reset}] [Remaining: {green}{remaining_xp} XP{reset}]",
"shop_buy_insufficient_xp": "{nick} > You don't have enough {red}XP{reset} to buy {cyan}{item_name}{reset}. Need {red}{price} XP{reset}, you have {green}{current_xp} XP{reset}.",
"shop_buy_invalid_id": "{nick} > {red}Invalid item ID{reset}. Use {blue}!shop{reset} to see available items.",
"shop_buy_usage": "Usage: {blue}!shop buy <item_id>{reset}",
"duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`",
"fast_duck_flies_away": "The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`",
"golden_duck_flies_away": "The golden duck flies away majestically. ·°'`'°-.,¸¸.·°'`",
"bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
"bang_hit_golden": "{nick} > *BANG* You shot a GOLDEN DUCK! [{hp_remaining} HP remaining] [+{xp_gained} xp]",
"bang_hit_golden_killed": "{nick} > *BANG* You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
"bang_hit_fast": "{nick} > *BANG* You shot a FAST DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
"bang_miss": "{nick} > *BANG* You missed the duck!",
"bang_no_duck": "{nick} > *BANG* What did you shoot at? There is no duck in the area... [GUN CONFISCATED]",
"bang_no_ammo": "{nick} > *click* You're out of ammo! Use !reload",
"bang_gun_jammed": "{nick} > *click* Your gun jammed! [AMMO WASTED]",
"bang_not_armed": "{nick} > You are not armed.",
"bef_success": "{nick} > *befriend* You befriended the duck! [+{xp_gained} xp] [Ducks befriended: {ducks_befriended}]",
"bef_failed": "{nick} > *gentle approach* The duck doesn't trust you and flies away...",
"bef_no_duck": "{nick} > *gentle approach* There is no duck to befriend in the area...",
"bef_duck_shot": "{nick} > *gentle approach* The duck is already dead! You can't befriend it now...",
"reload_success": "{nick} > *click* New magazine loaded! [Ammo: {ammo}/{max_ammo}] [Spare magazines: {chargers}]",
"reload_already_loaded": "{nick} > Your gun is already loaded!",
"reload_no_chargers": "{nick} > You're out of spare magazines!",
"reload_not_armed": "{nick} > You are not armed.",
"shop_display": "DuckHunt Shop: {items} | You have {xp} XP",
"shop_item_format": "({id}) {name} - {price} XP",
"help_header": "DuckHunt Commands:",
"help_user_commands": "!bang - Shoot at ducks | !bef - Befriend ducks | !reload - Reload your gun | !shop - View/buy from shop | !duckstats - View your stats and items | !use - Use inventory items",
"help_help_command": "!duckhelp - Show this help",
"help_admin_commands": "Admin: !rearm <player> | !disarm <player> | !ignore <player> | !unignore <player> | !ducklaunch",
"admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}",
"admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}",
"admin_rearm_self": "[ADMIN] {admin} has rearmed themselves",
"admin_disarm": "[ADMIN] {target} has been disarmed by {admin}",
"admin_ignore": "[ADMIN] {target} is now ignored by {admin}",
"admin_unignore": "[ADMIN] {target} is no longer ignored by {admin}",
"admin_ducklaunch": "[ADMIN] A duck has been launched by {admin}",
"admin_ducklaunch_not_enabled": "[ADMIN] This channel is not enabled for duckhunt",
"usage_rearm": "Usage: !rearm <player>",
"usage_disarm": "Usage: !disarm <player>",
"usage_ignore": "Usage: !ignore <player>",
"usage_unignore": "Usage: !unignore <player>",
"shop_buy_success": "{nick} > You bought {item_name}! [-{price} XP] [Remaining: {remaining_xp} XP]",
"shop_buy_insufficient_xp": "{nick} > You don't have enough XP to buy {item_name}. Need {price} XP, you have {current_xp} XP.",
"shop_buy_invalid_id": "{nick} > Invalid item ID. Use !shop to see available items.",
"shop_buy_usage": "Usage: !shop buy <item_id>",
"use_attract_ducks": "{nick} > You scattered bread around the pond! Ducks will spawn {spawn_multiplier}x faster for {duration} minutes.",
"colours": {
"white": "\u00030",

View File

@@ -28,6 +28,14 @@
"description": "Clean your gun - decreases jam chance by 10%",
"type": "clean_gun",
"amount": -10
},
"5": {
"name": "Bread",
"price": 50,
"description": "Attract ducks - increases duck spawn rate for 20 minutes",
"type": "attract_ducks",
"duration": 1200,
"spawn_multiplier": 2.0
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -12,8 +12,9 @@ import os
class DuckDB:
"""Simplified database management"""
def __init__(self, db_file="duckhunt.json"):
def __init__(self, db_file="duckhunt.json", bot=None):
self.db_file = db_file
self.bot = bot
self.players = {}
self.logger = logging.getLogger('DuckHuntBot.DB')
self.load_database()
@@ -67,7 +68,9 @@ class DuckDB:
sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP
sanitized['ducks_shot'] = max(0, int(player_data.get('ducks_shot', 0)))
sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 0)))
sanitized['accuracy'] = max(0, min(100, int(player_data.get('accuracy', 65)))) # 0-100 range
default_accuracy = self.bot.get_config('default_accuracy', 75) if self.bot else 75
max_accuracy = self.bot.get_config('max_accuracy', 100) if self.bot else 100
sanitized['accuracy'] = max(0, min(max_accuracy, int(player_data.get('accuracy', default_accuracy)))) # 0-max_accuracy range
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
# Ammo system with validation
@@ -209,23 +212,38 @@ class DuckDB:
return self.create_player(nick)
def create_player(self, nick):
"""Create a new player with basic stats and validation"""
"""Create a new player with configurable starting stats and inventory"""
try:
# Sanitize nick
safe_nick = str(nick)[:50] if nick else 'Unknown'
# Get configurable defaults from bot config
if self.bot:
accuracy = self.bot.get_config('player_defaults.accuracy', 75)
magazines = self.bot.get_config('player_defaults.magazines', 3)
bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6)
jam_chance = self.bot.get_config('player_defaults.jam_chance', 5)
xp = self.bot.get_config('player_defaults.xp', 0)
else:
# Fallback defaults if no bot config available
accuracy = 75
magazines = 3
bullets_per_mag = 6
jam_chance = 5
xp = 0
return {
'nick': safe_nick,
'xp': 0,
'xp': xp,
'ducks_shot': 0,
'ducks_befriended': 0,
'current_ammo': 6, # Bullets in current magazine
'magazines': 3, # Total magazines (including current)
'bullets_per_magazine': 6, # Bullets per magazine
'accuracy': 65,
'jam_chance': 5, # 5% base gun jamming chance
'current_ammo': bullets_per_mag, # Bullets in current magazine
'magazines': magazines, # Total magazines (including current)
'bullets_per_magazine': bullets_per_mag, # Bullets per magazine
'accuracy': accuracy, # Starting accuracy from config
'jam_chance': jam_chance, # Base gun jamming chance from config
'gun_confiscated': False,
'inventory': {}, # {item_id: quantity}
'inventory': {}, # Empty starting inventory
'temporary_effects': [] # List of temporary effects
}
except Exception as e:
@@ -238,7 +256,7 @@ class DuckDB:
'current_ammo': 6,
'magazines': 3,
'bullets_per_magazine': 6,
'accuracy': 65,
'accuracy': 75,
'jam_chance': 5,
'gun_confiscated': False,
'inventory': {},

View File

@@ -26,7 +26,7 @@ class DuckHuntBot:
self.channels_joined = set()
self.shutdown_requested = False
self.db = DuckDB()
self.db = DuckDB(bot=self)
self.game = DuckGame(self, self.db)
self.messages = MessageManager()
@@ -97,8 +97,8 @@ class DuckHuntBot:
async def connect(self):
"""Connect to IRC server with comprehensive error handling"""
max_retries = 3
retry_delay = 5
max_retries = self.get_config('connection.max_retries', 3) or 3
retry_delay = self.get_config('connection.retry_delay', 5) or 5
for attempt in range(max_retries):
try:
@@ -117,7 +117,7 @@ class DuckHuntBot:
self.config['port'],
ssl=ssl_context
),
timeout=30.0 # 30 second connection timeout
timeout=self.get_config('connection.timeout', 30) or 30.0 # Connection timeout from config
)
self.logger.info(f"✅ Successfully connected to {self.config['server']}:{self.config['port']}")
@@ -454,7 +454,7 @@ class DuckHuntBot:
xp = player.get('xp', 0)
ducks_shot = player.get('ducks_shot', 0)
ducks_befriended = player.get('ducks_befriended', 0)
accuracy = player.get('accuracy', 65)
accuracy = player.get('accuracy', self.get_config('player_defaults.accuracy', 75))
# Ammo info
current_ammo = player.get('current_ammo', 0)
@@ -531,13 +531,24 @@ class DuckHuntBot:
if not result["success"]:
message = f"{nick} > {result['message']}"
else:
if result.get("target_affected"):
# Handle specific item effect messages
effect = result.get('effect', {})
effect_type = effect.get('type', '')
if effect_type == 'attract_ducks':
# Use specific message for bread
message = self.messages.get('use_attract_ducks',
nick=nick,
spawn_multiplier=effect.get('spawn_multiplier', 2.0),
duration=effect.get('duration', 10)
)
elif result.get("target_affected"):
message = f"{nick} > Used {result['item_name']} on {target_nick}!"
else:
message = f"{nick} > Used {result['item_name']}!"
# Add remaining count if any
if result.get("remaining_in_inventory", 0) > 0:
# Add remaining count if any (not for bread message which has its own format)
if effect_type != 'attract_ducks' and result.get("remaining_in_inventory", 0) > 0:
message += f" ({result['remaining_in_inventory']} remaining)"
self.send_message(channel, message)

View File

@@ -35,8 +35,16 @@ class DuckGame:
try:
while True:
# Wait random time between spawns, but in small chunks for responsiveness
min_wait = self.bot.get_config('duck_spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawn_max', 900) # 15 minutes
min_wait = self.bot.get_config('duck_spawning.spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawning.spawn_max', 900) # 15 minutes
# Check for active bread effects to modify spawn timing
spawn_multiplier = self._get_active_spawn_multiplier()
if spawn_multiplier > 1.0:
# Reduce wait time when bread is active
min_wait = int(min_wait / spawn_multiplier)
max_wait = int(max_wait / spawn_multiplier)
wait_time = random.randint(min_wait, max_wait)
# Sleep in 1-second intervals to allow for quick cancellation
@@ -65,12 +73,9 @@ class DuckGame:
for channel, ducks in self.ducks.items():
ducks_to_remove = []
for duck in ducks:
# Different timeouts for different duck types
# Get timeout for each duck type from config
duck_type = duck.get('duck_type', 'normal')
if duck_type == 'fast':
timeout = self.bot.get_config('fast_duck_timeout', 30)
else:
timeout = self.bot.get_config('duck_timeout', 60)
timeout = self.bot.get_config(f'duck_types.{duck_type}.timeout', 60)
if current_time - duck['spawn_time'] > timeout:
ducks_to_remove.append(duck)
@@ -94,6 +99,9 @@ class DuckGame:
for channel in channels_to_clear:
if channel in self.ducks and not self.ducks[channel]:
del self.ducks[channel]
# Clean expired effects every loop iteration
self._clean_expired_effects()
except asyncio.CancelledError:
self.logger.info("Duck timeout loop cancelled")
@@ -175,8 +183,8 @@ class DuckGame:
'message_args': {'nick': nick}
}
# Check for gun jamming
jam_chance = player.get('jam_chance', 5) / 100.0 # Convert percentage to decimal
# Check for gun jamming using level-based jam chance
jam_chance = self.bot.levels.get_jam_chance(player) / 100.0 # Convert percentage to decimal
if random.random() < jam_chance:
# Gun jammed! Use ammo but don't shoot
player['current_ammo'] = player.get('current_ammo', 1) - 1
@@ -216,7 +224,9 @@ class DuckGame:
if duck['current_hp'] > 0:
# Still alive, reveal it's golden but don't remove
player['accuracy'] = min(player.get('accuracy', 65) + 1, 100)
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
self.db.save_database()
return {
'success': True,
@@ -241,14 +251,16 @@ class DuckGame:
else:
# Normal duck
self.ducks[channel].pop(0)
xp_gained = 10
xp_gained = self.bot.get_config('normal_duck_xp', 10)
message_key = 'bang_hit'
# Apply XP and level changes
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
player['accuracy'] = min(player.get('accuracy', 65) + 1, 100)
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
@@ -272,7 +284,9 @@ class DuckGame:
}
else:
# Miss! Duck stays in the channel
player['accuracy'] = max(player.get('accuracy', 65) - 2, 10)
accuracy_loss = self.bot.get_config('accuracy_loss_on_miss', 2)
min_accuracy = self.bot.get_config('min_accuracy', 10)
player['accuracy'] = max(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) - accuracy_loss, min_accuracy)
self.db.save_database()
return {
'success': True,
@@ -309,8 +323,8 @@ class DuckGame:
# Success - befriend the duck
duck = self.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10)
xp_gained = 5
# Lower XP gain than shooting
xp_gained = self.bot.get_config('befriend_duck_xp', 5)
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
@@ -407,4 +421,45 @@ class DuckGame:
if rearmed_count > 0:
self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot")
except Exception as e:
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
def _get_active_spawn_multiplier(self):
"""Get the current spawn rate multiplier from active bread effects"""
import time
max_multiplier = 1.0
current_time = time.time()
try:
for player_name, player_data in self.db.players.items():
effects = player_data.get('temporary_effects', [])
for effect in effects:
if (effect.get('type') == 'attract_ducks' and
effect.get('expires_at', 0) > current_time):
multiplier = effect.get('spawn_multiplier', 1.0)
max_multiplier = max(max_multiplier, multiplier)
return max_multiplier
except Exception as e:
self.logger.error(f"Error getting spawn multiplier: {e}")
return 1.0
def _clean_expired_effects(self):
"""Remove expired temporary effects from all players"""
import time
current_time = time.time()
try:
for player_name, player_data in self.db.players.items():
effects = player_data.get('temporary_effects', [])
active_effects = []
for effect in effects:
if effect.get('expires_at', 0) > current_time:
active_effects.append(effect)
if len(active_effects) != len(effects):
player_data['temporary_effects'] = active_effects
self.logger.debug(f"Cleaned expired effects for {player_name}")
except Exception as e:
self.logger.error(f"Error cleaning expired effects: {e}")

View File

@@ -141,7 +141,7 @@ class LevelManager:
def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
"""Get player's accuracy modified by their level"""
base_accuracy = player.get('accuracy', 65)
base_accuracy = player.get('accuracy', 75) # This will be updated by bot config in create_player
level_info = self.get_player_level_info(player)
modifier = level_info.get('accuracy_modifier', 0)
@@ -154,9 +154,20 @@ class LevelManager:
level_info = self.get_player_level_info(player)
level_rate = level_info.get('befriend_success_rate', base_rate)
# Return as percentage (0-100)
# Return as percentage (0-100) - these will be configurable later if bot reference is available
return max(5.0, min(95.0, level_rate))
def get_jam_chance(self, player: Dict[str, Any]) -> float:
"""Get player's gun jam chance based on their level"""
level_info = self.get_player_level_info(player)
level_data = self.get_level_data(level_info['level'])
if level_data and 'jam_chance' in level_data:
return level_data['jam_chance']
# Fallback to old system if no level-specific jam chance
return player.get('jam_chance', 5)
def get_duck_spawn_modifier(self, player_levels: list) -> float:
"""Get duck spawn speed modifier based on highest level player in channel"""
if not player_levels:

View File

@@ -178,7 +178,7 @@ class ShopManager:
elif item_type == 'accuracy':
# Increase accuracy up to 100%
current_accuracy = player.get('accuracy', 65)
current_accuracy = player.get('accuracy', 75)
new_accuracy = min(current_accuracy + amount, 100)
player['accuracy'] = new_accuracy
return {
@@ -279,7 +279,7 @@ class ShopManager:
elif item_type == 'sabotage_accuracy':
# Reduce target's accuracy temporarily
current_acc = player.get('accuracy', 65)
current_acc = player.get('accuracy', 75)
new_acc = max(current_acc + amount, 10) # Min 10% accuracy (amount is negative)
player['accuracy'] = new_acc
@@ -325,6 +325,27 @@ class ShopManager:
"new_total": new_jam
}
elif item_type == 'attract_ducks':
# Add bread effect to increase duck spawn rate
if 'temporary_effects' not in player:
player['temporary_effects'] = []
duration = item.get('duration', 600) # 10 minutes default
spawn_multiplier = item.get('spawn_multiplier', 2.0) # 2x spawn rate default
effect = {
'type': 'attract_ducks',
'spawn_multiplier': spawn_multiplier,
'expires_at': time.time() + duration
}
player['temporary_effects'].append(effect)
return {
"type": "attract_ducks",
"spawn_multiplier": spawn_multiplier,
"duration": duration // 60 # return duration in minutes
}
else:
self.logger.warning(f"Unknown item type: {item_type}")
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}