feat: Add shop system, befriend command, and level system

- Added configurable shop system with shop.json
- Created ShopManager class for modular shop handling
- Implemented level system with levels.json for difficulty scaling
- Added multiple duck spawn messages with random selection
- Enhanced message system with color placeholders
- Added ducks_befriended tracking separate from ducks_shot
- Updated help system and admin commands
- All systems tested and working correctly
This commit is contained in:
2025-09-23 18:05:28 +01:00
parent de64756b6d
commit 3aaf0d0bb4
15 changed files with 733 additions and 60 deletions

View File

@@ -14,5 +14,6 @@
"duck_spawn_min": 10, "duck_spawn_min": 10,
"duck_spawn_max": 30, "duck_spawn_max": 30,
"duck_timeout": 60 "duck_timeout": 60,
"befriend_success_rate": 75
} }

View File

@@ -2,15 +2,16 @@
"players": { "players": {
"computertech": { "computertech": {
"nick": "ComputerTech", "nick": "ComputerTech",
"xp": 10, "xp": 45,
"ducks_shot": 1, "ducks_shot": 4,
"ammo": 5, "ammo": 2,
"max_ammo": 6, "max_ammo": 6,
"chargers": 2, "chargers": 1,
"max_chargers": 2, "max_chargers": 2,
"accuracy": 64, "accuracy": 65,
"gun_confiscated": false "gun_confiscated": false,
"ducks_befriended": 1
} }
}, },
"last_save": "1758592642.337953" "last_save": "1758646365.5768785"
} }

View File

