From f3a9c5b611a2e768b1c48824034c206d4b48caec Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Fri, 26 Sep 2025 19:06:26 +0100 Subject: [PATCH] Security fixes, UI improvements, and game balance updates - Fixed critical security vulnerabilities in shop targeting system - Fixed admin authentication bypass issues - Fixed auto-rearm feature config path (duck_spawning.rearm_on_duck_shot) - Updated duck spawn timing to 20-60 minutes for better game balance - Enhanced inventory display formatting with proper spacing - Added comprehensive admin security documentation --- ADMIN_SECURITY.md | 46 ++ CONFIG.md | 127 ------ config.json | 11 +- duckhunt.json | 18 +- duckhunt.log | 75 ++++ logs/duckhunt.log | 412 ++++++++++++++++++ messages.json | 17 +- shop.json | 29 +- src/__pycache__/db.cpython-312.pyc | Bin 12404 -> 15234 bytes src/__pycache__/duckhuntbot.cpython-312.pyc | Bin 44739 -> 61544 bytes src/__pycache__/game.cpython-312.pyc | Bin 18387 -> 22970 bytes src/__pycache__/levels.cpython-312.pyc | Bin 10493 -> 10514 bytes src/__pycache__/logging_utils.cpython-312.pyc | Bin 3279 -> 14028 bytes src/__pycache__/sasl.cpython-312.pyc | Bin 10990 -> 11011 bytes src/__pycache__/shop.cpython-312.pyc | Bin 14952 -> 19829 bytes src/__pycache__/utils.cpython-312.pyc | Bin 10455 -> 10455 bytes src/db.py | 10 + src/duckhuntbot.py | 326 ++++++++++++-- src/game.py | 114 ++++- src/shop.py | 177 +++++++- test_config.py | 56 --- 21 files changed, 1162 insertions(+), 256 deletions(-) create mode 100644 ADMIN_SECURITY.md delete mode 100644 CONFIG.md create mode 100644 logs/duckhunt.log delete mode 100644 test_config.py diff --git a/ADMIN_SECURITY.md b/ADMIN_SECURITY.md new file mode 100644 index 0000000..5da2462 --- /dev/null +++ b/ADMIN_SECURITY.md @@ -0,0 +1,46 @@ +# Enhanced Admin Configuration + +For better security, update your `config.json` to use hostmask-based admin authentication: + +## Current (Less Secure) - Nick Only: +```json +{ + "admins": [ + "peorth", + "computertech", + "colby" + ] +} +``` + +## Recommended (More Secure) - Hostmask Based: +```json +{ + "admins": [ + { + "nick": "peorth", + "hostmask": "peorth!*@trusted.domain.com" + }, + { + "nick": "computertech", + "hostmask": "computertech!*@*.isp.net" + }, + { + "nick": "colby", + "hostmask": "colby!user@192.168.*.*" + } + ] +} +``` + +## Migration Notes: +- The bot supports both formats for backward compatibility +- Nick-only authentication generates security warnings in logs +- Hostmask patterns use shell-style wildcards (* and ?) +- Consider using registered nick services for additional security + +## Security Benefits: +- Prevents nick spoofing attacks +- Allows IP/hostname restrictions +- Provides audit logging of admin access +- Maintains backward compatibility during migration \ No newline at end of file diff --git a/CONFIG.md b/CONFIG.md deleted file mode 100644 index a57f682..0000000 --- a/CONFIG.md +++ /dev/null @@ -1,127 +0,0 @@ -# DuckHunt Bot Configuration Guide - -This document explains all configuration options in `config.json`. - -## ๐Ÿ“ก IRC Connection Settings (`connection`) - -| Setting | Description | Example | -|---------|-------------|---------| -| `server` | IRC server hostname | `"irc.rizon.net"` | -| `port` | IRC server port | `6697` (SSL) or `6667` (non-SSL) | -| `nick` | Bot's nickname on IRC | `"DuckHunt"` | -| `channels` | List of channels to auto-join | `["#channel1", "#channel2"]` | -| `ssl` | Use SSL/TLS encryption | `true` (recommended) | -| `password` | Server password (I-Line auth) | Change if server requires auth | -| `max_retries` | Connection retry attempts | `3` | -| `retry_delay` | Seconds between retries | `5` | -| `timeout` | Connection timeout in seconds | `30` | - -## ๐Ÿ” SASL Authentication (`sasl`) - -SASL is used for NickServ authentication on connect. - -| Setting | Description | Example | -|---------|-------------|---------| -| `enabled` | Enable SASL authentication | `false` | -| `username` | Registered nickname | `"your_registered_nick"` | -| `password` | NickServ password | `"your_nickserv_password"` | - -**Note:** Change `password` from the default if enabling SASL! - -## ๐Ÿ‘‘ Bot Administration (`admins`) - -Array of IRC nicknames with admin privileges. - -```json -"admins": ["peorth", "computertech", "colby"] -``` - -## ๐Ÿฆ† Duck Spawning (`duck_spawning`) - -| Setting | Description | Default | -|---------|-------------|---------| -| `spawn_min` | Minimum seconds between spawns | `10` | -| `spawn_max` | Maximum seconds between spawns | `30` | -| `timeout` | Global fallback timeout | `60` | -| `rearm_on_duck_shot` | Auto-rearm guns when duck shot | `true` | - -## ๐ŸŽฏ Duck Types (`duck_types`) - -### Normal Ducks (`normal`) -- `xp`: XP reward (default: `10`) -- `timeout`: Seconds before flying away (default: `60`) - -### Golden Ducks (`golden`) -- `chance`: Spawn probability (default: `0.15` = 15%) -- `min_hp`: Minimum hit points (default: `3`) -- `max_hp`: Maximum hit points (default: `5`) -- `xp`: XP reward (default: `15`) -- `timeout`: Seconds before flying away (default: `60`) - -### Fast Ducks (`fast`) -- `chance`: Spawn probability (default: `0.25` = 25%) -- `timeout`: Seconds before flying away (default: `20`) -- `xp`: XP reward (default: `12`) - -**Note:** Chances are decimal percentages (0.15 = 15%, 0.25 = 25%) - -## ๐Ÿ‘ค New Player Defaults (`player_defaults`) - -Starting values for new players: - -| Setting | Description | Default | -|---------|-------------|---------| -| `accuracy` | Starting accuracy percentage (0-100) | `75` | -| `magazines` | Starting number of magazines | `3` | -| `bullets_per_magazine` | Bullets per magazine | `6` | -| `jam_chance` | Gun jam percentage (0-100) | `15` | -| `xp` | Starting XP (also currency) | `0` | - -## ๐ŸŽฎ Game Mechanics (`gameplay`) - -| Setting | Description | Default | -|---------|-------------|---------| -| `befriend_success_rate` | Base befriend chance (%) | `75` | -| `befriend_xp` | XP from befriending | `5` | -| `accuracy_gain_on_hit` | Accuracy boost per hit | `1` | -| `accuracy_loss_on_miss` | Accuracy loss per miss | `2` | -| `min_accuracy` | Minimum accuracy limit | `10` | -| `max_accuracy` | Maximum accuracy limit | `100` | -| `min_befriend_success_rate` | Min befriend rate | `5` | -| `max_befriend_success_rate` | Max befriend rate | `95` | - -## ๐Ÿ”ง Feature Toggles (`features`) - -| Setting | Description | Default | -|---------|-------------|---------| -| `shop_enabled` | Enable shop system | `true` | -| `inventory_enabled` | Enable inventory system | `true` | -| `auto_rearm_enabled` | Enable auto gun rearming | `true` | - -## โš–๏ธ System Limits (`limits`) - -| Setting | Description | Default | -|---------|-------------|---------| -| `max_inventory_items` | Max items per player | `20` | -| `max_temp_effects` | Max temporary effects | `20` | - ---- - -## ๐Ÿ”ง Configuration Access - -The bot uses dot notation to access nested settings: - -```python -# Examples: -server = bot.get_config('connection.server') -normal_xp = bot.get_config('duck_types.normal.xp') -player_accuracy = bot.get_config('player_defaults.accuracy') -``` - -## ๐Ÿ“ Tips - -1. **Percentages:** Most percentage values use 0-100 scale, but spawn chances use 0.0-1.0 decimals -2. **Authentication:** Set real passwords when using server auth or SASL -3. **Balance:** Adjust XP rewards and duck spawn rates to balance gameplay -4. **Testing:** Change one setting at a time to test effects -5. **Backup:** Keep a backup of working config before major changes \ No newline at end of file diff --git a/config.json b/config.json index 98c07e4..947d82a 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "connection": { "server": "irc.rizon.net", "port": 6697, - "nick": "DuckHunt", + "nick": "DickHunt", "channels": [ "#ct" ], @@ -13,7 +13,7 @@ "timeout": 30 }, "sasl": { - "enabled": true, + "enabled": false, "username": "duckhunt", "password": "duckhunt//789//" }, @@ -24,8 +24,8 @@ "colby" ], "duck_spawning": { - "spawn_min": 10, - "spawn_max": 30, + "spawn_min": 1200, + "spawn_max": 3600, "timeout": 60, "rearm_on_duck_shot": true }, @@ -62,7 +62,8 @@ "min_accuracy": 10, "max_accuracy": 100, "min_befriend_success_rate": 5, - "max_befriend_success_rate": 95 + "max_befriend_success_rate": 95, + "wet_clothes_duration": 300 }, "features": { "shop_enabled": true, diff --git a/duckhunt.json b/duckhunt.json index e6f9d5b..3e041b9 100644 --- a/duckhunt.json +++ b/duckhunt.json @@ -1,19 +1,23 @@ { "players": { "computertech": { - "nick": "ComputerTech", - "xp": 60, - "ducks_shot": 5, + "nick": "computertech", + "xp": 67, + "ducks_shot": 6, "ducks_befriended": 2, - "accuracy": 62, + "shots_fired": 2, + "shots_missed": 1, + "accuracy": 63, "gun_confiscated": false, - "current_ammo": 5, + "current_ammo": 6, "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 5, - "inventory": {}, + "inventory": { + "1": 1 + }, "temporary_effects": [] } }, - "last_save": "1758825127.4272072" + "last_save": "1758909688.9995604" } \ No newline at end of file diff --git a/duckhunt.log b/duckhunt.log index f06dd90..2a1123a 100644 --- a/duckhunt.log +++ b/duckhunt.log @@ -651,3 +651,78 @@ 2025-09-24 18:56:53,434 [INFO ] DuckHuntBot.Shop - load_items:30: Loaded 5 shop items from /home/colby/duckhunt/shop.json 2025-09-24 18:56:53,436 [INFO ] DuckHuntBot.Levels - load_levels:28: Loaded 8 levels from /home/colby/duckhunt/levels.json 2025-09-24 18:56:53,439 [INFO ] DuckHuntBot.DB - load_database:47: Loaded 1 players from duckhunt.json +2025-09-25 18:05:24,467 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-25 18:05:24,469 [INFO ] DuckHuntBot.DB - load_database:48: Loaded 1 players from duckhunt.json +2025-09-25 18:05:24,470 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-25 18:05:24,471 [INFO ] DuckHuntBot.Shop - load_items:30: Loaded 5 shop items from /home/colby/duckhunt/shop.json +2025-09-25 18:05:24,478 [INFO ] DuckHuntBot.Levels - load_levels:28: Loaded 8 levels from /home/colby/duckhunt/levels.json +2025-09-25 18:05:24,479 [INFO ] DuckHuntBot - main:28: ๐Ÿฆ† Starting DuckHunt Bot... +2025-09-25 18:05:24,568 [INFO ] DuckHuntBot - connect:115: Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +2025-09-25 18:05:25,012 [INFO ] DuckHuntBot - connect:126: โœ… Successfully connected to irc.rizon.net:6697 +2025-09-25 18:05:25,013 [INFO ] SASL - start_negotiation:45: SASL authentication enabled +2025-09-25 18:05:25,013 [INFO ] DuckHuntBot - run:774: ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +2025-09-25 18:05:27,048 [INFO ] SASL - handle_cap_response:59: Server capabilities: ['away-notify', 'chghost', 'invite-notify', 'multi-prefix', 'sasl', 'userhost-in-names'] +2025-09-25 18:05:27,048 [INFO ] SASL - handle_cap_response:61: SASL capability available +2025-09-25 18:05:27,098 [INFO ] SASL - handle_cap_response:72: SASL capability acknowledged +2025-09-25 18:05:27,098 [INFO ] SASL - handle_cap_response:74: Authenticating via SASL as duckhunt +2025-09-25 18:05:27,099 [INFO ] SASL - handle_sasl:94: Sending AUTHENTICATE PLAIN +2025-09-25 18:05:27,507 [INFO ] SASL - handle_authenticate_response:103: Server ready for SASL authentication +2025-09-25 18:05:27,508 [DEBUG ] SASL - handle_authenticate_response:106: Auth string length: 33 chars +2025-09-25 18:05:27,508 [DEBUG ] SASL - handle_authenticate_response:107: Auth components: user='duckhunt', pass='duc...' +2025-09-25 18:05:27,508 [DEBUG ] SASL - handle_authenticate_response:110: Base64 encoded length: 44 chars +2025-09-25 18:05:27,508 [DEBUG ] SASL - handle_authenticate_response:111: Sending: AUTHENTICATE ZHVja2h1bnQAZHVja2h1... +2025-09-25 18:05:27,918 [INFO ] SASL - handle_sasl_result:123: SASL authentication successful! +2025-09-25 18:05:28,019 [INFO ] DuckHuntBot - handle_message:232: Successfully registered with IRC server +2025-09-25 18:05:47,029 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:05:51,297 [DEBUG ] DuckHuntBot.DB - save_database:134: Database saved successfully with 1 players +2025-09-25 18:06:06,048 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:06:13,814 [DEBUG ] DuckHuntBot.DB - save_database:134: Database saved successfully with 1 players +2025-09-25 18:06:24,064 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:07:26,130 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:07:56,160 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP +2025-09-25 18:09:17,234 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:10:24,298 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:11:42,412 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 4 HP +2025-09-25 18:12:55,485 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:14:06,578 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP +2025-09-25 18:15:14,722 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:15:54,792 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:16:15,812 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:17:22,878 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:18:31,953 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:19:49,029 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:20:57,099 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:22:04,175 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:23:19,252 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:23:46,278 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:24:57,354 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP +2025-09-25 18:26:17,435 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:27:24,505 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:28:33,580 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:29:37,647 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:30:17,686 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:31:35,762 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:33:01,847 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:34:13,914 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:35:27,982 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:36:39,046 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:37:41,102 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:38:18,132 [INFO ] DuckHuntBot.Game - spawn_duck:149: Fast duck (hidden) spawned in #ct +2025-09-25 18:38:44,166 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:40:12,271 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:41:23,349 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP +2025-09-25 18:42:28,415 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:43:52,494 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 4 HP +2025-09-25 18:45:03,560 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP +2025-09-25 18:46:04,620 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:47:20,702 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:48:29,761 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:49:35,827 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:50:48,894 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:51:51,944 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:52:58,006 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:53:59,056 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 4 HP +2025-09-25 18:55:08,121 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:56:27,201 [INFO ] DuckHuntBot.Game - spawn_duck:161: Normal duck spawned in #ct +2025-09-25 18:57:44,273 [INFO ] DuckHuntBot.Game - spawn_duck:137: Golden duck (hidden) spawned in #ct with 3 HP + \ No newline at end of file diff --git a/logs/duckhunt.log b/logs/duckhunt.log new file mode 100644 index 0000000..1ab517b --- /dev/null +++ b/logs/duckhunt.log @@ -0,0 +1,412 @@ +19:30:08.709 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:30:08.709 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +19:30:08.710 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +19:30:08.710 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +19:30:08.710 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +19:30:08.710 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +19:30:08.711 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:30:08.711 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +19:30:08.712 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +19:30:08.713 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:30:08.713 ๐Ÿ“˜ INFO SASL Debug mode: ON +19:30:08.714 ๐Ÿ“˜ INFO SASL Log everything: YES +19:30:08.714 ๐Ÿ“˜ INFO SASL Unified format: YES +19:30:08.714 ๐Ÿ“˜ INFO SASL Console level: INFO +19:30:08.715 ๐Ÿ“˜ INFO SASL File level: DEBUG +19:30:08.715 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:30:08.715 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +19:30:08.716 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 5 shop items from /home/colby/duckhunt/shop.json +19:30:08.718 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:30:08.718 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +19:30:08.807 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:30:09.028 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +19:30:09.028 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +19:30:09.028 ๐Ÿ“˜ INFO SASL SASL authentication enabled +19:30:09.029 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +19:30:09.416 ๐Ÿ“˜ INFO SASL Server capabilities: ['away-notify', 'chghost', 'invite-notify', 'multi-prefix', 'sasl', 'userhost-in-names'] +19:30:09.417 ๐Ÿ“˜ INFO SASL SASL capability available +19:30:09.476 ๐Ÿ“˜ INFO SASL SASL capability acknowledged +19:30:09.477 ๐Ÿ“˜ INFO SASL Authenticating via SASL as duckhunt +19:30:09.477 ๐Ÿ“˜ INFO SASL Sending AUTHENTICATE PLAIN +19:30:09.887 ๐Ÿ“˜ INFO SASL Server ready for SASL authentication +19:30:09.888 ๐Ÿ” DEBUG SASL Auth string length: 33 chars [handle_authenticate_response:106] +19:30:09.888 ๐Ÿ” DEBUG SASL Auth components: user='duckhunt', pass='duc...' [handle_authenticate_response:107] +19:30:09.888 ๐Ÿ” DEBUG SASL Base64 encoded length: 44 chars [handle_authenticate_response:110] +19:30:09.889 ๐Ÿ” DEBUG SASL Sending: AUTHENTICATE ZHVja2h1bnQAZHVja2h1... [handle_authenticate_response:111] +19:30:10.304 ๐Ÿ“˜ INFO SASL SASL authentication successful! +19:30:10.430 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +19:30:34.049 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +19:31:07.073 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:31:24.462 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +19:31:24.463 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +19:31:24.544 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +19:31:24.544 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +19:31:24.545 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +19:31:24.545 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +19:31:24.546 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +19:31:24.547 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +19:31:24.570 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:134] +19:31:24.571 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +19:31:24.773 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +19:31:24.774 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +19:31:25.531 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:31:25.531 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +19:31:25.531 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +19:31:25.531 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +19:31:25.532 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +19:31:25.532 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +19:31:25.532 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:31:25.532 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +19:31:25.533 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +19:31:25.533 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:31:25.534 ๐Ÿ“˜ INFO SASL Debug mode: ON +19:31:25.534 ๐Ÿ“˜ INFO SASL Log everything: YES +19:31:25.534 ๐Ÿ“˜ INFO SASL Unified format: YES +19:31:25.534 ๐Ÿ“˜ INFO SASL Console level: INFO +19:31:25.534 ๐Ÿ“˜ INFO SASL File level: DEBUG +19:31:25.535 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:31:25.535 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +19:31:25.536 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 5 shop items from /home/colby/duckhunt/shop.json +19:31:25.537 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:31:25.537 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +19:31:25.621 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:31:25.815 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +19:31:25.815 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +19:31:25.816 ๐Ÿ“˜ INFO SASL SASL authentication enabled +19:31:25.816 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +19:31:27.051 ๐Ÿ“˜ INFO SASL Server capabilities: ['away-notify', 'chghost', 'invite-notify', 'multi-prefix', 'sasl', 'userhost-in-names'] +19:31:27.051 ๐Ÿ“˜ INFO SASL SASL capability available +19:31:27.100 ๐Ÿ“˜ INFO SASL SASL capability acknowledged +19:31:27.103 ๐Ÿ“˜ INFO SASL Authenticating via SASL as duckhunt +19:31:27.103 ๐Ÿ“˜ INFO SASL Sending AUTHENTICATE PLAIN +19:31:27.512 ๐Ÿ“˜ INFO SASL Server ready for SASL authentication +19:31:27.513 ๐Ÿ” DEBUG SASL Auth string length: 33 chars [handle_authenticate_response:106] +19:31:27.513 ๐Ÿ” DEBUG SASL Auth components: user='duckhunt', pass='duc...' [handle_authenticate_response:107] +19:31:27.513 ๐Ÿ” DEBUG SASL Base64 encoded length: 44 chars [handle_authenticate_response:110] +19:31:27.513 ๐Ÿ” DEBUG SASL Sending: AUTHENTICATE ZHVja2h1bnQAZHVja2h1... [handle_authenticate_response:111] +19:31:27.922 ๐Ÿ“˜ INFO SASL SASL authentication successful! +19:31:29.097 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +19:31:46.833 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:32:07.421 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +19:32:07.422 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +19:32:07.424 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +19:32:07.424 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +19:32:07.425 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +19:32:07.425 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +19:32:07.425 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +19:32:07.426 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +19:32:07.439 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:134] +19:32:07.439 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +19:32:07.647 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +19:32:07.650 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +17:11:18.874 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +17:11:18.874 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +17:11:18.874 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +17:11:18.875 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +17:11:18.875 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +17:11:18.875 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +17:11:18.875 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +17:11:18.876 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +17:11:18.883 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +17:11:18.884 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +17:11:18.884 ๐Ÿ“˜ INFO SASL Debug mode: ON +17:11:18.884 ๐Ÿ“˜ INFO SASL Log everything: YES +17:11:18.885 ๐Ÿ“˜ INFO SASL Unified format: YES +17:11:18.885 ๐Ÿ“˜ INFO SASL Console level: INFO +17:11:18.885 ๐Ÿ“˜ INFO SASL File level: DEBUG +17:11:18.885 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +17:11:18.885 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +17:11:18.886 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 6 shop items from /home/colby/duckhunt/shop.json +17:11:18.886 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +17:11:18.886 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +17:11:18.941 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +17:11:19.228 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +17:11:19.228 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +17:11:19.228 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +17:11:21.111 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +17:11:38.245 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:12:50.310 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +17:13:11.327 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:14:31.393 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:15:37.451 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:16:52.528 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 5 HP +17:17:56.586 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:18:59.645 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +17:19:06.831 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:136] +17:19:14.373 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:136] +17:19:16.665 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:20:39.736 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 3 HP +17:21:48.800 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 5 HP +17:22:49.858 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:24:01.922 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +17:25:10.997 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:26:32.071 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:27:35.135 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:28:57.215 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:30:15.284 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:31:30.355 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +17:32:06.393 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 5 HP +17:33:13.455 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:34:22.518 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:35:24.570 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:36:37.639 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +17:37:54.733 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +17:38:16.763 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 5 HP +17:39:18.620 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +17:39:20.268 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +17:39:20.271 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +17:39:20.272 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +17:39:20.279 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +17:39:20.279 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +17:39:20.280 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +17:39:20.280 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +17:39:20.321 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:136] +17:39:20.321 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +17:39:20.567 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +17:39:20.568 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +18:37:16.947 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +18:37:16.948 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +18:37:16.949 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +18:37:16.949 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +18:37:16.949 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +18:37:16.949 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +18:37:16.950 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +18:37:16.950 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +18:37:16.958 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +18:37:16.959 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +18:37:16.960 ๐Ÿ“˜ INFO SASL Debug mode: ON +18:37:16.960 ๐Ÿ“˜ INFO SASL Log everything: YES +18:37:16.960 ๐Ÿ“˜ INFO SASL Unified format: YES +18:37:16.960 ๐Ÿ“˜ INFO SASL Console level: INFO +18:37:16.960 ๐Ÿ“˜ INFO SASL File level: DEBUG +18:37:16.960 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +18:37:16.961 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +18:37:16.962 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +18:37:16.962 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +18:37:16.963 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +18:37:17.032 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +18:37:17.428 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +18:37:17.428 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +18:37:17.429 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +18:37:19.105 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +18:37:32.441 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:38:34.495 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:39:37.167 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +18:39:43.570 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 3 HP +18:40:51.627 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:41:14.648 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:42:21.722 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:43:25.380 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +18:43:25.389 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +18:43:25.411 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +18:43:25.411 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +18:43:25.412 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +18:43:25.416 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +18:43:25.416 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +18:43:25.416 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +18:43:25.426 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +18:43:25.427 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +18:43:25.639 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +18:43:25.639 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +18:43:26.132 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +18:43:26.132 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +18:43:26.133 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +18:43:26.134 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +18:43:26.135 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +18:43:26.135 ๐Ÿ“˜ INFO SASL Debug mode: ON +18:43:26.136 ๐Ÿ“˜ INFO SASL Log everything: YES +18:43:26.136 ๐Ÿ“˜ INFO SASL Unified format: YES +18:43:26.136 ๐Ÿ“˜ INFO SASL Console level: INFO +18:43:26.136 ๐Ÿ“˜ INFO SASL File level: DEBUG +18:43:26.136 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +18:43:26.137 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +18:43:26.137 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +18:43:26.138 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +18:43:26.138 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +18:43:26.215 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +18:43:26.392 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +18:43:26.393 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +18:43:26.393 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +18:43:30.050 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +18:43:53.434 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:44:23.468 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:45:10.514 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +18:45:10.515 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +18:45:10.519 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +18:45:10.519 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +18:45:10.519 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +18:45:10.520 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +18:45:10.520 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +18:45:10.521 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +18:45:10.550 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +18:45:10.550 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +18:45:10.755 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +18:45:10.755 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +18:45:11.133 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +18:45:11.134 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +18:45:11.134 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +18:45:11.134 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +18:45:11.135 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +18:45:11.136 ๐Ÿ“˜ INFO SASL Debug mode: ON +18:45:11.136 ๐Ÿ“˜ INFO SASL Log everything: YES +18:45:11.136 ๐Ÿ“˜ INFO SASL Unified format: YES +18:45:11.136 ๐Ÿ“˜ INFO SASL Console level: INFO +18:45:11.136 ๐Ÿ“˜ INFO SASL File level: DEBUG +18:45:11.137 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +18:45:11.137 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +18:45:11.137 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +18:45:11.138 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +18:45:11.138 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +18:45:11.187 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +18:45:11.355 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +18:45:11.355 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +18:45:11.356 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +18:45:13.291 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +18:45:34.379 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:46:10.413 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:47:06.032 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +18:47:06.041 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +18:47:06.082 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +18:47:06.082 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +18:47:06.082 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +18:47:06.083 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +18:47:06.084 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +18:47:06.084 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +18:47:06.108 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +18:47:06.109 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +18:47:06.324 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +18:47:06.325 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +18:47:06.755 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +18:47:06.755 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +18:47:06.756 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +18:47:06.756 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +18:47:06.756 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +18:47:06.756 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +18:47:06.756 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +18:47:06.757 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +18:47:06.757 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +18:47:06.764 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +18:47:06.765 ๐Ÿ“˜ INFO SASL Debug mode: ON +18:47:06.765 ๐Ÿ“˜ INFO SASL Log everything: YES +18:47:06.765 ๐Ÿ“˜ INFO SASL Unified format: YES +18:47:06.765 ๐Ÿ“˜ INFO SASL Console level: INFO +18:47:06.765 ๐Ÿ“˜ INFO SASL File level: DEBUG +18:47:06.765 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +18:47:06.766 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +18:47:06.766 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +18:47:06.766 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +18:47:06.766 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +18:47:06.813 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +18:47:06.991 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +18:47:06.991 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +18:47:06.992 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +18:47:09.170 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +18:47:23.007 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:48:02.049 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:48:31.072 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:49:49.143 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:50:38.197 โš ๏ธ WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +18:51:05.220 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:52:09.272 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 3 HP +18:53:29.365 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 3 HP +18:54:43.468 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +18:55:15.496 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:56:27.559 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:56:48.773 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +18:56:48.777 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +18:56:48.816 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +18:56:48.816 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +18:56:48.817 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +18:56:48.817 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +18:56:48.817 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +18:56:48.818 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +18:56:48.857 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +18:56:48.857 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +18:56:49.325 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +18:56:49.326 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +18:56:49.936 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +18:56:49.937 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +18:56:49.938 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +18:56:49.938 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +18:56:49.939 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +18:56:49.939 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +18:56:49.939 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +18:56:49.940 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +18:56:49.940 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +18:56:49.941 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +18:56:49.941 ๐Ÿ“˜ INFO SASL Debug mode: ON +18:56:49.942 ๐Ÿ“˜ INFO SASL Log everything: YES +18:56:49.942 ๐Ÿ“˜ INFO SASL Unified format: YES +18:56:49.942 ๐Ÿ“˜ INFO SASL Console level: INFO +18:56:49.943 ๐Ÿ“˜ INFO SASL File level: DEBUG +18:56:49.943 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +18:56:49.943 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +18:56:49.944 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +18:56:49.945 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +18:56:49.946 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +18:56:50.005 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +18:56:50.174 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +18:56:50.174 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +18:56:50.175 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +18:56:52.108 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +18:57:01.185 ๐Ÿ“˜ INFO DuckHuntBot.Game Golden duck (hidden) spawned in #ct with 4 HP +18:57:11.327 โš ๏ธ WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +18:58:06.241 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +18:58:38.133 โš ๏ธ WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +18:59:10.630 โš ๏ธ WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +18:59:21.310 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:00:26.365 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:01:20.851 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Received SIGINT (Ctrl+C), shutting down immediately... +19:01:20.860 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”„ Cancelled 5 running tasks +19:01:20.861 ๐Ÿ“˜ INFO DuckHuntBot Message loop cancelled +19:01:20.861 ๐Ÿ“˜ INFO DuckHuntBot Message loop ended +19:01:20.862 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ›‘ Main loop cancelled +19:01:20.862 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck spawning loop cancelled +19:01:20.862 ๐Ÿ“˜ INFO DuckHuntBot.Game Duck timeout loop cancelled +19:01:20.863 ๐Ÿ“˜ INFO DuckHuntBot.Game Game loops cancelled +19:01:20.888 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +19:01:20.888 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ’พ Database saved +19:01:21.097 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ”Œ IRC connection closed +19:01:21.097 ๐Ÿ“˜ INFO DuckHuntBot โœ… Bot shutdown complete +19:01:22.172 ๐Ÿ“˜ INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:01:22.172 ๐Ÿ“˜ INFO DuckHuntBot Debug mode: ON +19:01:22.173 ๐Ÿ“˜ INFO DuckHuntBot Log everything: YES +19:01:22.173 ๐Ÿ“˜ INFO DuckHuntBot Unified format: YES +19:01:22.173 ๐Ÿ“˜ INFO DuckHuntBot Console level: INFO +19:01:22.173 ๐Ÿ“˜ INFO DuckHuntBot File level: DEBUG +19:01:22.173 ๐Ÿ“˜ INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:01:22.174 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿค– Initializing DuckHunt Bot components... +19:01:22.174 ๐Ÿ“˜ INFO DuckHuntBot.DB Loaded 1 players from duckhunt.json +19:01:22.181 ๐Ÿ“˜ INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:01:22.181 ๐Ÿ“˜ INFO SASL Debug mode: ON +19:01:22.181 ๐Ÿ“˜ INFO SASL Log everything: YES +19:01:22.182 ๐Ÿ“˜ INFO SASL Unified format: YES +19:01:22.182 ๐Ÿ“˜ INFO SASL Console level: INFO +19:01:22.182 ๐Ÿ“˜ INFO SASL File level: DEBUG +19:01:22.182 ๐Ÿ“˜ INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:01:22.183 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ‘‘ Configured 3 admin(s): peorth, computertech, colby +19:01:22.184 ๐Ÿ“˜ INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +19:01:22.185 ๐Ÿ“˜ INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:01:22.185 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Starting DuckHunt Bot... +19:01:22.231 ๐Ÿ“˜ INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:01:22.424 ๐Ÿ“˜ INFO DuckHuntBot โœ… Successfully connected to irc.rizon.net:6697 +19:01:22.424 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿ” Sending server password +19:01:22.425 ๐Ÿ“˜ INFO DuckHuntBot ๐Ÿฆ† Bot is now running! Press Ctrl+C to stop. +19:01:24.093 ๐Ÿ“˜ INFO DuckHuntBot Successfully registered with IRC server +19:01:28.998 โš ๏ธ WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:01:29.016 ๐Ÿ” DEBUG DuckHuntBot.DB Database saved successfully with 1 players [save_database:142] +19:01:48.445 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:02:49.507 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:04:05.573 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct +19:05:15.648 ๐Ÿ“˜ INFO DuckHuntBot.Game Fast duck (hidden) spawned in #ct +19:05:39.670 ๐Ÿ“˜ INFO DuckHuntBot.Game Normal duck spawned in #ct diff --git a/messages.json b/messages.json index 6a4055a..1d5dd72 100644 --- a/messages.json +++ b/messages.json @@ -12,10 +12,12 @@ "bang_hit_golden_killed": "{nick} > *BANG* You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_hit_fast": "{nick} > *BANG* You shot a FAST DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_miss": "{nick} > *BANG* You missed the duck!", + "bang_friendly_fire_penalty": "{nick} > *BANG* You missed and hit {victim}! [GUN CONFISCATED] [LOST {xp_lost} XP]", + "bang_friendly_fire_insured": "{nick} > *BANG* You missed and hit {victim}! [INSURANCE PROTECTED - No penalties]", "bang_no_duck": "{nick} > *BANG* What did you shoot at? There is no duck in the area... [GUN CONFISCATED]", "bang_no_ammo": "{nick} > *click* You're out of ammo! Use !reload", "bang_gun_jammed": "{nick} > *click* Your gun jammed! [AMMO WASTED]", - "bang_not_armed": "{nick} > You are not armed.", + "bang_not_armed": "{nick} > Your gun has been confiscated. Buy it back from the shop.", "bef_success": "{nick} > *befriend* You befriended the duck! [+{xp_gained} xp] [Ducks befriended: {ducks_befriended}]", "bef_failed": "{nick} > *gentle approach* The duck doesn't trust you and flies away...", "bef_no_duck": "{nick} > *gentle approach* There is no duck to befriend in the area...", @@ -47,6 +49,19 @@ "shop_buy_invalid_id": "{nick} > Invalid item ID. Use !shop to see available items.", "shop_buy_usage": "Usage: !shop buy ", "use_attract_ducks": "{nick} > You scattered bread around the pond! Ducks will spawn {spawn_multiplier}x faster for {duration} minutes.", + "use_insurance": "{nick} > You activated Hunter's Insurance! Protected from friendly fire penalties for {duration} hours.", + "use_buy_gun_back": "{nick} > Your gun has been returned with {ammo_restored} bullets and {magazines_restored} magazines.", + "use_buy_gun_back_not_needed": "{nick} > Your gun is not confiscated.", + "bang_wet_clothes": "{nick} > *SPLASH* Your clothes are soaked! You can't shoot until you dry off or buy new clothes.", + "use_splash_water": "{nick} > *SPLASH* You soaked {target_nick} with water! They can't shoot for {duration} minutes.", + "use_dry_clothes": "{nick} > You changed into dry clothes! Ready to hunt again.", + "use_dry_clothes_not_needed": "{nick} > You weren't wet - no need for new clothes.", + "gift_ammo": "{nick} > Gave {amount} bullet(s) to {target_nick}! What a generous hunter.", + "gift_magazine": "{nick} > Gave 1 magazine to {target_nick}! Sharing the ammo love.", + "gift_gun_brush": "{nick} > Gave a gun brush to {target_nick} - keeping their weapon clean!", + "gift_insurance": "{nick} > Gave Hunter's Insurance to {target_nick} - protecting them from friendly fire!", + "gift_dry_clothes": "{nick} > Gave dry clothes to {target_nick} - now they can stay dry!", + "gift_buy_gun_back": "{nick} > Gave a gun license to {target_nick} - helping them get their gun back!", "colours": { "white": "\u00030", diff --git a/shop.json b/shop.json index 9cc4319..d226d05 100644 --- a/shop.json +++ b/shop.json @@ -19,8 +19,7 @@ "price": 10, "description": "Throw sand in target's gun - increases jam chance by 15%", "type": "sabotage_jam", - "amount": 15, - "target_required": true + "amount": 15 }, "4": { "name": "Gun Brush", @@ -36,6 +35,32 @@ "type": "attract_ducks", "duration": 1200, "spawn_multiplier": 2.0 + }, + "6": { + "name": "Hunter's Insurance", + "price": 75, + "description": "Protects against friendly fire penalties for 24 hours - no XP loss or gun confiscation", + "type": "insurance", + "duration": 86400, + "protection": "friendly_fire" + }, + "7": { + "name": "Buy Gun Back", + "price": 40, + "description": "Get your confiscated gun back with the same ammo it had when taken", + "type": "buy_gun_back" + }, + "8": { + "name": "Bucket of Water", + "price": 25, + "description": "Splash another hunter with water - soaks their clothes and prevents shooting until they dry or buy new clothes", + "type": "splash_water" + }, + "9": { + "name": "Dry Clothes", + "price": 30, + "description": "Change into dry clothes - allows shooting again after being soaked", + "type": "dry_clothes" } } } \ No newline at end of file diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc index 1f8efe806882a37e2943ff24610ec9821b3e82bc..3eaf3570fc9f1961ebdf7c37c37616048ed7b6d4 100644 GIT binary patch delta 4790 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 delta 2567 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 z

