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", "connection": {
"port": 6697, "server": "irc.rizon.net",
"nick": "DickHunt", "port": 6697,
"channels": ["#ct"], "nick": "DickHunt",
"ssl": true, "channels": ["#ct"],
"ssl": true,
"password": "your_iline_password_here",
"max_retries": 3,
"retry_delay": 5,
"timeout": 30
},
"sasl": { "sasl": {
"enabled": false, "enabled": false,
"username": "duckhunt", "username": "duckhunt",
"password": "duckhunt//789//" "password": "duckhunt//789//"
}, },
"password": "your_iline_password_here",
"admins": ["peorth", "computertech", "colby"], "admins": ["peorth", "computertech", "colby"],
"duck_spawn_min": 10, "duck_spawning": {
"duck_spawn_max": 30, "spawn_min": 10,
"duck_timeout": 60, "spawn_max": 30,
"befriend_success_rate": 75, "timeout": 60,
"rearm_on_duck_shot": true, "rearm_on_duck_shot": true
},
"golden_duck_chance": 0.15, "duck_types": {
"golden_duck_min_hp": 3, "normal": {
"golden_duck_max_hp": 5, "xp": 10,
"golden_duck_xp": 15, "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, "player_defaults": {
"fast_duck_timeout": 30, "accuracy": 75,
"fast_duck_xp": 12 "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,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 - _close_connection:559: 🔌 IRC connection closed
2025-09-23 20:12:39,766 [INFO ] DuckHuntBot - run:542: ✅ Bot shutdown complete 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", "name": "Duck Novice",
"min_xp": 0, "min_xp": 0,
"max_xp": 49, "max_xp": 49,
"befriend_success_rate": 85, "befriend_success_rate": 95,
"accuracy_modifier": 5, "accuracy_modifier": 25,
"jam_chance": 0,
"duck_spawn_speed_modifier": 1.0, "duck_spawn_speed_modifier": 1.0,
"magazines": 3, "magazines": 3,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -19,8 +20,9 @@
"name": "Pond Visitor", "name": "Pond Visitor",
"min_xp": 50, "min_xp": 50,
"max_xp": 149, "max_xp": 149,
"befriend_success_rate": 80, "befriend_success_rate": 85,
"accuracy_modifier": 0, "accuracy_modifier": 15,
"jam_chance": 1,
"duck_spawn_speed_modifier": 1.0, "duck_spawn_speed_modifier": 1.0,
"magazines": 3, "magazines": 3,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -30,10 +32,11 @@
"name": "Duck Hunter", "name": "Duck Hunter",
"min_xp": 150, "min_xp": 150,
"max_xp": 299, "max_xp": 299,
"befriend_success_rate": 75, "befriend_success_rate": 80,
"accuracy_modifier": -5, "accuracy_modifier": 5,
"jam_chance": 2,
"duck_spawn_speed_modifier": 0.9, "duck_spawn_speed_modifier": 0.9,
"magazines": 2, "magazines": 3,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
"description": "Your reputation precedes you, ducks are more cautious" "description": "Your reputation precedes you, ducks are more cautious"
}, },
@@ -41,8 +44,9 @@
"name": "Wetland Stalker", "name": "Wetland Stalker",
"min_xp": 300, "min_xp": 300,
"max_xp": 599, "max_xp": 599,
"befriend_success_rate": 70, "befriend_success_rate": 75,
"accuracy_modifier": -10, "accuracy_modifier": -5,
"jam_chance": 3,
"duck_spawn_speed_modifier": 0.8, "duck_spawn_speed_modifier": 0.8,
"magazines": 2, "magazines": 2,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -52,8 +56,9 @@
"name": "Apex Predator", "name": "Apex Predator",
"min_xp": 600, "min_xp": 600,
"max_xp": 999, "max_xp": 999,
"befriend_success_rate": 65, "befriend_success_rate": 70,
"accuracy_modifier": -15, "accuracy_modifier": -15,
"jam_chance": 4,
"duck_spawn_speed_modifier": 0.7, "duck_spawn_speed_modifier": 0.7,
"magazines": 2, "magazines": 2,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -63,8 +68,9 @@
"name": "Duck Whisperer", "name": "Duck Whisperer",
"min_xp": 1000, "min_xp": 1000,
"max_xp": 1999, "max_xp": 1999,
"befriend_success_rate": 60, "befriend_success_rate": 65,
"accuracy_modifier": -20, "accuracy_modifier": -20,
"jam_chance": 5,
"duck_spawn_speed_modifier": 0.6, "duck_spawn_speed_modifier": 0.6,
"magazines": 1, "magazines": 1,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -74,8 +80,9 @@
"name": "Legendary Hunter", "name": "Legendary Hunter",
"min_xp": 2000, "min_xp": 2000,
"max_xp": 4999, "max_xp": 4999,
"befriend_success_rate": 55, "befriend_success_rate": 60,
"accuracy_modifier": -25, "accuracy_modifier": -25,
"jam_chance": 6,
"duck_spawn_speed_modifier": 0.5, "duck_spawn_speed_modifier": 0.5,
"magazines": 1, "magazines": 1,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,
@@ -85,8 +92,9 @@
"name": "Duck Deity", "name": "Duck Deity",
"min_xp": 5000, "min_xp": 5000,
"max_xp": 999999, "max_xp": 999999,
"befriend_success_rate": 50, "befriend_success_rate": 55,
"accuracy_modifier": -30, "accuracy_modifier": -30,
"jam_chance": 8,
"duck_spawn_speed_modifier": 0.4, "duck_spawn_speed_modifier": 0.4,
"magazines": 1, "magazines": 1,
"bullets_per_magazine": 6, "bullets_per_magazine": 6,

