From 3aaf0d0bb44e14bbcfc870ea9fda6df5f7b98f6e Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Tue, 23 Sep 2025 18:05:28 +0100 Subject: [PATCH] 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 --- config.json | 3 +- duckhunt.json | 15 +- duckhunt.log | 129 +++++++++++++++ levels.json | 80 ++++++++++ messages.json | 69 ++++---- shop.json | 25 +++ src/__pycache__/db.cpython-312.pyc | Bin 4444 -> 4571 bytes src/__pycache__/duckhuntbot.cpython-312.pyc | Bin 26762 -> 31941 bytes src/__pycache__/game.cpython-312.pyc | Bin 5341 -> 5475 bytes src/__pycache__/utils.cpython-312.pyc | Bin 3281 -> 9170 bytes src/db.py | 6 + src/duckhuntbot.py | 121 +++++++++++--- src/levels.py | 166 ++++++++++++++++++++ src/shop.py | 149 ++++++++++++++++++ src/utils.py | 30 +++- 15 files changed, 733 insertions(+), 60 deletions(-) create mode 100644 levels.json create mode 100644 shop.json create mode 100644 src/levels.py create mode 100644 src/shop.py 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 c76a996ee8e20ee13e17314fa94a905c88d76b55..0a3ab5e9da0cb7cd50a169612ae19ffa04aa7e3c 100644 GIT binary patch delta 436 zcmXv~%`XE%6yMoxS2DG;>!aHbEiDb&)JG5}2TDkYyMw5YS*eedOiPdoM@RAwA{-nX zBu=gl&ca2VCgqPP&Ju4m=FR-x%zMB0@|*Y3{@%e}41+4T?w;RrpY)!c81xyR=SqpJ z6F932!-EkeZ6J*iH6MgW!?sIm2pNYOCnar{2rpMlcn6Dh5DkV{I#99r&dNq1yX>rr zH7Cyx_5Mb=6@aKr+vqHk|5?K`62!FUWVS(2VtA=A-Gn=b dq%J0*YDGx~SVNFZL03bkIYS8jQe6Ba{sOD`aF751 delta 378 zcmcbud`F4zG%qg~0}wF(yqNKab0c2|FV_qpcRE82V;1A&iR@C7ck#+H%1yq(YcqKt zuh`@mK2Ao)$!UB#lUMS|$ubx502SY2$;(X6zQqM1<8$)MQ;Uk3fx?sD^64--PL|>~ z^1R5S@_=8szoxThhSx=Y^$Q&8Mf^Y&$xJ{320+dLY5Ob%BtZ73fFKhPG6Nw?3Tq16 z9L~wR_#MPJi;RFuZZTC9+~P_pP0lWkFV4s>nJg;c8x4{u2AQg$P{a$QiVT57m52*a zm5Y;Ja#3nxNossSPGV(hkt;~4CXi@g_`t>>DtSXl_=c494H3~B5|SV!BYQ(a>V~|^ zM`11jt`7`clMe_)aGSu)EOMBvENCXi&Z_ZQh=-L6?42SOpsXfak;~+KL0LwJ$-ROS VjINWH399kCGBR>~VgQj~g#bghU2OmW diff --git a/src/__pycache__/duckhuntbot.cpython-312.pyc b/src/__pycache__/duckhuntbot.cpython-312.pyc index b591786a12ae139c1b9ee836400a1837ae68888a..a69bc08d20d5409abf685fdf0e2616a51ec091f5 100644 GIT binary patch delta 13306 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 diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 863a05f54cc1099412b5524c8a15128952b152b2..00635f81932b1cf2c7b3ab47303573d5ae082d8d 100644 GIT binary patch delta 808 zcmY+BO-vI(6vt<}gTrO%Y5? z)TBu@69XI+^q@_=;78)Yn@UX77&buKG@59<7*7?AF+>w*R?+wlzxnUHeeccu=dJ%k zJ@=C1EC7f8Lm$c9<1Sj@Cv?q{h4)csc?R~Q*X6sQh|UWVbfC{R32{uBX_7|ERK>Qq zlNA0RDJn(ZHT~X$AU$9PDYFLCw@?e~g%8mocFduw;0jFFc)`Tq(zuLX&@Q;enBz9) zxVR%ifge6SG&Xqr#Aq}z7)eIXpe$DndGwr9t<1@2A`yv269p<3oj^ZvfD4Jd9wzUni=)H9yLOFKBS#w@?&$DyvLjS5pW2Xr}*jV&hE(K$YBX#kQlZ>!t*8bXixU~d>+sO;$i*I`eG`9^mwmQPB$8}ahAiF`bB z@q`T`VOL{sh*}DmaM4l_5*?zj9x|b6i=ExTw>k(Qm;ot>x`m1dQCkqAW?C}Eg+CtR zx1!Lb;6$$kNld{M_3v#`AZ|DOb{hY_6An)mEQa5SQ<3EOSz8bG-ec(KuxE7K<7YME zXn`6XpU}2rrPUC42*L!V1kD631S&x}K_!OYtXYY!Cuk?=z(|-eJUBES0Q60CmG+Ze z9XX|RKyuFbuKIGKR~NlGu}&B3vh0f3e2LC8Y=*ha+qgm5W?t(<8M_Skp{%`@KMJ9`4hW3~zX0?j B(M$jU delta 790 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 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)