On3B~I1iYqtsR6zu6^?wMXlT->-C*@dm$c7to>SidC3q5m(Ozv9x2)QFm%+_M? zrIG|T^d_m^!C!zJ8#F=?ANvicHsnb6B~UB0P>Zjy8W<(abhTw z(m@6;1C4qw_FS_c?jv{f1A5g}A!=Z)bTy?hV6 zBy4ce=a2dP{75M7dyHqoynA9n_c$EjK8hY@N6}R9^7!;P_t>N^VJ)88H@o&)NpvU@uWaQ#8|T~RPk-0{ zZ9k}XhmOWKKg4f3#^)TLR4r*8GaVmjOVKV?c%_i{Y`<|J?%XxG`+h`4BVV!kxkEFq z=X!6nfl9ghceedSqztU+_g#U z1(=6fl?hjFqM(puP42jxBhl!e)m+j%YrJHfF~rpFMRjdVT^kK9sGGRI{Q|wYHd?)) zUMC#KmMu;=q|xyF-uW%JJ$%n8{!kz9WKWLwYALO-L>@@eqXM8fYf#QDo%#-`@bwI2Q!AS&906>qem%$hk_9?`D z1%VX-gWzcd{{}%Hf;<38CF8?agR!&RpZ{KkdpqJ$pJu;{0R4eU!*+BXLj*lTBF{V; zc)^kW_v>rU;KCh3vhN`HQw0AJ!Jh#LV8Euh^6)KVXMVp*S|-1K`S+*fQj>=JQe^?0 z#j>~q+$OH{1fI*(-0LqYxc!&qT=jDrP+=;`S&%J(B)J{8nndY$5dQ<*@yIBdr50aV z%k3YvqB@ZdQzR99?>S8t6n&e?-oV-G&GNwZppol(J`e7` zg{A#cMiw-1-@2T~^*>+59dMY{a7AM~w305z{n3zy`^ob~+*?<2xfieKxU#Qg!~5Q_ z{-0gZDAs5sP~XH?@}%%}X(o@m@|7B@2^*!>5u#tcpjc)C-T?*SGLkbJNR}`@ri|=g zzNoDor{MK6(r1bZ)RR}uED-qT1iB!06FsoxbEcU4)62OT70iB7?|}E$JjB8ucF#L1F=zD|4hE|uU1r7zg!))hMRr?0>`LlY~rnMApPg7t-g|H&xQ+@ zipLepfuMwICD(gJ970ce`1I7Oq08mAyTov;Hyt5|06Dr3*0|^^~s!k)(`BkzlY!(2x#uo z)#^wTlelt|+LXH^2=DzAF6^4%+5j8u#g|R=4vmbA2H3Z-KC+T=)v~zulkyARL5Oi^ z?>u;@r+53lz5Dm1wWaQAa}*KFCJ1eKDshmQxka zsae!PjD8K^VA~~Lltp7x%-FPGTuWXObUptij?z9xN%TdZqEM~9{_kf$p(3xqC_H67 zsHA4hoq3Yis7}57njKO%WmKn0epB9x=`B>JS^kE^0qHj!RHse;hI3mv{CEp8obtCN zHb}p1&qdg%gu32duj-=Y??_Atv*ie_xqyGCp{7eMxm7@QDdo2c<&citDsArCB)RQ0 zLpHn;XHTR2b`1^bcl8uh_pXURGl4b&odo99?%phWFH5$kS@NDz+xL_dH1<7pZC8uzduwHg*+5}B(n5Z0((I{|eP3Cyr&9L)QtZL^D;0>T zlViGBbxk^zqdA~e@g)TZl&TnTDwD;O3c$p)HcS^$2TIkkVr=FE z88CW%>(@`ol_NNDFL2RlG-8K2#c~KxofE>CU%(XE>Xx@KToDj3>^n8q5AUId_hkBn zS+2#UAxvgQ*b2!^Cu@hS3L6xJ;{ma!pm&?5FPSp(+%`_j;&Yh2vg}Tv2^bCxTCOPDSk}Ya0~wz0=fQ3Mx1$ zEmIt&?sZdoMa{heAEl_j*Dc#d0YL}6{E)skN;v>F5ztL3vhSUe?xYlJ?sel=%O~B^ zLPcS*&^wg)9=k9Z#;~Vl2=FMwKEmDmqqPT#NK@nQ0tBdxB;^5bV2DM74Y}{eYB5!b q0B=DHgY{D!$MgVxrrtO_!i)|1HnV@ph5oq3*ao*d?@166qW=jg8%^^7 delta 11392 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 IA!LI5KOIz`sQ>@~ diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 6b864107223cdaf42da7ac0f71cd5095ef5bf029..f58a32e292b554cc1427ea31de6c28bf8ad5bc4d 100644 GIT binary patch delta 6859 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 delta 3367 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 UOu28k)Hi&BU~%GCxK76We|bCbdH?_b diff --git a/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc index b8767e6c31653ccc5aea29a737af62f201217fc6..177f689a258bb26f98c76ce980845495fc3e7722 100644 GIT binary patch delta 48 zcmaDC+8oAxnwOW00SMM^y|$6tiCIiRKO;XkRX;gDC#h0Dr8GG^qcpEXf3qL+0(Ag@ CI}kMh delta 27 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 IWR1Z4KZV`Uy#N3J delta 2354 zcmZ`)ZERCj7{2HBYolw|_4D>?ZCA#2*}73S1Yx6Y5EvUEW6HE3W$d&t*b2EV2=h!O zEDC-g@M?&%MNnhLNBFS(0HbCYjYbpGDTzyoApeXeG9mm>f9N^)c4PR_q|bfN`##V6 zp7Y*w&b{{*`YCPv+F+Q$(O-DZwXG+YT{H^FdKhusF)q$U5$2*iMo|?OU?Dn&oUn(l zfZx$d7(vb-XaaJ82@UnLbLWv29QtwNh_Eg2#4h+puGhi`w@Z zM0e&_zSK~nqbe2s-+hN?g$V~Ieb!9aRD*b_9d4N%&|k9LLkh9ApR$OK6f`JLge}a% zHb6GPgl#M&Pr*KI5yIBhLb&5()CLi)rGnbFnkC`LqchSp`jfjmP$ z10#hY3Yk^5B?$ciN9_CIbH`Il*Hd0j8&4$S-RWF$x=Y#;Psh6CVH}@QOU*Pcc^FBR zN=rF&tZV1a-o3G&bi6O7s6;)S6K2*&8AmVsdvJ_#4c)sWDV|Knx_9hKrgH{`9+Tp! zUA^g)p7LR>Gs5#cT#uB)W# zVim%!Y4f)cTV7=+cB*p8Di>ARr&Mk>f8>FaeqWV$s-8du%J;~8H??sMc2^+awk3Fial z`cj&wSm_`HybjxRP5F>o?))s!mon2c|bA^dDQJW_u+)19AM%NrT@j zsLF6Hh~cmw4&A=<&pL@_e{$Gazi9H51np)p2f^ zKSQr7_Cp-z@jS(c9~$C2IB`DQtT?ajr9r6=Uat(oN0oC?AKb3Iq*t6QvJcLQ4f@?O zNg3Z0w>w^D=6(vIN(wVucI<=eRSmOs`NQ4Ula!J)mQ$yD`r?kDGGElLR>7TG0qX1Chl}%Sl+zEl7TOE6)2>mlEIJC| zMK)9f7fqI`BBI8H?EN0+uwj^=9AUk@4>Ux(hu@>z;zqDEgpitPONua>d{=_7OIPiVO!(*v>4xX1s!r*6zscVX3N z{db<01FI;>;~)11PRx39!?>$rJXir2nuN?tO*au;$hIZj9j94s_F2v8(>u~#z4G;+ z61DQ)kpq&KQRI)sA!f0?@W7JfHLw)QLe7G)^hy3W%A}TRczv!g8Gl;&Ou)dhdHfZW zxwPyvgwDX;mO!|ZspuZyL|&=I9JHB>nO{!nVHT^KQ*YbRlZ3BY2KlQ9)~xs#Il$Oj zgSw!xwaJxWw9O1g7@TA90fP@=xHZ84fMB%sU1vMem`^Mnv5a(&2q#xhaBO_JgFb<@ zxr}Ty!Ewv^HlDfOYpZ-tb|v^$m8KJoSVy`q)QxOCRXV}Rj?3xMC0|-bTdq8tcVySD z%XUVy8)Dhc*x1@lcNN-iPiPltjMLS0I2xSb=p(ysV>Z^E#apsHy;0#oDG=j4$Bc@N-id+or3|@slS1v|ZVd1Lk;$|Ik-r*R^-`2kY*P2Sj diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index ff7e9d9ee3974ca38e34560ae434b72768eb30d9..7983bd02bd6f75a5af097c0740a9f0ae95e21873 100644 GIT binary patch delta 20 acmcZ}cs-E&G%qg~0}!m;dTk^384UnSng=rg delta 20 acmcZ}cs-E&G%qg~0}zxOT;9liMgssw^9FSQ diff --git a/src/db.py b/src/db.py index 2fa3cf3..44a9ab1 100644 --- a/src/db.py +++ b/src/db.py @@ -68,6 +68,8 @@ class DuckDB: sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP sanitized['ducks_shot'] = max(0, int(player_data.get('ducks_shot', 0))) sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 0))) + sanitized['shots_fired'] = max(0, int(player_data.get('shots_fired', 0))) + sanitized['shots_missed'] = max(0, int(player_data.get('shots_missed', 0))) default_accuracy = self.bot.get_config('default_accuracy', 75) if self.bot else 75 max_accuracy = self.bot.get_config('max_accuracy', 100) if self.bot else 100 sanitized['accuracy'] = max(0, min(max_accuracy, int(player_data.get('accuracy', default_accuracy)))) # 0-max_accuracy range @@ -76,6 +78,12 @@ class DuckDB: # Ammo system with validation sanitized['current_ammo'] = max(0, min(50, int(player_data.get('current_ammo', 6)))) sanitized['magazines'] = max(0, min(20, int(player_data.get('magazines', 3)))) + + # Confiscated ammo (optional fields) + if 'confiscated_ammo' in player_data: + sanitized['confiscated_ammo'] = max(0, min(50, int(player_data.get('confiscated_ammo', 0)))) + if 'confiscated_magazines' in player_data: + sanitized['confiscated_magazines'] = max(0, min(20, int(player_data.get('confiscated_magazines', 0)))) sanitized['bullets_per_magazine'] = max(1, min(50, int(player_data.get('bullets_per_magazine', 6)))) sanitized['jam_chance'] = max(0, min(100, int(player_data.get('jam_chance', 5)))) @@ -237,6 +245,8 @@ class DuckDB: 'xp': xp, 'ducks_shot': 0, 'ducks_befriended': 0, + 'shots_fired': 0, # Total shots fired + 'shots_missed': 0, # Total shots that missed 'current_ammo': bullets_per_mag, # Bullets in current magazine 'magazines': magazines, # Total magazines (including current) 'bullets_per_magazine': bullets_per_mag, # Bullets per magazine diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index 77192c8..b628030 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -58,11 +58,44 @@ class DuckHuntBot: return value def is_admin(self, user): - """Check if user is admin by nick only""" + """Check if user is admin with enhanced security checks""" if '!' not in user: return False + nick = user.split('!')[0].lower() - return nick in self.admins + + # Check admin configuration - support both nick-only (legacy) and hostmask patterns + admin_config = self.get_config('admins', []) + + # Ensure admin_config is a list + if not isinstance(admin_config, list): + admin_config = [] + + for admin_entry in admin_config: + if isinstance(admin_entry, str): + # Simple nick-based check (less secure but compatible) + if admin_entry.lower() == nick: + self.logger.warning(f"Admin access granted via nick-only authentication: {user}") + return True + elif isinstance(admin_entry, dict): + # Enhanced hostmask-based authentication + if admin_entry.get('nick', '').lower() == nick: + # Check hostmask pattern if provided + required_pattern = admin_entry.get('hostmask') + if required_pattern: + import fnmatch + if fnmatch.fnmatch(user.lower(), required_pattern.lower()): + self.logger.info(f"Admin access granted via hostmask: {user}") + return True + else: + self.logger.warning(f"Admin nick match but hostmask mismatch: {user} vs {required_pattern}") + return False + else: + # Nick-only fallback + self.logger.warning(f"Admin access granted via nick-only (no hostmask configured): {user}") + return True + + return False def _handle_single_target_admin_command(self, args, usage_message_key, action_func, success_message_key, nick, channel): """Helper for admin commands that target a single player""" @@ -307,6 +340,11 @@ class DuckHuntBot: self.logger.error(f"Error getting player data for {nick}: {e}") player = {} + # Track activity for channel membership validation + if channel.startswith('#'): # Only track for channel messages + player['last_activity_channel'] = channel + player['last_activity_time'] = time.time() + # Check if player is ignored (unless it's an admin) try: if player.get('ignored', False) and not self.is_admin(user): @@ -360,6 +398,77 @@ class DuckHuntBot: except Exception as send_error: self.logger.error(f"Error sending error message: {send_error}") + def validate_target_player(self, target_nick, channel): + """ + Validate that a target player is a valid hunter + Returns (is_valid, player_data, error_message) + + TODO: Implement proper channel membership tracking to ensure + the target is actually present in the channel + """ + if not target_nick: + return False, None, "No target specified" + + # Normalize the nickname + target_nick = target_nick.lower().strip() + + # Check if target_nick is empty after normalization + if not target_nick: + return False, None, "Invalid target nickname" + + # Check if player exists in database + player = self.db.get_player(target_nick) + if not player: + return False, None, f"Player '{target_nick}' not found. They need to participate in the game first." + + # Check if player has any game activity (basic validation they're a hunter) + has_activity = ( + player.get('xp', 0) > 0 or + player.get('shots_fired', 0) > 0 or + 'current_ammo' in player or + 'magazines' in player + ) + + if not has_activity: + return False, None, f"Player '{target_nick}' has no hunting activity. They may not be an active hunter." + + # Check if player is currently in the channel (for channel messages only) + if channel.startswith('#'): + is_in_channel = self.is_user_in_channel_sync(target_nick, channel) + if not is_in_channel: + return False, None, f"Player '{target_nick}' is not currently in {channel}." + + return True, player, None + + def is_user_in_channel_sync(self, nick, channel): + """ + Check if a user is likely in the channel based on recent activity (synchronous version) + + This is a practical approach that doesn't require complex IRC response parsing. + We assume if someone has been active recently, they're still in the channel. + """ + try: + player = self.db.get_player(nick) + if not player: + return False + + # Check if they've been active in this channel recently + last_activity_channel = player.get('last_activity_channel') + last_activity_time = player.get('last_activity_time', 0) + current_time = time.time() + + # If they were active in this channel within the last 30 minutes, assume they're still here + if (last_activity_channel == channel and + current_time - last_activity_time < 1800): # 30 minutes + return True + + # If no recent activity in this channel, they might not be here + return False + + except Exception as e: + self.logger.error(f"Error checking channel membership for {nick} in {channel}: {e}") + return True # Default to allowing the command if we can't check + async def handle_bang(self, nick, channel, player): """Handle !bang command""" result = self.game.shoot_duck(nick, channel, player) @@ -409,11 +518,12 @@ class DuckHuntBot: """Handle buying an item from the shop""" target_player = None - # Get target player if specified + # Get target player if specified and validate they're in channel if target_nick: - target_player = self.db.get_player(target_nick) - if not target_player: - message = f"{nick} > Target player '{target_nick}' not found" + # Use the same validation as other commands + is_valid, target_player, error_msg = self.validate_target_player(target_nick, channel) + if not is_valid: + message = f"{nick} > {error_msg}" self.send_message(channel, message) return @@ -460,11 +570,15 @@ class DuckHuntBot: # Apply color formatting bold = self.messages.messages.get('colours', {}).get('bold', '') reset = self.messages.messages.get('colours', {}).get('reset', '') + green = self.messages.messages.get('colours', {}).get('green', '') + blue = self.messages.messages.get('colours', {}).get('blue', '') + yellow = self.messages.messages.get('colours', {}).get('yellow', '') + red = self.messages.messages.get('colours', {}).get('red', '') # Get player level info level_info = self.levels.get_player_level_info(player) level = level_info['level'] - level_name = level_info['level_data']['name'] + level_name = level_info['name'] # Build stats message xp = player.get('xp', 0) @@ -472,23 +586,40 @@ class DuckHuntBot: ducks_befriended = player.get('ducks_befriended', 0) accuracy = player.get('accuracy', self.get_config('player_defaults.accuracy', 75)) + # Calculate additional stats + total_ducks_encountered = ducks_shot + ducks_befriended + shots_missed = player.get('shots_missed', 0) + total_shots = ducks_shot + shots_missed + hit_rate = round((ducks_shot / total_shots * 100) if total_shots > 0 else 0, 1) + + # Get level progression info + xp_needed = level_info.get('needed_for_next', 0) + next_level_name = level_info.get('next_level_name', 'Max Level') + if xp_needed > 0: + xp_progress = f" (Need {xp_needed} XP for {next_level_name})" + else: + xp_progress = " (Max level reached!)" + # Ammo info current_ammo = player.get('current_ammo', 0) magazines = player.get('magazines', 0) bullets_per_mag = player.get('bullets_per_magazine', 6) + jam_chance = player.get('jam_chance', 0) # Gun status - gun_status = "๐Ÿ”ซ Armed" if not player.get('gun_confiscated', False) else "โŒ Confiscated" + gun_status = "Armed" if not player.get('gun_confiscated', False) else "Confiscated" - stats_lines = [ - f"๐Ÿ“Š {bold}Duck Hunt Stats for {nick}{reset}", - f"๐Ÿ† Level {level}: {level_name}", - f"โญ XP: {xp}", - f"๐Ÿฆ† Ducks Shot: {ducks_shot}", - f"๐Ÿ’š Ducks Befriended: {ducks_befriended}", - f"๐ŸŽฏ Accuracy: {accuracy}%", - f"๐Ÿ”ซ Status: {gun_status}", - f"๐Ÿ’€ Ammo: {current_ammo}/{bullets_per_mag} | Magazines: {magazines}" + # Build compact stats message with subtle colors + stats_parts = [ + f"Lv{level} {level_name}", + f"{green}{xp}XP{reset}{xp_progress}", + f"{ducks_shot} shot", + f"{ducks_befriended} befriended", + f"{accuracy}% accuracy", + f"{hit_rate}% hit rate", + f"{green if gun_status == 'Armed' else red}{gun_status}{reset}", + f"{current_ammo}/{bullets_per_mag}|{magazines} mags", + f"{jam_chance}% jam chance" ] # Add inventory if player has items @@ -500,11 +631,18 @@ class DuckHuntBot: if item: items.append(f"{item['name']} x{quantity}") if items: - stats_lines.append(f"๐ŸŽ’ Inventory: {', '.join(items)}") + stats_parts.append(f"Items: {', '.join(items)}") - # Send each line - for line in stats_lines: - self.send_message(channel, line) + # Add temporary effects if any + temp_effects = player.get('temporary_effects', []) + if temp_effects: + active_effects = [effect.get('name', 'Unknown Effect') for effect in temp_effects if isinstance(effect, dict)] + if active_effects: + stats_parts.append(f"Effects:{','.join(active_effects)}") + + # Send as one compact message + stats_message = f"{bold}{nick}{reset}: {' | '.join(stats_parts)}" + self.send_message(channel, stats_message) async def handle_topduck(self, nick, channel): """Handle !topduck command - show leaderboards""" @@ -574,9 +712,9 @@ class DuckHuntBot: # Get target player if specified if target_nick: - target_player = self.db.get_player(target_nick) - if not target_player: - message = f"{nick} > Target player '{target_nick}' not found" + is_valid, target_player, error_msg = self.validate_target_player(target_nick, channel) + if not is_valid: + message = f"{nick} > {error_msg}" self.send_message(channel, message) return @@ -597,8 +735,58 @@ class DuckHuntBot: spawn_multiplier=effect.get('spawn_multiplier', 2.0), duration=effect.get('duration', 10) ) + elif effect_type == 'insurance': + # Use specific message for insurance + message = self.messages.get('use_insurance', + nick=nick, + duration=effect.get('duration', 24) + ) + elif effect_type == 'buy_gun_back': + # Use specific message for buying gun back + if effect.get('restored', False): + message = self.messages.get('use_buy_gun_back', nick=nick, + ammo_restored=effect.get('ammo_restored', 0), + magazines_restored=effect.get('magazines_restored', 0)) + else: + message = self.messages.get('use_buy_gun_back_not_needed', nick=nick) + elif effect_type == 'splash_water': + # Use specific message for water splash + message = self.messages.get('use_splash_water', + nick=nick, + target_nick=target_nick, + duration=effect.get('duration', 5)) + elif effect_type == 'dry_clothes': + # Use specific message for dry clothes + if effect.get('was_wet', False): + message = self.messages.get('use_dry_clothes', nick=nick) + else: + message = self.messages.get('use_dry_clothes_not_needed', nick=nick) elif result.get("target_affected"): - message = f"{nick} > Used {result['item_name']} on {target_nick}!" + # Check if it's a gift (beneficial effect to target) + if effect.get('is_gift', False): + # Use specific gift messages based on item type + if effect_type == 'ammo': + message = self.messages.get('gift_ammo', + nick=nick, target_nick=target_nick, amount=effect.get('amount', 1)) + elif effect_type == 'magazine': + message = self.messages.get('gift_magazine', + nick=nick, target_nick=target_nick) + elif effect_type == 'clean_gun': + message = self.messages.get('gift_gun_brush', + nick=nick, target_nick=target_nick) + elif effect_type == 'insurance': + message = self.messages.get('gift_insurance', + nick=nick, target_nick=target_nick) + elif effect_type == 'dry_clothes': + message = self.messages.get('gift_dry_clothes', + nick=nick, target_nick=target_nick) + elif effect_type == 'buy_gun_back': + message = self.messages.get('gift_buy_gun_back', + nick=nick, target_nick=target_nick) + else: + message = f"{nick} > Gave {result['item_name']} to {target_nick}!" + else: + message = f"{nick} > Used {result['item_name']} on {target_nick}!" else: message = f"{nick} > Used {result['item_name']}!" @@ -618,20 +806,48 @@ class DuckHuntBot: is_private_msg = not channel.startswith('#') if args: - target = args[0].lower() - player = self.db.get_player(target) - if player is None: - player = {} - player['gun_confiscated'] = False + target_nick = args[0] - # Update magazines based on player level + # Validate target player (only for channel messages, skip validation if targeting self) + player = None + if not is_private_msg: + # If targeting self, skip validation since the user is obviously in the channel + if target_nick.lower() == nick.lower(): + target_nick = target_nick.lower() + player = self.db.get_player(target_nick) + if player is None: + player = self.db.create_player(target_nick) + self.db.players[target_nick] = player + else: + is_valid, player, error_msg = self.validate_target_player(target_nick, channel) + if not is_valid: + message = f"{nick} > {error_msg}" + self.send_message(channel, message) + return + # Ensure player is properly stored in database + target_nick = target_nick.lower() + if target_nick not in self.db.players: + self.db.players[target_nick] = player + else: + # For private messages, allow targeting any nick (admin override) + target_nick = target_nick.lower() + player = self.db.get_player(target_nick) + if player is None: + # Create new player data for the target + player = self.db.create_player(target_nick) + self.db.players[target_nick] = player + + # At this point player is guaranteed to be not None + if player is not None: + player['gun_confiscated'] = False # Update magazines based on player level self.levels.update_player_magazines(player) player['current_ammo'] = player.get('bullets_per_magazine', 6) + # Player data is already modified in place and will be saved by save_database() if is_private_msg: - message = f"{nick} > Rearmed {target}" + message = f"{nick} > Rearmed {target_nick}" else: - message = self.messages.get('admin_rearm_player', target=target, admin=nick) + message = self.messages.get('admin_rearm_player', target=target_nick, admin=nick) self.send_message(channel, message) else: if is_private_msg: @@ -665,16 +881,46 @@ class DuckHuntBot: self.send_message(channel, message) return - target = args[0].lower() - player = self.db.get_player(target) - if player is None: - player = {} - player['gun_confiscated'] = True + target_nick = args[0] + + # Validate target player (only for channel messages, skip validation if targeting self) + player = None + if not is_private_msg: + # If targeting self, skip validation since the user is obviously in the channel + if target_nick.lower() == nick.lower(): + target_nick = target_nick.lower() + player = self.db.get_player(target_nick) + if player is None: + player = self.db.create_player(target_nick) + self.db.players[target_nick] = player + else: + is_valid, player, error_msg = self.validate_target_player(target_nick, channel) + if not is_valid: + message = f"{nick} > {error_msg}" + self.send_message(channel, message) + return + # Ensure player is properly stored in database + target_nick = target_nick.lower() + if target_nick not in self.db.players: + self.db.players[target_nick] = player + else: + # For private messages, allow targeting any nick (admin override) + target_nick = target_nick.lower() + player = self.db.get_player(target_nick) + if player is None: + # Create new player data for the target + player = self.db.create_player(target_nick) + self.db.players[target_nick] = player + + # At this point player is guaranteed to be not None + if player is not None: + player['gun_confiscated'] = True + # Player data is already modified in place and will be saved by save_database() if is_private_msg: - message = f"{nick} > Disarmed {target}" + message = f"{nick} > Disarmed {target_nick}" else: - message = self.messages.get('admin_disarm', target=target, admin=nick) + message = self.messages.get('admin_disarm', target=target_nick, admin=nick) self.send_message(channel, message) self.db.save_database() diff --git a/src/game.py b/src/game.py index 06c3f5f..f58be43 100644 --- a/src/game.py +++ b/src/game.py @@ -175,6 +175,14 @@ class DuckGame: 'message_args': {'nick': nick} } + # Check if clothes are wet + if self._is_player_wet(player): + return { + 'success': False, + 'message_key': 'bang_wet_clothes', + 'message_args': {'nick': nick} + } + # Check ammo if player.get('current_ammo', 0) <= 0: return { @@ -197,8 +205,14 @@ class DuckGame: # Check for duck if channel not in self.ducks or not self.ducks[channel]: - # Wild shot - gun confiscated - player['current_ammo'] = player.get('current_ammo', 1) - 1 + # Wild shot - gun confiscated for unsafe shooting + player['shots_fired'] = player.get('shots_fired', 0) + 1 # Track wild shots too + player['shots_missed'] = player.get('shots_missed', 0) + 1 # Wild shots count as misses + # Use ammo for the shot, then store remaining ammo before confiscation + remaining_ammo = player.get('current_ammo', 1) - 1 + player['confiscated_ammo'] = remaining_ammo + player['confiscated_magazines'] = player.get('magazines', 0) + player['current_ammo'] = 0 # No ammo while confiscated player['gun_confiscated'] = True self.db.save_database() return { @@ -209,6 +223,7 @@ class DuckGame: # Shoot at duck player['current_ammo'] = player.get('current_ammo', 1) - 1 + player['shots_fired'] = player.get('shots_fired', 0) + 1 # Track total shots fired # Calculate hit chance using level-modified accuracy modified_accuracy = self.bot.levels.get_modified_accuracy(player) hit_chance = modified_accuracy / 100.0 @@ -268,7 +283,7 @@ class DuckGame: self.bot.levels.update_player_magazines(player) # If config option enabled, rearm all disarmed players when duck is shot - if self.bot.get_config('rearm_on_duck_shot', False): + if self.bot.get_config('duck_spawning.rearm_on_duck_shot', False): self._rearm_all_disarmed_players() self.db.save_database() @@ -284,9 +299,67 @@ class DuckGame: } else: # Miss! Duck stays in the channel - accuracy_loss = self.bot.get_config('accuracy_loss_on_miss', 2) - min_accuracy = self.bot.get_config('min_accuracy', 10) - player['accuracy'] = max(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) - accuracy_loss, min_accuracy) + player['shots_missed'] = player.get('shots_missed', 0) + 1 # Track missed shots + accuracy_loss = self.bot.get_config('gameplay.accuracy_loss_on_miss', 2) + min_accuracy = self.bot.get_config('gameplay.min_accuracy', 10) + player['accuracy'] = max(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) - accuracy_loss, min_accuracy) + + # Check for friendly fire (chance to hit another hunter) + friendly_fire_chance = 0.15 # 15% chance of hitting another hunter on miss + if random.random() < friendly_fire_chance: + # Get other armed players in the same channel + armed_players = [] + for other_nick, other_player in self.db.players.items(): + if (other_nick.lower() != nick.lower() and + not other_player.get('gun_confiscated', False) and + other_player.get('current_ammo', 0) > 0): + armed_players.append((other_nick, other_player)) + + if armed_players: + # Hit a random armed hunter + victim_nick, victim_player = random.choice(armed_players) + + # Check if shooter has insurance protection + has_insurance = self._check_insurance_protection(player, 'friendly_fire') + + if has_insurance: + # Protected by insurance - no penalties + self.db.save_database() + return { + 'success': True, + 'hit': False, + 'friendly_fire': True, + 'victim': victim_nick, + 'message_key': 'bang_friendly_fire_insured', + 'message_args': { + 'nick': nick, + 'victim': victim_nick + } + } + else: + # Apply friendly fire penalties - gun confiscated for unsafe shooting + xp_loss = min(player.get('xp', 0) // 4, 25) # Lose 25% XP or max 25 XP + player['xp'] = max(0, player.get('xp', 0) - xp_loss) + # Store current ammo state before confiscation (no shot fired yet in friendly fire) + player['confiscated_ammo'] = player.get('current_ammo', 0) + player['confiscated_magazines'] = player.get('magazines', 0) + player['current_ammo'] = 0 # No ammo while confiscated + player['gun_confiscated'] = True + + self.db.save_database() + return { + 'success': True, + 'hit': False, + 'friendly_fire': True, + 'victim': victim_nick, + 'message_key': 'bang_friendly_fire_penalty', + 'message_args': { + 'nick': nick, + 'victim': victim_nick, + 'xp_lost': xp_loss + } + } + self.db.save_database() return { 'success': True, @@ -443,6 +516,35 @@ class DuckGame: self.logger.error(f"Error getting spawn multiplier: {e}") return 1.0 + def _is_player_wet(self, player): + """Check if player has wet clothes that prevent shooting""" + import time + current_time = time.time() + + effects = player.get('temporary_effects', []) + for effect in effects: + if (effect.get('type') == 'wet_clothes' and + effect.get('expires_at', 0) > current_time): + return True + return False + + def _check_insurance_protection(self, player, protection_type): + """Check if player has active insurance protection""" + import time + current_time = time.time() + + try: + effects = player.get('temporary_effects', []) + for effect in effects: + if (effect.get('type') == 'insurance' and + effect.get('protection') == protection_type and + effect.get('expires_at', 0) > current_time): + return True + return False + except Exception as e: + self.logger.error(f"Error checking insurance protection: {e}") + return False + def _clean_expired_effects(self): """Remove expired temporary effects from all players""" import time diff --git a/src/shop.py b/src/shop.py index f5e7626..9c0cf62 100644 --- a/src/shop.py +++ b/src/shop.py @@ -105,10 +105,42 @@ class ShopManager: player['xp'] = player_xp - item['price'] if store_in_inventory: - # Add to inventory + # Add to inventory with bounds checking inventory = player.get('inventory', {}) item_id_str = str(item_id) current_count = inventory.get(item_id_str, 0) + + # Load inventory limits from config + config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config.json') + max_per_item = 99 # Default limit per item type + max_total_items = 20 # Default total items limit + try: + with open(config_path, 'r') as f: + config = json.load(f) + max_total_items = config.get('gameplay', {}).get('max_inventory_items', 20) + max_per_item = config.get('gameplay', {}).get('max_per_item_type', 99) + except: + pass # Use defaults + + # Check individual item limit + if current_count >= max_per_item: + return { + "success": False, + "error": "item_limit_reached", + "message": f"Cannot hold more than {max_per_item} {item['name']}s", + "item_name": item['name'] + } + + # Check total inventory size limit + total_items = sum(inventory.values()) + if total_items >= max_total_items: + return { + "success": False, + "error": "inventory_full", + "message": f"Inventory full! (max {max_total_items} items)", + "item_name": item['name'] + } + inventory[item_id_str] = current_count + 1 player['inventory'] = inventory @@ -190,11 +222,11 @@ class ShopManager: 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 + new_luck = min(max(current_luck + amount, -50), 100) # Bounded between -50 and +100 player['luck_bonus'] = new_luck return { "type": "luck", - "added": amount, + "added": new_luck - current_luck, "new_total": new_luck } @@ -316,7 +348,7 @@ class ShopManager: elif item_type == 'clean_gun': # Clean gun to reduce jamming chance (positive amount reduces jam chance) current_jam = player.get('jam_chance', 5) # Default 5% jam chance - new_jam = max(current_jam + amount, 0) # amount is negative for cleaning + new_jam = min(max(current_jam + amount, 0), 100) # Bounded between 0% and 100% player['jam_chance'] = new_jam return { @@ -346,10 +378,109 @@ class ShopManager: "duration": duration // 60 # return duration in minutes } + elif item_type == 'insurance': + # Add insurance protection against friendly fire + if 'temporary_effects' not in player: + player['temporary_effects'] = [] + + duration = item.get('duration', 86400) # 24 hours default + protection_type = item.get('protection', 'friendly_fire') + + effect = { + 'type': 'insurance', + 'protection': protection_type, + 'expires_at': time.time() + duration, + 'name': 'Hunter\'s Insurance' + } + player['temporary_effects'].append(effect) + + return { + "type": "insurance", + "protection": protection_type, + "duration": duration // 3600 # return duration in hours + } + + elif item_type == 'buy_gun_back': + # Restore confiscated gun with original ammo + was_confiscated = player.get('gun_confiscated', False) + + if was_confiscated: + player['gun_confiscated'] = False + # Restore original ammo and magazines from when gun was confiscated + restored_ammo = player.get('confiscated_ammo', 0) + restored_magazines = player.get('confiscated_magazines', 1) + player['current_ammo'] = restored_ammo + player['magazines'] = restored_magazines + # Clean up the stored values + player.pop('confiscated_ammo', None) + player.pop('confiscated_magazines', None) + + return { + "type": "buy_gun_back", + "restored": True, + "ammo_restored": restored_ammo + } + else: + return { + "type": "buy_gun_back", + "restored": False, + "message": "Your gun is not confiscated" + } + + + + elif item_type == 'dry_clothes': + # Remove wet clothes effect + + # Remove any wet clothes effects + if 'temporary_effects' in player: + original_count = len(player['temporary_effects']) + player['temporary_effects'] = [ + effect for effect in player['temporary_effects'] + if effect.get('type') != 'wet_clothes' + ] + new_count = len(player['temporary_effects']) + was_wet = original_count > new_count + else: + was_wet = False + + return { + "type": "dry_clothes", + "was_wet": was_wet, + "message": "You changed into dry clothes!" if was_wet else "You weren't wet!" + } + else: self.logger.warning(f"Unknown item type: {item_type}") return {"type": "unknown", "message": f"Unknown effect type: {item_type}"} + def _apply_splash_water_effect(self, target_player: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]: + """Apply splash water effect to target player""" + # Load config directly without import issues + config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config.json') + try: + with open(config_path, 'r') as f: + config = json.load(f) + wet_duration = config.get('gameplay', {}).get('wet_clothes_duration', 300) # 5 minutes default + except: + wet_duration = 300 # Default 5 minutes + + if 'temporary_effects' not in target_player: + target_player['temporary_effects'] = [] + + # Add wet clothes effect + wet_effect = { + 'type': 'wet_clothes', + 'expires_at': time.time() + wet_duration + } + target_player['temporary_effects'].append(wet_effect) + + return { + "type": "splash_water", + "target_soaked": True, + "duration": wet_duration // 60 # return duration in minutes + } + def use_inventory_item(self, player: Dict[str, Any], item_id: int, target_player: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Use an item from player's inventory @@ -370,12 +501,22 @@ class ShopManager: "item_name": item['name'] } - # Check if item requires a target - if item.get('target_required', False) and not target_player: + # Special restrictions: Some items require targets, bread cannot have targets + if item['type'] == 'attract_ducks' and target_player: + return { + "success": False, + "error": "bread_no_target", + "message": "Bread affects everyone in the channel - you cannot target a specific player", + "item_name": item['name'] + } + + # Items that must have targets when used (but can be stored in inventory) + target_required_items = ['sabotage_jam', 'splash_water'] + if item['type'] in target_required_items and not target_player: return { "success": False, "error": "target_required", - "message": f"{item['name']} requires a target player", + "message": f"{item['name']} requires a target player to use", "item_name": item['name'] } @@ -385,19 +526,31 @@ class ShopManager: del inventory[item_id_str] player['inventory'] = inventory - # Apply effect - if item.get('target_required', False) and target_player: - effect_result = self._apply_item_effect(target_player, item) + # Determine who gets the effect + if target_player: + # Special handling for harmful effects + if item['type'] == 'splash_water': + effect_result = self._apply_splash_water_effect(target_player, item) + target_affected = True + elif item['type'] == 'sabotage_jam': + effect_result = self._apply_item_effect(target_player, item) + target_affected = True + else: + # Beneficial items - give to target (gifting) + effect_result = self._apply_item_effect(target_player, item) + target_affected = True + # Mark as gift in the result + effect_result['is_gift'] = True return { "success": True, "item_name": item['name'], "effect": effect_result, - "target_affected": True, + "target_affected": target_affected, "remaining_in_inventory": inventory.get(item_id_str, 0) } else: - # Apply effect to user + # Apply effect to user (no target specified) effect_result = self._apply_item_effect(player, item) return { diff --git a/test_config.py b/test_config.py deleted file mode 100644 index 912fdd9..0000000 --- a/test_config.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os - -def test_config(): - """Test the config structure and values""" - config_path = 'config.json' - - if not os.path.exists(config_path): - print("โŒ Config file not found") - return - - with open(config_path, 'r') as f: - config = json.load(f) - - print("๐Ÿงช Testing configuration structure...") - - # Test connection settings - connection = config.get('connection', {}) - print(f"๐ŸŒ Server: {connection.get('server')}") - print(f"๐Ÿ”Œ Port: {connection.get('port')}") - print(f"๐Ÿค– Nick: {connection.get('nick')}") - print(f"๐Ÿ”’ SSL: {connection.get('ssl')}") - - # Check if password is set (not default) - password = connection.get('password') - password_set = password and password != "your_iline_password_here" - print(f"๐Ÿ”‘ Password configured: {'Yes' if password_set else 'No (default placeholder)'}") - - # Test SASL settings - sasl = config.get('sasl', {}) - print(f"๐Ÿ” SASL enabled: {sasl.get('enabled')}") - print(f"๐Ÿ‘ค SASL username: {sasl.get('username')}") - - # Check if SASL password is set (not default) - sasl_password = sasl.get('password') - sasl_password_set = sasl_password and sasl_password != "duckhunt//789//" - print(f"๐Ÿ—๏ธ SASL password configured: {'Yes' if sasl_password_set else 'No (default placeholder)'}") - - # Test channels - channels = connection.get('channels', []) - print(f"๐Ÿ“บ Channels to join: {channels}") - - print("\nโœ… Configuration structure looks good!") - - if not password_set: - print("โš ๏ธ Warning: Server password is still set to placeholder value") - print(" Update 'connection.password' if your server requires authentication") - - if sasl.get('enabled') and not sasl_password_set: - print("โš ๏ธ Warning: SASL is enabled but password is still placeholder") - print(" Update 'sasl.password' with your actual NickServ password") - -if __name__ == "__main__": - test_config() \ No newline at end of file