@@ -342,3 +342,132 @@
2025-09-23 02:57:22,339 [INFO ] DuckHuntBot - run:419: 💾 Database saved 2025-09-23 02:57:22,339 [INFO ] DuckHuntBot - run:419: 💾 Database saved
2025-09-23 02:57:22,340 [INFO ] DuckHuntBot - _close_connection:454: 🔌 IRC connection closed 2025-09-23 02:57:22,340 [INFO ] DuckHuntBot - _close_connection:454: 🔌 IRC connection closed
2025-09-23 02:57:22,341 [INFO ] DuckHuntBot - run:426: ✅ Bot shutdown complete 2025-09-23 02:57:22,341 [INFO ] DuckHuntBot - run:426: ✅ Bot shutdown complete
2025-09-23 16:49:06,532 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:49:06,533 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 16:49:06,534 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:49:06,534 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 16:49:06,815 [INFO ] DuckHuntBot - connect:88: Connected to irc.rizon.net:6697
2025-09-23 16:49:06,817 [ERROR ] DuckHuntBot - run:428: ❌ Bot error: 'DuckHuntBot' object has no attribute 'shutdown_event'
2025-09-23 16:49:06,819 [INFO ] DuckHuntBot - run:430: 🔄 Final cleanup...
2025-09-23 16:49:06,820 [INFO ] DuckHuntBot - message_loop:380: Message loop cancelled
2025-09-23 16:49:06,823 [INFO ] DuckHuntBot - message_loop:384: Message loop ended
2025-09-23 16:49:06,826 [INFO ] DuckHuntBot - run:444: 💾 Database saved
2025-09-23 16:49:06,874 [ERROR ] DuckHuntBot - _close_connection:483: ❌ Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685)
2025-09-23 16:49:06,875 [INFO ] DuckHuntBot - run:451: ✅ Bot shutdown complete
2025-09-23 16:51:03,713 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:51:03,716 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 16:51:03,717 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:51:03,717 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 16:51:04,034 [INFO ] DuckHuntBot - connect:88: Connected to irc.rizon.net:6697
2025-09-23 16:51:04,036 [ERROR ] DuckHuntBot - run:428: ❌ Bot error: 'DuckHuntBot' object has no attribute 'shutdown_event'
2025-09-23 16:51:04,036 [INFO ] DuckHuntBot - run:430: 🔄 Final cleanup...
2025-09-23 16:51:04,036 [INFO ] DuckHuntBot - message_loop:380: Message loop cancelled
2025-09-23 16:51:04,036 [INFO ] DuckHuntBot - message_loop:384: Message loop ended
2025-09-23 16:51:04,038 [INFO ] DuckHuntBot - run:444: 💾 Database saved
2025-09-23 16:51:04,095 [ERROR ] DuckHuntBot - _close_connection:483: ❌ Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685)
2025-09-23 16:51:04,095 [INFO ] DuckHuntBot - run:451: ✅ Bot shutdown complete
2025-09-23 16:52:13,816 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:52:13,817 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 16:52:13,818 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 16:52:13,818 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 16:52:14,082 [INFO ] DuckHuntBot - connect:88: Connected to irc.rizon.net:6697
2025-09-23 16:52:14,083 [INFO ] DuckHuntBot - run:401: 🦆 Bot is now running! Press Ctrl+C to stop.
2025-09-23 16:52:14,573 [INFO ] DuckHuntBot - handle_message:114: Successfully registered with IRC server
2025-09-23 16:52:35,085 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:53:53,120 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:55:12,150 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:56:18,166 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:57:34,190 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:58:37,206 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 16:59:54,240 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:01:05,255 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:02:02,274 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:03:12,297 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:04:24,322 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:05:33,340 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:06:40,363 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:06:52,804 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:06:52,805 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 17:06:52,806 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:06:52,807 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 17:06:53,090 [INFO ] DuckHuntBot - connect:88: Connected to irc.rizon.net:6697
2025-09-23 17:06:53,094 [INFO ] DuckHuntBot - run:401: 🦆 Bot is now running! Press Ctrl+C to stop.
2025-09-23 17:07:06,659 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:07:09,342 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:07:20,148 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:07:31,073 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:07:31,074 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 17:07:31,075 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:07:31,076 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 17:07:31,318 [INFO ] DuckHuntBot - connect:88: Connected to irc.rizon.net:6697
2025-09-23 17:07:31,319 [INFO ] DuckHuntBot - run:401: 🦆 Bot is now running! Press Ctrl+C to stop.
2025-09-23 17:07:31,739 [INFO ] DuckHuntBot - handle_message:114: Successfully registered with IRC server
2025-09-23 17:07:45,324 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:09:13,347 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:10:29,378 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:11:38,392 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:12:47,413 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:14:00,456 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:15:14,463 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:16:34,482 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:17:45,499 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:19:06,515 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:20:24,546 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:21:40,559 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:22:47,570 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:24:00,607 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:25:14,623 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:26:30,648 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:27:50,675 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:29:08,696 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:30:14,724 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:31:33,738 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:33:02,761 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:34:20,783 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:35:30,808 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:35:52,920 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:01,664 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:01,986 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:02,267 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:02,538 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:02,815 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:36:22,777 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:36:22,778 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 17:36:22,779 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:36:22,779 [INFO ] DuckHuntBot - load_shop_items:57: Loaded 3 shop items
2025-09-23 17:36:22,779 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 17:36:23,018 [INFO ] DuckHuntBot - connect:110: Connected to irc.rizon.net:6697
2025-09-23 17:36:23,019 [INFO ] DuckHuntBot - run:509: 🦆 Bot is now running! Press Ctrl+C to stop.
2025-09-23 17:36:23,504 [INFO ] DuckHuntBot - handle_message:136: Successfully registered with IRC server
2025-09-23 17:36:47,024 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:37:24,684 [INFO ] DuckHuntBot - signal_handler:95: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown...
2025-09-23 17:37:32,155 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:37:32,156 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json
2025-09-23 17:37:32,157 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation
2025-09-23 17:37:32,158 [INFO ] DuckHuntBot - load_shop_items:58: Loaded 3 shop items
2025-09-23 17:37:32,159 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot...
2025-09-23 17:37:32,392 [INFO ] DuckHuntBot - connect:111: Connected to irc.rizon.net:6697
2025-09-23 17:37:32,392 [INFO ] DuckHuntBot - run:510: 🦆 Bot is now running! Press Ctrl+C to stop.
2025-09-23 17:37:32,534 [INFO ] DuckHuntBot - handle_message:137: Successfully registered with IRC server
2025-09-23 17:38:43,405 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:40:04,430 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:41:28,459 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:42:50,488 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:43:54,512 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:45:05,539 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:46:22,571 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:47:32,595 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:48:37,614 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:50:06,649 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:51:28,685 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:52:43,706 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:53:04,709 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:54:16,736 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:55:32,758 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:56:50,792 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:58:15,809 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 17:59:34,837 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 18:00:53,856 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 18:02:15,867 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 18:03:33,887 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct
2025-09-23 18:04:45,906 [INFO ] DuckHuntBot.Game - spawn_duck:105: Duck spawned in #ct

80
levels.json Normal file
View File

