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:
2025-09-26 19:06:26 +01:00
parent 5484548c30
commit f3a9c5b611
21 changed files with 1162 additions and 256 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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 {