diff --git a/config.json b/config.json index d5620de..ad0d4ea 100644 --- a/config.json +++ b/config.json @@ -14,5 +14,6 @@ "duck_spawn_min": 10, "duck_spawn_max": 30, - "duck_timeout": 60 + "duck_timeout": 60, + "befriend_success_rate": 75 } \ No newline at end of file diff --git a/duckhunt.json b/duckhunt.json index 3f9b65e..04a3cd2 100644 --- a/duckhunt.json +++ b/duckhunt.json @@ -2,15 +2,16 @@ "players": { "computertech": { "nick": "ComputerTech", - "xp": 10, - "ducks_shot": 1, - "ammo": 5, + "xp": 45, + "ducks_shot": 4, + "ammo": 2, "max_ammo": 6, - "chargers": 2, + "chargers": 1, "max_chargers": 2, - "accuracy": 64, - "gun_confiscated": false + "accuracy": 65, + "gun_confiscated": false, + "ducks_befriended": 1 } }, - "last_save": "1758592642.337953" + "last_save": "1758646365.5768785" } \ No newline at end of file diff --git a/duckhunt.log b/duckhunt.log index 06b41fe..d2b81f6 100644 --- a/duckhunt.log +++ b/duckhunt.log @@ -342,3 +342,132 @@ 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,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 diff --git a/levels.json b/levels.json new file mode 100644 index 0000000..39a06ad --- /dev/null +++ b/levels.json @@ -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" + } + } +} \ No newline at end of file diff --git a/messages.json b/messages.json index fdf5ae6..ae10cfa 100644 --- a/messages.json +++ b/messages.json @@ -1,32 +1,43 @@ -{{ - "duck_spawn": "γƒ»γ‚œγ‚œγƒ»γ€‚γ€‚γƒ»γ‚œγ‚œ\\_o< \u000303QUACK!\u000F A \u000308duck\u000F has appeared! Type \u000302!bang\u000F to shoot it!", - "duck_flies_away": "The \u000308duck\u000F flies away. Β·Β°'`'Β°-.,ΒΈΒΈ.Β·Β°'`", - "bang_hit": "{nick} > \u000304*BANG*\u000F You shot the \u000308duck\u000F! [\u000303+{xp_gained} xp\u000F] [Total ducks: \u000302{ducks_shot}\u000F]", - "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]", - "bang_no_ammo": "{nick} > \u000307*click*\u000F You're out of ammo! Use \u000302!reload\u000F", - "bang_not_armed": "{nick} > You are \u000304not armed\u000F.", - "reload_success": "{nick} > \u000307*click*\u000F Reloaded! [Ammo: \u000303{ammo}\u000F/\u000303{max_ammo}\u000F] [Chargers: \u000302{chargers}\u000F]", - "reload_already_loaded": "{nick} > Your gun is \u000303already loaded\u000F!", - "reload_no_chargers": "{nick} > You're out of \u000304chargers\u000F!", - "reload_not_armed": "{nick} > You are \u000304not armed\u000F.", - "shop_display": "DuckHunt Shop: {items} | You have \u000303{xp} XP\u000F", - "shop_item_format": "(\u000302{id}\u000F) \u000310{name}\u000F - \u000303{price} XP\u000F", - "help_header": "\u000302DuckHunt Commands:\u000F", - "help_user_commands": "\u000302!bang\u000F - Shoot at ducks | \u000302!reload\u000F - Reload your gun | \u000302!shop\u000F - View the shop", - "help_help_command": "\u000302!duckhelp\u000F - Show this help", - "help_admin_commands": "\u000304Admin:\u000F \u000302!rearm \u000F | \u000302!disarm \u000F | \u000302!ignore \u000F | \u000302!unignore \u000F | \u000302!ducklaunch\u000F", - "admin_rearm_player": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been rearmed by \u000302{admin}\u000F", - "admin_rearm_all": "[\u000304ADMIN\u000F] All players have been rearmed by \u000302{admin}\u000F", - "admin_disarm": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been disarmed by \u000302{admin}\u000F", - "admin_ignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is now ignored by \u000302{admin}\u000F", - "admin_unignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is no longer ignored by \u000302{admin}\u000F", - "admin_ducklaunch": "[\u000304ADMIN\u000F] A \u000308duck\u000F has been launched by \u000302{admin}\u000F", - "admin_ducklaunch_not_enabled": "[\u000304ADMIN\u000F] This channel is \u000304not enabled\u000F for duckhunt", - "usage_rearm": "Usage: \u000302!rearm \u000F", - "usage_disarm": "Usage: \u000302!disarm \u000F", - "usage_ignore": "Usage: \u000302!ignore \u000F", - "usage_unignore": "Usage: \u000302!unignore \u000F", +{ + "duck_spawn": [ + "γƒ»γ‚œγ‚œγƒ»γ€‚γ€‚γƒ»γ‚œγ‚œ\\_O< {bold}QUACK!{reset}", + "γƒ»γ‚œγ‚œγƒ»γ€‚γ€‚γƒ»γ‚œγ‚œ\\_O> {yellow}*flap flap*" + ], + "duck_flies_away": "The {cyan}duck{reset} flies away. Β·Β°'`'Β°-.,ΒΈΒΈ.Β·Β°'`", + "bang_hit": "{nick} > {red}*BANG*{reset} You shot the {cyan}duck{reset}! [{green}+{xp_gained} xp{reset}] [Total ducks: {blue}{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_not_armed": "{nick} > You are {red}not armed{reset}.", + "bef_success": "{nick} > {green}*befriend*{reset} You befriended the {cyan}duck{reset}! [{green}+{xp_gained} xp{reset}] [Ducks befriended: {pink}{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} Reloaded! [Ammo: {green}{ammo}{reset}/{green}{max_ammo}{reset}] [Chargers: {blue}{chargers}{reset}]", + "reload_already_loaded": "{nick} > Your gun is {green}already loaded{reset}!", + "reload_no_chargers": "{nick} > You're out of {red}chargers{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 the shop", + "help_help_command": "{blue}!duckhelp{reset} - Show this help", + "help_admin_commands": "{red}Admin:{reset} {blue}!rearm {reset} | {blue}!disarm {reset} | {blue}!ignore {reset} | {blue}!unignore {reset} | {blue}!ducklaunch{reset} | {blue}!reloadshop{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_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 {reset}", + "usage_disarm": "Usage: {blue}!disarm {reset}", + "usage_ignore": "Usage: {blue}!ignore {reset}", + "usage_unignore": "Usage: {blue}!unignore {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 {reset}", "colours": { "white": "\u00030", diff --git a/shop.json b/shop.json new file mode 100644 index 0000000..c462d74 --- /dev/null +++ b/shop.json @@ -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 + } + } +} \ No newline at end of file diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc index c76a996..0a3ab5e 100644 Binary files a/src/__pycache__/db.cpython-312.pyc and b/src/__pycache__/db.cpython-312.pyc differ diff --git a/src/__pycache__/duckhuntbot.cpython-312.pyc b/src/__pycache__/duckhuntbot.cpython-312.pyc index b591786..a69bc08 100644 Binary files a/src/__pycache__/duckhuntbot.cpython-312.pyc and b/src/__pycache__/duckhuntbot.cpython-312.pyc differ diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 863a05f..00635f8 100644 Binary files a/src/__pycache__/game.cpython-312.pyc and b/src/__pycache__/game.cpython-312.pyc differ diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index 2d841cf..73f5132 100644 Binary files a/src/__pycache__/utils.cpython-312.pyc and b/src/__pycache__/utils.cpython-312.pyc differ diff --git a/src/db.py b/src/db.py index dbf2b72..50ba69b 100644 --- a/src/db.py +++ b/src/db.py @@ -63,6 +63,11 @@ class DuckDB: if nick_lower not in self.players: 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] @@ -72,6 +77,7 @@ class DuckDB: 'nick': nick, 'xp': 0, 'ducks_shot': 0, + 'ducks_befriended': 0, 'ammo': 6, 'max_ammo': 6, 'chargers': 2, diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index edfb569..e38f463 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -1,8 +1,3 @@ -""" -Simplified DuckHunt IRC Bot - Core Features Only -Commands: !bang, !reload, !shop, !duckhelp, !rearm, !disarm, !ignore, !unignore, !ducklaunch -""" - import asyncio import ssl import json @@ -19,6 +14,7 @@ from .utils import parse_irc_message, InputValidator, MessageManager from .db import DuckDB from .game import DuckGame from .sasl import SASLHandler +from .shop import ShopManager class DuckHuntBot: @@ -41,12 +37,9 @@ class DuckHuntBot: self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] - # Simple shop items - hardcoded - self.shop_items = { - 1: {"name": "Extra Shots", "price": 10, "description": "5 extra shots"}, - 2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy"}, - 3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance"} - } + # Initialize shop manager + shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json') + self.shop = ShopManager(shop_file) def get_config(self, path, default=None): """Get configuration value using dot notation""" @@ -148,10 +141,12 @@ class DuckHuntBot: if cmd == "bang": await self.handle_bang(nick, channel, player) + elif cmd == "bef" or cmd == "befriend": + await self.handle_bef(nick, channel, player) elif cmd == "reload": await self.handle_reload(nick, channel, player) elif cmd == "shop": - await self.handle_shop(nick, channel, player) + await self.handle_shop(nick, channel, player, args) elif cmd == "duckhelp": await self.handle_duckhelp(nick, channel, player) elif cmd == "rearm" and self.is_admin(user): @@ -164,6 +159,8 @@ class DuckHuntBot: await self.handle_unignore(nick, channel, args) elif cmd == "ducklaunch" and self.is_admin(user): 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): """Handle !bang command""" @@ -215,6 +212,44 @@ class DuckHuntBot: 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): """Handle !reload command""" if player.get('gun_confiscated', False): @@ -243,10 +278,22 @@ class DuckHuntBot: self.send_message(channel, message) 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 buying: !shop buy + 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 = [] - 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', id=item_id, name=item['name'], @@ -259,6 +306,36 @@ class DuckHuntBot: 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): """Handle !duckhelp command""" help_lines = [ @@ -357,6 +434,15 @@ class DuckHuntBot: self.send_message(channel, admin_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): """Main message processing loop""" @@ -397,20 +483,15 @@ class DuckHuntBot: # Start game loops game_task = asyncio.create_task(self.game.start_game_loops()) 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.") # Wait for shutdown signal or task completion done, pending = await asyncio.wait( - [game_task, message_task, shutdown_task], + [game_task, message_task], 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 for task in pending: if not task.done(): diff --git a/src/levels.py b/src/levels.py new file mode 100644 index 0000000..b95b2c9 --- /dev/null +++ b/src/levels.py @@ -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 \ No newline at end of file diff --git a/src/shop.py b/src/shop.py new file mode 100644 index 0000000..fd014bf --- /dev/null +++ b/src/shop.py @@ -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 \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 7050445..d307ff9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -5,6 +5,7 @@ Utility functions for DuckHunt Bot import re import json import os +import random from typing import Optional, Tuple, List, Dict, Any @@ -29,10 +30,17 @@ class MessageManager: print(f"Error loading messages: {e}, using defaults") 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""" 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. Β·Β°'`'Β°-.,ΒΈΒΈ.Β·Β°'`", "bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_miss": "{nick} > *BANG* You missed the duck!", @@ -63,12 +71,28 @@ class MessageManager: } 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: return f"[Missing message: {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 try: return message.format(**kwargs)