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
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
src/db.py
10
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
114
src/game.py
114
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
|
||||
|
||||
177
src/shop.py
177
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 {
|
||||
|
||||
Reference in New Issue
Block a user