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:
@@ -14,5 +14,6 @@
|
||||
|
||||
"duck_spawn_min": 10,
|
||||
"duck_spawn_max": 30,
|
||||
"duck_timeout": 60
|
||||
"duck_timeout": 60,
|
||||
"befriend_success_rate": 75
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
129
duckhunt.log
129
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
|
||||
|
||||
80
levels.json
Normal file
80
levels.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <player>\u000F | \u000302!disarm <player>\u000F | \u000302!ignore <player>\u000F | \u000302!unignore <player>\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 <player>\u000F",
|
||||
"usage_disarm": "Usage: \u000302!disarm <player>\u000F",
|
||||
"usage_ignore": "Usage: \u000302!ignore <player>\u000F",
|
||||
"usage_unignore": "Usage: \u000302!unignore <player>\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 <player>{reset} | {blue}!disarm <player>{reset} | {blue}!ignore <player>{reset} | {blue}!unignore <player>{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 <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": {
|
||||
"white": "\u00030",
|
||||
|
||||
25
shop.json
Normal file
25
shop.json
Normal 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.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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 <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 = []
|
||||
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():
|
||||
|
||||
166
src/levels.py
Normal file
166
src/levels.py
Normal 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
149
src/shop.py
Normal 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
|
||||
30
src/utils.py
30
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)
|
||||
|
||||
Reference in New Issue
Block a user