View File

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

View File

@@ -28,6 +28,14 @@
"description": "Clean your gun - decreases jam chance by 10%", "description": "Clean your gun - decreases jam chance by 10%",
"type": "clean_gun", "type": "clean_gun",
"amount": -10 "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: class DuckDB:
"""Simplified database management""" """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.db_file = db_file
self.bot = bot
self.players = {} self.players = {}
self.logger = logging.getLogger('DuckHuntBot.DB') self.logger = logging.getLogger('DuckHuntBot.DB')
self.load_database() self.load_database()
@@ -67,7 +68,9 @@ class DuckDB:
sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP 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_shot'] = max(0, int(player_data.get('ducks_shot', 0)))
sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 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)) sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
# Ammo system with validation # Ammo system with validation
@@ -209,23 +212,38 @@ class DuckDB:
return self.create_player(nick) return self.create_player(nick)
def create_player(self, 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: try:
# Sanitize nick # Sanitize nick
safe_nick = str(nick)[:50] if nick else 'Unknown' 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 { return {
'nick': safe_nick, 'nick': safe_nick,
'xp': 0, 'xp': xp,
'ducks_shot': 0, 'ducks_shot': 0,
'ducks_befriended': 0, 'ducks_befriended': 0,
'current_ammo': 6, # Bullets in current magazine 'current_ammo': bullets_per_mag, # Bullets in current magazine
'magazines': 3, # Total magazines (including current) 'magazines': magazines, # Total magazines (including current)
'bullets_per_magazine': 6, # Bullets per magazine 'bullets_per_magazine': bullets_per_mag, # Bullets per magazine
'accuracy': 65, 'accuracy': accuracy, # Starting accuracy from config
'jam_chance': 5, # 5% base gun jamming chance 'jam_chance': jam_chance, # Base gun jamming chance from config
'gun_confiscated': False, 'gun_confiscated': False,
'inventory': {}, # {item_id: quantity} 'inventory': {}, # Empty starting inventory
'temporary_effects': [] # List of temporary effects 'temporary_effects': [] # List of temporary effects
} }
except Exception as e: except Exception as e:
@@ -238,7 +256,7 @@ class DuckDB:
'current_ammo': 6, 'current_ammo': 6,
'magazines': 3, 'magazines': 3,
'bullets_per_magazine': 6, 'bullets_per_magazine': 6,
'accuracy': 65, 'accuracy': 75,
'jam_chance': 5, 'jam_chance': 5,
'gun_confiscated': False, 'gun_confiscated': False,
'inventory': {}, 'inventory': {},

View File

@@ -26,7 +26,7 @@ class DuckHuntBot:
self.channels_joined = set() self.channels_joined = set()
self.shutdown_requested = False self.shutdown_requested = False
self.db = DuckDB() self.db = DuckDB(bot=self)
self.game = DuckGame(self, self.db) self.game = DuckGame(self, self.db)
self.messages = MessageManager() self.messages = MessageManager()
@@ -97,8 +97,8 @@ class DuckHuntBot:
async def connect(self): async def connect(self):
"""Connect to IRC server with comprehensive error handling""" """Connect to IRC server with comprehensive error handling"""
max_retries = 3 max_retries = self.get_config('connection.max_retries', 3) or 3
retry_delay = 5 retry_delay = self.get_config('connection.retry_delay', 5) or 5
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
@@ -117,7 +117,7 @@ class DuckHuntBot:
self.config['port'], self.config['port'],
ssl=ssl_context 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']}") self.logger.info(f"✅ Successfully connected to {self.config['server']}:{self.config['port']}")
@@ -454,7 +454,7 @@ class DuckHuntBot:
xp = player.get('xp', 0) xp = player.get('xp', 0)
ducks_shot = player.get('ducks_shot', 0) ducks_shot = player.get('ducks_shot', 0)
ducks_befriended = player.get('ducks_befriended', 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 # Ammo info
current_ammo = player.get('current_ammo', 0) current_ammo = player.get('current_ammo', 0)
@@ -531,13 +531,24 @@ class DuckHuntBot:
if not result["success"]: if not result["success"]:
message = f"{nick} > {result['message']}" message = f"{nick} > {result['message']}"
else: 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}!" message = f"{nick} > Used {result['item_name']} on {target_nick}!"
else: else:
message = f"{nick} > Used {result['item_name']}!" message = f"{nick} > Used {result['item_name']}!"
# Add remaining count if any # Add remaining count if any (not for bread message which has its own format)
if result.get("remaining_in_inventory", 0) > 0: if effect_type != 'attract_ducks' and result.get("remaining_in_inventory", 0) > 0:
message += f" ({result['remaining_in_inventory']} remaining)" message += f" ({result['remaining_in_inventory']} remaining)"
self.send_message(channel, message) self.send_message(channel, message)

View File

@@ -35,8 +35,16 @@ class DuckGame:
try: try:
while True: while True:
# Wait random time between spawns, but in small chunks for responsiveness # Wait random time between spawns, but in small chunks for responsiveness
min_wait = self.bot.get_config('duck_spawn_min', 300) # 5 minutes min_wait = self.bot.get_config('duck_spawning.spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawn_max', 900) # 15 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) wait_time = random.randint(min_wait, max_wait)
# Sleep in 1-second intervals to allow for quick cancellation # Sleep in 1-second intervals to allow for quick cancellation
@@ -65,12 +73,9 @@ class DuckGame:
for channel, ducks in self.ducks.items(): for channel, ducks in self.ducks.items():
ducks_to_remove = [] ducks_to_remove = []
for duck in ducks: 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') duck_type = duck.get('duck_type', 'normal')
if duck_type == 'fast': timeout = self.bot.get_config(f'duck_types.{duck_type}.timeout', 60)
timeout = self.bot.get_config('fast_duck_timeout', 30)
else:
timeout = self.bot.get_config('duck_timeout', 60)
if current_time - duck['spawn_time'] > timeout: if current_time - duck['spawn_time'] > timeout:
ducks_to_remove.append(duck) ducks_to_remove.append(duck)
@@ -94,6 +99,9 @@ class DuckGame:
for channel in channels_to_clear: for channel in channels_to_clear:
if channel in self.ducks and not self.ducks[channel]: if channel in self.ducks and not self.ducks[channel]:
del self.ducks[channel] del self.ducks[channel]
# Clean expired effects every loop iteration
self._clean_expired_effects()
except asyncio.CancelledError: except asyncio.CancelledError:
self.logger.info("Duck timeout loop cancelled") self.logger.info("Duck timeout loop cancelled")
@@ -175,8 +183,8 @@ class DuckGame:
'message_args': {'nick': nick} 'message_args': {'nick': nick}
} }
# Check for gun jamming # Check for gun jamming using level-based jam chance
jam_chance = player.get('jam_chance', 5) / 100.0 # Convert percentage to decimal jam_chance = self.bot.levels.get_jam_chance(player) / 100.0 # Convert percentage to decimal
if random.random() < jam_chance: if random.random() < jam_chance:
# Gun jammed! Use ammo but don't shoot # Gun jammed! Use ammo but don't shoot
player['current_ammo'] = player.get('current_ammo', 1) - 1 player['current_ammo'] = player.get('current_ammo', 1) - 1
@@ -216,7 +224,9 @@ class DuckGame:
if duck['current_hp'] > 0: if duck['current_hp'] > 0:
# Still alive, reveal it's golden but don't remove # 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() self.db.save_database()
return { return {
'success': True, 'success': True,
@@ -241,14 +251,16 @@ class DuckGame:
else: else:
# Normal duck # Normal duck
self.ducks[channel].pop(0) self.ducks[channel].pop(0)
xp_gained = 10 xp_gained = self.bot.get_config('normal_duck_xp', 10)
message_key = 'bang_hit' message_key = 'bang_hit'
# Apply XP and level changes # Apply XP and level changes
old_level = self.bot.levels.calculate_player_level(player) old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_shot'] = player.get('ducks_shot', 0) + 1 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 # Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player) new_level = self.bot.levels.calculate_player_level(player)
@@ -272,7 +284,9 @@ class DuckGame:
} }
else: else:
# Miss! Duck stays in the channel # 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() self.db.save_database()
return { return {
'success': True, 'success': True,
@@ -309,8 +323,8 @@ class DuckGame:
# Success - befriend the duck # Success - befriend the duck
duck = self.ducks[channel].pop(0) duck = self.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10) # Lower XP gain than shooting
xp_gained = 5 xp_gained = self.bot.get_config('befriend_duck_xp', 5)
old_level = self.bot.levels.calculate_player_level(player) old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1 player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
@@ -407,4 +421,45 @@ class DuckGame:
if rearmed_count > 0: if rearmed_count > 0:
self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot") self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot")
except Exception as e: 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: def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
"""Get player's accuracy modified by their level""" """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) level_info = self.get_player_level_info(player)
modifier = level_info.get('accuracy_modifier', 0) modifier = level_info.get('accuracy_modifier', 0)
@@ -154,9 +154,20 @@ class LevelManager:
level_info = self.get_player_level_info(player) level_info = self.get_player_level_info(player)
level_rate = level_info.get('befriend_success_rate', base_rate) 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)) 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: def get_duck_spawn_modifier(self, player_levels: list) -> float:
"""Get duck spawn speed modifier based on highest level player in channel""" """Get duck spawn speed modifier based on highest level player in channel"""
if not player_levels: if not player_levels:

View File

@@ -178,7 +178,7 @@ class ShopManager:
elif item_type == 'accuracy': elif item_type == 'accuracy':
# Increase accuracy up to 100% # Increase accuracy up to 100%
current_accuracy = player.get('accuracy', 65) current_accuracy = player.get('accuracy', 75)
new_accuracy = min(current_accuracy + amount, 100) new_accuracy = min(current_accuracy + amount, 100)
player['accuracy'] = new_accuracy player['accuracy'] = new_accuracy
return { return {
@@ -279,7 +279,7 @@ class ShopManager:
elif item_type == 'sabotage_accuracy': elif item_type == 'sabotage_accuracy':
# Reduce target's accuracy temporarily # 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) new_acc = max(current_acc + amount, 10) # Min 10% accuracy (amount is negative)
player['accuracy'] = new_acc player['accuracy'] = new_acc
@@ -325,6 +325,27 @@ class ShopManager:
"new_total": new_jam "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: else:
self.logger.warning(f"Unknown item type: {item_type}") self.logger.warning(f"Unknown item type: {item_type}")
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"} return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}