@@ -0,0 +1,80 @@
{
"level_calculation": {
"method": "total_ducks",
"description": "Level based on total ducks interacted with (shot + befriended)"
},
"levels": {
"1": {
"name": "Duck Novice",
"min_ducks": 0,
"max_ducks": 9,
"befriend_success_rate": 85,
"accuracy_modifier": 5,
"duck_spawn_speed_modifier": 1.0,
"description": "Just starting out, ducks are trusting and easier to hit"
},
"2": {
"name": "Pond Visitor",
"min_ducks": 10,
"max_ducks": 24,
"befriend_success_rate": 80,
"accuracy_modifier": 0,
"duck_spawn_speed_modifier": 1.0,
"description": "Ducks are getting wary of you"
},
"3": {
"name": "Duck Hunter",
"min_ducks": 25,
"max_ducks": 49,
"befriend_success_rate": 75,
"accuracy_modifier": -5,
"duck_spawn_speed_modifier": 0.9,
"description": "Your reputation precedes you, ducks are more cautious"
},
"4": {
"name": "Wetland Stalker",
"min_ducks": 50,
"max_ducks": 99,
"befriend_success_rate": 70,
"accuracy_modifier": -10,
"duck_spawn_speed_modifier": 0.8,
"description": "Ducks flee at your approach, spawns are less frequent"
},
"5": {
"name": "Apex Predator",
"min_ducks": 100,
"max_ducks": 199,
"befriend_success_rate": 65,
"accuracy_modifier": -15,
"duck_spawn_speed_modifier": 0.7,
"description": "You're feared throughout the pond, ducks are very elusive"
},
"6": {
"name": "Duck Whisperer",
"min_ducks": 200,
"max_ducks": 399,
"befriend_success_rate": 60,
"accuracy_modifier": -20,
"duck_spawn_speed_modifier": 0.6,
"description": "Only the bravest ducks dare show themselves"
},
"7": {
"name": "Legendary Hunter",
"min_ducks": 400,
"max_ducks": 999,
"befriend_success_rate": 55,
"accuracy_modifier": -25,
"duck_spawn_speed_modifier": 0.5,
"description": "Duck folklore speaks of your prowess, they're extremely rare"
},
"8": {
"name": "Duck Deity",
"min_ducks": 1000,
"max_ducks": 999999,
"befriend_success_rate": 50,
"accuracy_modifier": -30,
"duck_spawn_speed_modifier": 0.4,
"description": "You've transcended mortal hunting, ducks are mythically scarce"
}
}
}

View File

@@ -1,32 +1,43 @@
{{ {
"duck_spawn": "・゜゜・。。・゜゜\\_o< \u000303QUACK!\u000F A \u000308duck\u000F has appeared! Type \u000302!bang\u000F to shoot it!", "duck_spawn": [
"duck_flies_away": "The \u000308duck\u000F flies away. ·°'`'°-.,¸¸.·°'`", "・゜゜・。。・゜゜\\_O< {bold}QUACK!{reset}",
"bang_hit": "{nick} > \u000304*BANG*\u000F You shot the \u000308duck\u000F! [\u000303+{xp_gained} xp\u000F] [Total ducks: \u000302{ducks_shot}\u000F]", "・゜゜・。。・゜゜\\_O> {yellow}*flap flap*"
"bang_miss": "{nick} > \u000304*BANG*\u000F You missed the \u000308duck\u000F!", ],
"bang_no_duck": "{nick} > \u000304*BANG*\u000F What did you shoot at? There is \u000304no duck\u000F in the area... [\u000304GUN CONFISCATED\u000F]", "duck_flies_away": "The {cyan}duck{reset} flies away. ·°'`'°-.,¸¸.·°'`",
"bang_no_ammo": "{nick} > \u000307*click*\u000F You're out of ammo! Use \u000302!reload\u000F", "bang_hit": "{nick} > {red}*BANG*{reset} You shot the {cyan}duck{reset}! [{green}+{xp_gained} xp{reset}] [Total ducks: {blue}{ducks_shot}{reset}]",
"bang_not_armed": "{nick} > You are \u000304not armed\u000F.", "bang_miss": "{nick} > {red}*BANG*{reset} You missed the {cyan}duck{reset}!",
"reload_success": "{nick} > \u000307*click*\u000F Reloaded! [Ammo: \u000303{ammo}\u000F/\u000303{max_ammo}\u000F] [Chargers: \u000302{chargers}\u000F]", "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}]",
"reload_already_loaded": "{nick} > Your gun is \u000303already loaded\u000F!", "bang_no_ammo": "{nick} > {orange}*click*{reset} You're out of ammo! Use {blue}!reload{reset}",
"reload_no_chargers": "{nick} > You're out of \u000304chargers\u000F!", "bang_not_armed": "{nick} > You are {red}not armed{reset}.",
"reload_not_armed": "{nick} > You are \u000304not armed\u000F.", "bef_success": "{nick} > {green}*befriend*{reset} You befriended the {cyan}duck{reset}! [{green}+{xp_gained} xp{reset}] [Ducks befriended: {pink}{ducks_befriended}{reset}]",
"shop_display": "DuckHunt Shop: {items} | You have \u000303{xp} XP\u000F", "bef_failed": "{nick} > {pink}*gentle approach*{reset} The {cyan}duck{reset} doesn't trust you and {yellow}flies away{reset}...",
"shop_item_format": "(\u000302{id}\u000F) \u000310{name}\u000F - \u000303{price} XP\u000F", "bef_no_duck": "{nick} > {pink}*gentle approach*{reset} There is {red}no duck{reset} to befriend in the area...",
"help_header": "\u000302DuckHunt Commands:\u000F", "bef_duck_shot": "{nick} > {pink}*gentle approach*{reset} The {cyan}duck{reset} is {red}already dead{reset}! You can't befriend it now...",
"help_user_commands": "\u000302!bang\u000F - Shoot at ducks | \u000302!reload\u000F - Reload your gun | \u000302!shop\u000F - View the shop", "reload_success": "{nick} > {orange}*click*{reset} Reloaded! [Ammo: {green}{ammo}{reset}/{green}{max_ammo}{reset}] [Chargers: {blue}{chargers}{reset}]",
"help_help_command": "\u000302!duckhelp\u000F - Show this help", "reload_already_loaded": "{nick} > Your gun is {green}already loaded{reset}!",
"help_admin_commands": "\u000304Admin:\u000F \u000302!rearm <player>\u000F | \u000302!disarm <player>\u000F | \u000302!ignore <player>\u000F | \u000302!unignore <player>\u000F | \u000302!ducklaunch\u000F", "reload_no_chargers": "{nick} > You're out of {red}chargers{reset}!",
"admin_rearm_player": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been rearmed by \u000302{admin}\u000F", "reload_not_armed": "{nick} > You are {red}not armed{reset}.",
"admin_rearm_all": "[\u000304ADMIN\u000F] All players have been rearmed by \u000302{admin}\u000F", "shop_display": "DuckHunt Shop: {items} | You have {green}{xp} XP{reset}",
"admin_disarm": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been disarmed by \u000302{admin}\u000F", "shop_item_format": "({blue}{id}{reset}) {cyan}{name}{reset} - {green}{price} XP{reset}",
"admin_ignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is now ignored by \u000302{admin}\u000F", "help_header": "{blue}DuckHunt Commands:{reset}",
"admin_unignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is no longer ignored by \u000302{admin}\u000F", "help_user_commands": "{blue}!bang{reset} - Shoot at ducks | {blue}!bef{reset} - Befriend ducks | {blue}!reload{reset} - Reload your gun | {blue}!shop{reset} - View the shop",
"admin_ducklaunch": "[\u000304ADMIN\u000F] A \u000308duck\u000F has been launched by \u000302{admin}\u000F", "help_help_command": "{blue}!duckhelp{reset} - Show this help",
"admin_ducklaunch_not_enabled": "[\u000304ADMIN\u000F] This channel is \u000304not enabled\u000F for duckhunt", "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} | {blue}!reloadshop{reset}",
"usage_rearm": "Usage: \u000302!rearm <player>\u000F", "admin_rearm_player": "[{red}ADMIN{reset}] {cyan}{target}{reset} has been rearmed by {blue}{admin}{reset}",
"usage_disarm": "Usage: \u000302!disarm <player>\u000F", "admin_rearm_all": "[{red}ADMIN{reset}] All players have been rearmed by {blue}{admin}{reset}",
"usage_ignore": "Usage: \u000302!ignore <player>\u000F", "admin_disarm": "[{red}ADMIN{reset}] {cyan}{target}{reset} has been disarmed by {blue}{admin}{reset}",
"usage_unignore": "Usage: \u000302!unignore <player>\u000F", "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}",
"colours": { "colours": {
"white": "\u00030", "white": "\u00030",

25
shop.json Normal file
View File

@@ -0,0 +1,25 @@
{
"items": {
"1": {
"name": "Single Bullet",
"price": 5,
"description": "1 extra bullet",
"type": "ammo",
"amount": 1
},
"2": {
"name": "Accuracy Boost",
"price": 20,
"description": "+10% accuracy",
"type": "accuracy",
"amount": 10
},
"3": {
"name": "Lucky Charm",
"price": 30,
"description": "+5% duck spawn chance",
"type": "luck",
"amount": 5
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -63,6 +63,11 @@ class DuckDB:
if nick_lower not in self.players: if nick_lower not in self.players:
self.players[nick_lower] = self.create_player(nick) self.players[nick_lower] = self.create_player(nick)
else:
# Ensure existing players have new fields
player = self.players[nick_lower]
if 'ducks_befriended' not in player:
player['ducks_befriended'] = 0
return self.players[nick_lower] return self.players[nick_lower]
@@ -72,6 +77,7 @@ class DuckDB:
'nick': nick, 'nick': nick,
'xp': 0, 'xp': 0,
'ducks_shot': 0, 'ducks_shot': 0,
'ducks_befriended': 0,
'ammo': 6, 'ammo': 6,
'max_ammo': 6, 'max_ammo': 6,
'chargers': 2, 'chargers': 2,

View File

@@ -1,8 +1,3 @@
"""
Simplified DuckHunt IRC Bot - Core Features Only
Commands: !bang, !reload, !shop, !duckhelp, !rearm, !disarm, !ignore, !unignore, !ducklaunch
"""
import asyncio import asyncio
import ssl import ssl
import json import json
@@ -19,6 +14,7 @@ from .utils import parse_irc_message, InputValidator, MessageManager
from .db import DuckDB from .db import DuckDB
from .game import DuckGame from .game import DuckGame
from .sasl import SASLHandler from .sasl import SASLHandler
from .shop import ShopManager
class DuckHuntBot: class DuckHuntBot:
@@ -41,12 +37,9 @@ class DuckHuntBot:
self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])]
# Simple shop items - hardcoded # Initialize shop manager
self.shop_items = { shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json')
1: {"name": "Extra Shots", "price": 10, "description": "5 extra shots"}, self.shop = ShopManager(shop_file)
2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy"},
3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance"}
}
def get_config(self, path, default=None): def get_config(self, path, default=None):
"""Get configuration value using dot notation""" """Get configuration value using dot notation"""
@@ -148,10 +141,12 @@ class DuckHuntBot:
if cmd == "bang": if cmd == "bang":
await self.handle_bang(nick, channel, player) await self.handle_bang(nick, channel, player)
elif cmd == "bef" or cmd == "befriend":
await self.handle_bef(nick, channel, player)
elif cmd == "reload": elif cmd == "reload":
await self.handle_reload(nick, channel, player) await self.handle_reload(nick, channel, player)
elif cmd == "shop": elif cmd == "shop":
await self.handle_shop(nick, channel, player) await self.handle_shop(nick, channel, player, args)
elif cmd == "duckhelp": elif cmd == "duckhelp":
await self.handle_duckhelp(nick, channel, player) await self.handle_duckhelp(nick, channel, player)
elif cmd == "rearm" and self.is_admin(user): elif cmd == "rearm" and self.is_admin(user):
@@ -164,6 +159,8 @@ class DuckHuntBot:
await self.handle_unignore(nick, channel, args) await self.handle_unignore(nick, channel, args)
elif cmd == "ducklaunch" and self.is_admin(user): elif cmd == "ducklaunch" and self.is_admin(user):
await self.handle_ducklaunch(nick, channel, args) await self.handle_ducklaunch(nick, channel, args)
elif cmd == "reloadshop" and self.is_admin(user):
await self.handle_reloadshop(nick, channel, args)
async def handle_bang(self, nick, channel, player): async def handle_bang(self, nick, channel, player):
"""Handle !bang command""" """Handle !bang command"""
@@ -215,6 +212,44 @@ class DuckHuntBot:
self.db.save_database() self.db.save_database()
async def handle_bef(self, nick, channel, player):
"""Handle !bef (befriend) command"""
# Check for duck
if channel not in self.game.ducks or not self.game.ducks[channel]:
message = self.messages.get('bef_no_duck', nick=nick)
self.send_message(channel, message)
return
# Check befriend success rate from config (default 75%)
success_rate_config = self.get_config('befriend_success_rate', 75)
try:
success_rate = float(success_rate_config) / 100.0
except (ValueError, TypeError):
success_rate = 0.75 # 75% default
if random.random() < success_rate:
# Success - befriend the duck
duck = self.game.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10)
xp_gained = 5
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
message = self.messages.get('bef_success',
nick=nick,
xp_gained=xp_gained,
ducks_befriended=player['ducks_befriended'])
self.send_message(channel, message)
else:
# Failure - duck flies away, remove from channel
duck = self.game.ducks[channel].pop(0)
message = self.messages.get('bef_failed', nick=nick)
self.send_message(channel, message)
self.db.save_database()
async def handle_reload(self, nick, channel, player): async def handle_reload(self, nick, channel, player):
"""Handle !reload command""" """Handle !reload command"""
if player.get('gun_confiscated', False): if player.get('gun_confiscated', False):
@@ -243,10 +278,22 @@ class DuckHuntBot:
self.send_message(channel, message) self.send_message(channel, message)
self.db.save_database() self.db.save_database()
async def handle_shop(self, nick, channel, player): async def handle_shop(self, nick, channel, player, args=None):
"""Handle !shop command""" """Handle !shop command"""
# Handle buying: !shop buy <item_id>
if args and len(args) >= 2 and args[0].lower() == "buy":
try:
item_id = int(args[1])
await self.handle_shop_buy(nick, channel, player, item_id)
return
except (ValueError, IndexError):
message = self.messages.get('shop_buy_usage', nick=nick)
self.send_message(channel, message)
return
# Display shop items
items = [] items = []
for item_id, item in self.shop_items.items(): for item_id, item in self.shop.get_items().items():
item_text = self.messages.get('shop_item_format', item_text = self.messages.get('shop_item_format',
id=item_id, id=item_id,
name=item['name'], name=item['name'],
@@ -259,6 +306,36 @@ class DuckHuntBot:
self.send_message(channel, shop_text) self.send_message(channel, shop_text)
async def handle_shop_buy(self, nick, channel, player, item_id):
"""Handle buying an item from the shop"""
# Use ShopManager to handle the purchase
result = self.shop.purchase_item(player, item_id)
if not result["success"]:
# Handle different error types
if result["error"] == "invalid_id":
message = self.messages.get('shop_buy_invalid_id', nick=nick)
elif result["error"] == "insufficient_xp":
message = self.messages.get('shop_buy_insufficient_xp',
nick=nick,
item_name=result["item_name"],
price=result["price"],
current_xp=result["current_xp"])
else:
message = f"{nick} > Error: {result['message']}"
self.send_message(channel, message)
return
# Purchase successful
message = self.messages.get('shop_buy_success',
nick=nick,
item_name=result["item_name"],
price=result["price"],
remaining_xp=result["remaining_xp"])
self.send_message(channel, message)
self.db.save_database()
async def handle_duckhelp(self, nick, channel, player): async def handle_duckhelp(self, nick, channel, player):
"""Handle !duckhelp command""" """Handle !duckhelp command"""
help_lines = [ help_lines = [
@@ -357,6 +434,15 @@ class DuckHuntBot:
self.send_message(channel, admin_message) self.send_message(channel, admin_message)
self.send_message(channel, duck_message) self.send_message(channel, duck_message)
async def handle_reloadshop(self, nick, channel, args):
"""Handle !reloadshop admin command"""
old_count = len(self.shop.get_items())
new_count = self.shop.reload_items()
message = f"[ADMIN] Shop reloaded by {nick} - {new_count} items loaded"
self.send_message(channel, message)
self.logger.info(f"Shop reloaded by admin {nick}: {old_count} -> {new_count} items")
async def message_loop(self): async def message_loop(self):
"""Main message processing loop""" """Main message processing loop"""
@@ -397,20 +483,15 @@ class DuckHuntBot:
# Start game loops # Start game loops
game_task = asyncio.create_task(self.game.start_game_loops()) game_task = asyncio.create_task(self.game.start_game_loops())
message_task = asyncio.create_task(self.message_loop()) message_task = asyncio.create_task(self.message_loop())
shutdown_task = asyncio.create_task(self.shutdown_event.wait())
self.logger.info("🦆 Bot is now running! Press Ctrl+C to stop.") self.logger.info("🦆 Bot is now running! Press Ctrl+C to stop.")
# Wait for shutdown signal or task completion # Wait for shutdown signal or task completion
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
[game_task, message_task, shutdown_task], [game_task, message_task],
return_when=asyncio.FIRST_COMPLETED return_when=asyncio.FIRST_COMPLETED
) )
if shutdown_task in done:
self.logger.info("🛑 Shutdown signal received, cleaning up...")
await self._graceful_shutdown()
# Cancel remaining tasks # Cancel remaining tasks
for task in pending: for task in pending:
if not task.done(): if not task.done():

166
src/levels.py Normal file
View File

@@ -0,0 +1,166 @@
"""
Level system for DuckHunt Bot
Manages player levels and difficulty scaling
"""
import json
import os
import logging
from typing import Dict, Any, Optional, Tuple
class LevelManager:
"""Manages the DuckHunt level system and difficulty scaling"""
def __init__(self, levels_file: str = "levels.json"):
self.levels_file = levels_file
self.levels_data = {}
self.logger = logging.getLogger('DuckHuntBot.Levels')
self.load_levels()
def load_levels(self):
"""Load level definitions from JSON file"""
try:
if os.path.exists(self.levels_file):
with open(self.levels_file, 'r', encoding='utf-8') as f:
self.levels_data = json.load(f)
level_count = len(self.levels_data.get('levels', {}))
self.logger.info(f"Loaded {level_count} levels from {self.levels_file}")
else:
# Fallback levels if file doesn't exist
self.levels_data = self._get_default_levels()
self.logger.warning(f"{self.levels_file} not found, using default levels")
except Exception as e:
self.logger.error(f"Error loading levels: {e}, using defaults")
self.levels_data = self._get_default_levels()
def _get_default_levels(self) -> Dict[str, Any]:
"""Default fallback level system"""
return {
"level_calculation": {
"method": "total_ducks",
"description": "Level based on total ducks interacted with"
},
"levels": {
"1": {
"name": "Duck Novice",
"min_ducks": 0,
"max_ducks": 9,
"befriend_success_rate": 85,
"accuracy_modifier": 5,
"duck_spawn_speed_modifier": 1.0,
"description": "Just starting out"
},
"2": {
"name": "Duck Hunter",
"min_ducks": 10,
"max_ducks": 99,
"befriend_success_rate": 75,
"accuracy_modifier": 0,
"duck_spawn_speed_modifier": 0.8,
"description": "Getting experienced"
}
}
}
def calculate_player_level(self, player: Dict[str, Any]) -> int:
"""Calculate a player's current level based on their stats"""
method = self.levels_data.get('level_calculation', {}).get('method', 'total_ducks')
if method == 'total_ducks':
total_ducks = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
elif method == 'xp':
total_ducks = player.get('xp', 0) // 10 # 10 XP per "duck equivalent"
else:
total_ducks = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
# Find the appropriate level
levels = self.levels_data.get('levels', {})
for level_num in sorted(levels.keys(), key=int, reverse=True):
level_data = levels[level_num]
if total_ducks >= level_data.get('min_ducks', 0):
return int(level_num)
return 1 # Default to level 1
def get_level_data(self, level: int) -> Optional[Dict[str, Any]]:
"""Get level data for a specific level"""
return self.levels_data.get('levels', {}).get(str(level))
def get_player_level_info(self, player: Dict[str, Any]) -> Dict[str, Any]:
"""Get complete level information for a player"""
level = self.calculate_player_level(player)
level_data = self.get_level_data(level)
if not level_data:
return {
"level": 1,
"name": "Duck Novice",
"description": "Default level",
"befriend_success_rate": 75,
"accuracy_modifier": 0,
"duck_spawn_speed_modifier": 1.0
}
total_ducks = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
# Calculate progress to next level
next_level_data = self.get_level_data(level + 1)
if next_level_data:
ducks_needed = next_level_data.get('min_ducks', 0) - total_ducks
next_level_name = next_level_data.get('name', f"Level {level + 1}")
else:
ducks_needed = 0
next_level_name = "Max Level"
return {
"level": level,
"name": level_data.get('name', f"Level {level}"),
"description": level_data.get('description', ''),
"befriend_success_rate": level_data.get('befriend_success_rate', 75),
"accuracy_modifier": level_data.get('accuracy_modifier', 0),
"duck_spawn_speed_modifier": level_data.get('duck_spawn_speed_modifier', 1.0),
"total_ducks": total_ducks,
"ducks_needed_for_next": max(0, ducks_needed),
"next_level_name": next_level_name
}
def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
"""Get player's accuracy modified by their level"""
base_accuracy = player.get('accuracy', 65)
level_info = self.get_player_level_info(player)
modifier = level_info.get('accuracy_modifier', 0)
# Apply modifier and clamp between 10-100
modified_accuracy = base_accuracy + modifier
return max(10, min(100, modified_accuracy))
def get_modified_befriend_rate(self, player: Dict[str, Any], base_rate: float = 75.0) -> float:
"""Get player's befriend success rate modified by their level"""
level_info = self.get_player_level_info(player)
level_rate = level_info.get('befriend_success_rate', base_rate)
# Return as percentage (0-100)
return max(5.0, min(95.0, level_rate))
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:
return 1.0
# Use the modifier from the highest level player (makes it harder for everyone)
max_level = max(player_levels)
level_data = self.get_level_data(max_level)
if level_data:
return level_data.get('duck_spawn_speed_modifier', 1.0)
return 1.0
def reload_levels(self) -> int:
"""Reload levels from file and return count"""
old_count = len(self.levels_data.get('levels', {}))
self.load_levels()
new_count = len(self.levels_data.get('levels', {}))
self.logger.info(f"Levels reloaded: {old_count} -> {new_count} levels")
return new_count

149
src/shop.py Normal file
View File

@@ -0,0 +1,149 @@
"""
Shop system for DuckHunt Bot
Handles loading items, purchasing, and item effects
"""
import json
import os
import logging
from typing import Dict, Any, Optional
class ShopManager:
"""Manages the DuckHunt shop system"""
def __init__(self, shop_file: str = "shop.json"):
self.shop_file = shop_file
self.items = {}
self.logger = logging.getLogger('DuckHuntBot.Shop')
self.load_items()
def load_items(self):
"""Load shop items from JSON file"""
try:
if os.path.exists(self.shop_file):
with open(self.shop_file, 'r', encoding='utf-8') as f:
shop_data = json.load(f)
# Convert string keys to integers for easier handling
self.items = {int(k): v for k, v in shop_data.get('items', {}).items()}
self.logger.info(f"Loaded {len(self.items)} shop items from {self.shop_file}")
else:
# Fallback items if file doesn't exist
self.items = self._get_default_items()
self.logger.warning(f"{self.shop_file} not found, using default items")
except Exception as e:
self.logger.error(f"Error loading shop items: {e}, using defaults")
self.items = self._get_default_items()
def _get_default_items(self) -> Dict[int, Dict[str, Any]]:
"""Default fallback shop items"""
return {
1: {"name": "Single Bullet", "price": 5, "description": "1 extra bullet", "type": "ammo", "amount": 1},
2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy", "type": "accuracy", "amount": 10},
3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance", "type": "luck", "amount": 5}
}
def get_items(self) -> Dict[int, Dict[str, Any]]:
"""Get all shop items"""
return self.items.copy()
def get_item(self, item_id: int) -> Optional[Dict[str, Any]]:
"""Get a specific shop item by ID"""
return self.items.get(item_id)
def is_valid_item(self, item_id: int) -> bool:
"""Check if item ID exists"""
return item_id in self.items
def can_afford(self, player_xp: int, item_id: int) -> bool:
"""Check if player can afford an item"""
item = self.get_item(item_id)
if not item:
return False
return player_xp >= item['price']
def purchase_item(self, player: Dict[str, Any], item_id: int) -> Dict[str, Any]:
"""
Purchase an item and apply its effects to the player
Returns a result dictionary with success status and details
"""
item = self.get_item(item_id)
if not item:
return {"success": False, "error": "invalid_id", "message": "Invalid item ID"}
player_xp = player.get('xp', 0)
if player_xp < item['price']:
return {
"success": False,
"error": "insufficient_xp",
"message": f"Need {item['price']} XP, have {player_xp} XP",
"item_name": item['name'],
"price": item['price'],
"current_xp": player_xp
}
# Deduct XP
player['xp'] = player_xp - item['price']
# Apply item effect
effect_result = self._apply_item_effect(player, item)
return {
"success": True,
"item_name": item['name'],
"price": item['price'],
"remaining_xp": player['xp'],
"effect": effect_result
}
def _apply_item_effect(self, player: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]:
"""Apply the effect of an item to a player"""
item_type = item.get('type', 'unknown')
amount = item.get('amount', 0)
if item_type == 'ammo':
# Add ammo up to max capacity
current_ammo = player.get('ammo', 0)
max_ammo = player.get('max_ammo', 6)
new_ammo = min(current_ammo + amount, max_ammo)
player['ammo'] = new_ammo
return {
"type": "ammo",
"added": new_ammo - current_ammo,
"new_total": new_ammo,
"max": max_ammo
}
elif item_type == 'accuracy':
# Increase accuracy up to 100%
current_accuracy = player.get('accuracy', 65)
new_accuracy = min(current_accuracy + amount, 100)
player['accuracy'] = new_accuracy
return {
"type": "accuracy",
"added": new_accuracy - current_accuracy,
"new_total": new_accuracy
}
elif item_type == 'luck':
# Store luck bonus (would be used in duck spawning logic)
current_luck = player.get('luck_bonus', 0)
new_luck = current_luck + amount
player['luck_bonus'] = new_luck
return {
"type": "luck",
"added": amount,
"new_total": new_luck
}
else:
self.logger.warning(f"Unknown item type: {item_type}")
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}
def reload_items(self) -> int:
"""Reload items from file and return count"""
old_count = len(self.items)
self.load_items()
new_count = len(self.items)
self.logger.info(f"Shop reloaded: {old_count} -> {new_count} items")
return new_count

View File

@@ -5,6 +5,7 @@ Utility functions for DuckHunt Bot
import re import re
import json import json
import os import os
import random
from typing import Optional, Tuple, List, Dict, Any from typing import Optional, Tuple, List, Dict, Any
@@ -29,10 +30,17 @@ class MessageManager:
print(f"Error loading messages: {e}, using defaults") print(f"Error loading messages: {e}, using defaults")
self.messages = self._get_default_messages() self.messages = self._get_default_messages()
def _get_default_messages(self) -> Dict[str, str]: def _get_default_messages(self) -> Dict[str, Any]:
"""Default fallback messages without colors""" """Default fallback messages without colors"""
return { return {
"duck_spawn": "・゜゜・。。・゜゜\\_o< QUACK! A duck has appeared! Type !bang to shoot it!", "duck_spawn": [
"・゜゜・。。・゜゜\\_o< QUACK! A duck has appeared! Type !bang to shoot it!",
"・゜゜・。。・゜゜\\_o< *flap flap* A wild duck landed! Use !bang to hunt it!",
"🦆 A duck swoops into view! Quick, type !bang before it escapes!",
"・゜゜・。。・゜゜\\_o< Quack quack! Fresh duck spotted! !bang to bag it!",
"*rustling* A duck waddles out from the bushes! Fire with !bang!",
"・゜゜・。。・゜゜\\_o< Splash! A duck surfaces! Shoot it with !bang!"
],
"duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`",
"bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
"bang_miss": "{nick} > *BANG* You missed the duck!", "bang_miss": "{nick} > *BANG* You missed the duck!",
@@ -63,12 +71,28 @@ class MessageManager:
} }
def get(self, key: str, **kwargs) -> str: def get(self, key: str, **kwargs) -> str:
"""Get a formatted message by key""" """Get a formatted message by key with color placeholder replacement"""
if key not in self.messages: if key not in self.messages:
return f"[Missing message: {key}]" return f"[Missing message: {key}]"
message = self.messages[key] message = self.messages[key]
# If message is an array, randomly select one
if isinstance(message, list):
if not message:
return f"[Empty message array: {key}]"
message = random.choice(message)
# Ensure message is a string
if not isinstance(message, str):
return f"[Invalid message type: {key}]"
# Replace color placeholders with IRC codes
if "colours" in self.messages and isinstance(self.messages["colours"], dict):
for color_name, color_code in self.messages["colours"].items():
placeholder = "{" + color_name + "}"
message = message.replace(placeholder, color_code)
# Format with provided variables # Format with provided variables
try: try:
return message.format(**kwargs) return message.format(**kwargs)