677 lines
29 KiB
Python
677 lines
29 KiB
Python
"""
|
|
Game mechanics for DuckHunt Bot
|
|
Handles duck spawning, shooting, befriending, and other game actions
|
|
"""
|
|
|
|
import asyncio
|
|
import random
|
|
import time
|
|
import logging
|
|
|
|
|
|
class DuckGame:
|
|
"""Game mechanics for DuckHunt - shooting, befriending, reloading"""
|
|
|
|
def __init__(self, bot, db):
|
|
self.bot = bot
|
|
self.db = db
|
|
self.ducks = {} # {channel: [duck1, duck2, ...]}
|
|
self.logger = logging.getLogger('DuckHuntBot.Game')
|
|
self.spawn_task = None
|
|
self.timeout_task = None
|
|
|
|
async def start_game_loops(self):
|
|
"""Start the game loops"""
|
|
self.spawn_task = asyncio.create_task(self.duck_spawn_loop())
|
|
self.timeout_task = asyncio.create_task(self.duck_timeout_loop())
|
|
|
|
try:
|
|
await asyncio.gather(self.spawn_task, self.timeout_task)
|
|
except asyncio.CancelledError:
|
|
self.logger.info("Game loops cancelled")
|
|
|
|
async def duck_spawn_loop(self):
|
|
"""Duck spawning loop with responsive shutdown"""
|
|
try:
|
|
while True:
|
|
# Wait random time between spawns, but in small chunks for responsiveness
|
|
min_wait = self.bot.get_config('duck_spawning.spawn_min', 300) # 5 minutes
|
|
max_wait = self.bot.get_config('duck_spawning.spawn_max', 900) # 15 minutes
|
|
|
|
# Check for active bread effects to modify spawn timing
|
|
spawn_multiplier = self._get_active_spawn_multiplier()
|
|
if spawn_multiplier > 1.0:
|
|
# Reduce wait time when bread is active
|
|
min_wait = int(min_wait / spawn_multiplier)
|
|
max_wait = int(max_wait / spawn_multiplier)
|
|
|
|
wait_time = random.randint(min_wait, max_wait)
|
|
|
|
# Sleep in 1-second intervals to allow for quick cancellation
|
|
for _ in range(wait_time):
|
|
await asyncio.sleep(1)
|
|
|
|
# Spawn duck in random channel
|
|
channels = list(self.bot.channels_joined)
|
|
if channels:
|
|
channel = random.choice(channels)
|
|
await self.spawn_duck(channel)
|
|
|
|
except asyncio.CancelledError:
|
|
self.logger.info("Duck spawning loop cancelled")
|
|
|
|
async def duck_timeout_loop(self):
|
|
"""Duck timeout loop with responsive shutdown"""
|
|
try:
|
|
while True:
|
|
# Check every 2 seconds instead of 10 for more responsiveness
|
|
await asyncio.sleep(2)
|
|
|
|
current_time = time.time()
|
|
channels_to_clear = []
|
|
|
|
for channel, ducks in self.ducks.items():
|
|
ducks_to_remove = []
|
|
for duck in ducks:
|
|
# Get timeout for each duck type from config
|
|
duck_type = duck.get('duck_type', 'normal')
|
|
timeout = self.bot.get_config(f'duck_types.{duck_type}.timeout', 60)
|
|
|
|
if current_time - duck['spawn_time'] > timeout:
|
|
ducks_to_remove.append(duck)
|
|
|
|
for duck in ducks_to_remove:
|
|
ducks.remove(duck)
|
|
# Use appropriate fly away message based on duck type - revealing the type!
|
|
duck_type = duck.get('duck_type', 'normal')
|
|
if duck_type == 'golden':
|
|
message = self.bot.messages.get('golden_duck_flies_away')
|
|
elif duck_type == 'fast':
|
|
message = self.bot.messages.get('fast_duck_flies_away')
|
|
else:
|
|
message = self.bot.messages.get('duck_flies_away')
|
|
self.bot.send_message(channel, message)
|
|
|
|
if not ducks:
|
|
channels_to_clear.append(channel)
|
|
|
|
# Clean up empty channels
|
|
for channel in channels_to_clear:
|
|
if channel in self.ducks and not self.ducks[channel]:
|
|
del self.ducks[channel]
|
|
|
|
# Clean expired effects every loop iteration
|
|
self._clean_expired_effects()
|
|
|
|
except asyncio.CancelledError:
|
|
self.logger.info("Duck timeout loop cancelled")
|
|
|
|
async def spawn_duck(self, channel):
|
|
"""Spawn a duck in the channel"""
|
|
if channel not in self.ducks:
|
|
self.ducks[channel] = []
|
|
|
|
# Don't spawn if there's already a duck
|
|
if self.ducks[channel]:
|
|
return
|
|
|
|
# Determine duck type randomly
|
|
golden_chance = self.bot.get_config('golden_duck_chance', 0.15)
|
|
fast_chance = self.bot.get_config('fast_duck_chance', 0.25)
|
|
|
|
rand = random.random()
|
|
if rand < golden_chance:
|
|
# Golden duck - high HP, high XP
|
|
min_hp = self.bot.get_config('golden_duck_min_hp', 3)
|
|
max_hp = self.bot.get_config('golden_duck_max_hp', 5)
|
|
hp = random.randint(min_hp, max_hp)
|
|
duck_type = 'golden'
|
|
duck = {
|
|
'id': f"golden_duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
|
'spawn_time': time.time(),
|
|
'channel': channel,
|
|
'duck_type': duck_type,
|
|
'max_hp': hp,
|
|
'current_hp': hp
|
|
}
|
|
self.logger.info(f"Golden duck (hidden) spawned in {channel} with {hp} HP")
|
|
elif rand < golden_chance + fast_chance:
|
|
# Fast duck - normal HP, flies away faster
|
|
duck_type = 'fast'
|
|
duck = {
|
|
'id': f"fast_duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
|
'spawn_time': time.time(),
|
|
'channel': channel,
|
|
'duck_type': duck_type,
|
|
'max_hp': 1,
|
|
'current_hp': 1
|
|
}
|
|
self.logger.info(f"Fast duck (hidden) spawned in {channel}")
|
|
else:
|
|
# Normal duck
|
|
duck_type = 'normal'
|
|
duck = {
|
|
'id': f"duck_{int(time.time())}_{random.randint(1000, 9999)}",
|
|
'spawn_time': time.time(),
|
|
'channel': channel,
|
|
'duck_type': duck_type,
|
|
'max_hp': 1,
|
|
'current_hp': 1
|
|
}
|
|
self.logger.info(f"Normal duck spawned in {channel}")
|
|
|
|
# All duck types use the same spawn message - type is hidden!
|
|
message = self.bot.messages.get('duck_spawn')
|
|
self.ducks[channel].append(duck)
|
|
self.bot.send_message(channel, message)
|
|
|
|
def shoot_duck(self, nick, channel, player):
|
|
"""Handle shooting at a duck"""
|
|
# Check if gun is confiscated
|
|
if player.get('gun_confiscated', False):
|
|
return {
|
|
'success': False,
|
|
'message_key': 'bang_not_armed',
|
|
'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 {
|
|
'success': False,
|
|
'message_key': 'bang_no_ammo',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# Check for gun jamming using level-based jam chance
|
|
jam_chance = self.bot.levels.get_jam_chance(player) / 100.0 # Convert percentage to decimal
|
|
if random.random() < jam_chance:
|
|
# Gun jammed! Use ammo but don't shoot
|
|
player['current_ammo'] = player.get('current_ammo', 1) - 1
|
|
self.db.save_database()
|
|
return {
|
|
'success': False,
|
|
'message_key': 'bang_gun_jammed',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# Check for duck
|
|
if channel not in self.ducks or not self.ducks[channel]:
|
|
# 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 {
|
|
'success': False,
|
|
'message_key': 'bang_no_duck',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# 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
|
|
|
|
# Apply clover luck effect (temporary boost to minimum hit chance)
|
|
clover = self._get_active_effect(player, 'clover_luck')
|
|
if clover:
|
|
try:
|
|
min_hit = float(clover.get('min_hit_chance', 0.0) or 0.0)
|
|
except (ValueError, TypeError):
|
|
min_hit = 0.0
|
|
hit_chance = max(hit_chance, max(0.0, min(min_hit, 1.0)))
|
|
|
|
if random.random() < hit_chance:
|
|
# Hit! Get the duck and reveal its type
|
|
duck = self.ducks[channel][0]
|
|
duck_type = duck.get('duck_type', 'normal')
|
|
|
|
if duck_type == 'golden':
|
|
# Golden duck - multi-hit with high XP
|
|
duck['current_hp'] -= 1
|
|
xp_gained = self.bot.get_config('golden_duck_xp', 15)
|
|
|
|
if duck['current_hp'] > 0:
|
|
# Still alive, reveal it's golden but don't remove
|
|
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
|
|
max_accuracy = self.bot.get_config('max_accuracy', 100)
|
|
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
|
|
self.db.save_database()
|
|
return {
|
|
'success': True,
|
|
'hit': True,
|
|
'message_key': 'bang_hit_golden',
|
|
'message_args': {
|
|
'nick': nick,
|
|
'hp_remaining': duck['current_hp'],
|
|
'xp_gained': xp_gained
|
|
}
|
|
}
|
|
else:
|
|
# Golden duck killed!
|
|
self.ducks[channel].pop(0)
|
|
xp_gained = xp_gained * duck['max_hp'] # Bonus XP for killing
|
|
message_key = 'bang_hit_golden_killed'
|
|
elif duck_type == 'fast':
|
|
# Fast duck - normal HP but higher XP
|
|
self.ducks[channel].pop(0)
|
|
xp_gained = self.bot.get_config('fast_duck_xp', 12)
|
|
message_key = 'bang_hit_fast'
|
|
else:
|
|
# Normal duck
|
|
self.ducks[channel].pop(0)
|
|
xp_gained = self.bot.get_config('normal_duck_xp', 10)
|
|
message_key = 'bang_hit'
|
|
|
|
# Apply XP and level changes
|
|
old_level = self.bot.levels.calculate_player_level(player)
|
|
player['xp'] = player.get('xp', 0) + xp_gained
|
|
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
|
|
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
|
|
max_accuracy = self.bot.get_config('max_accuracy', 100)
|
|
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
|
|
|
|
# Check if player leveled up and update magazines if needed
|
|
new_level = self.bot.levels.calculate_player_level(player)
|
|
if new_level != old_level:
|
|
self.bot.levels.update_player_magazines(player)
|
|
|
|
# If config option enabled, rearm all disarmed players when duck is shot
|
|
if self.bot.get_config('duck_spawning.rearm_on_duck_shot', False):
|
|
self._rearm_all_disarmed_players(channel)
|
|
|
|
# Check for item drops
|
|
dropped_item = self._check_item_drop(player, duck_type)
|
|
|
|
self.db.save_database()
|
|
|
|
# Include drop info in the return
|
|
result = {
|
|
'success': True,
|
|
'hit': True,
|
|
'message_key': message_key,
|
|
'message_args': {
|
|
'nick': nick,
|
|
'xp_gained': xp_gained,
|
|
'ducks_shot': player['ducks_shot']
|
|
}
|
|
}
|
|
|
|
# Add drop info if an item was dropped
|
|
if dropped_item:
|
|
result['dropped_item'] = dropped_item
|
|
|
|
return result
|
|
else:
|
|
# Miss! Duck stays in the channel
|
|
player['shots_missed'] = player.get('shots_missed', 0) + 1 # Track missed shots
|
|
|
|
# Lose 1 XP for missing
|
|
player['xp'] = max(0, player.get('xp', 0) - 1)
|
|
|
|
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.get_players_for_channel(channel).items():
|
|
if (str(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,
|
|
'hit': False,
|
|
'message_key': 'bang_miss',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
def befriend_duck(self, nick, channel, player):
|
|
"""Handle befriending a duck"""
|
|
# Check for duck
|
|
if channel not in self.ducks or not self.ducks[channel]:
|
|
return {
|
|
'success': False,
|
|
'message_key': 'bef_no_duck',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# Check befriend success rate from config and level modifiers
|
|
base_rate = self.bot.get_config('gameplay.befriend_success_rate', 75)
|
|
try:
|
|
if base_rate is not None:
|
|
base_rate = float(base_rate)
|
|
else:
|
|
base_rate = 75.0
|
|
except (ValueError, TypeError):
|
|
base_rate = 75.0
|
|
|
|
# Apply level-based modification to befriend rate
|
|
level_modified_rate = self.bot.levels.get_modified_befriend_rate(player, base_rate)
|
|
success_rate = level_modified_rate / 100.0
|
|
|
|
# Apply clover luck effect (temporary boost to minimum befriend chance)
|
|
clover = self._get_active_effect(player, 'clover_luck')
|
|
if clover:
|
|
try:
|
|
min_bef = float(clover.get('min_befriend_chance', 0.0) or 0.0)
|
|
except (ValueError, TypeError):
|
|
min_bef = 0.0
|
|
success_rate = max(success_rate, max(0.0, min(min_bef, 1.0)))
|
|
|
|
if random.random() < success_rate:
|
|
# Success - befriend the duck
|
|
duck = self.ducks[channel].pop(0)
|
|
|
|
# Lower XP gain than shooting
|
|
xp_gained = self.bot.get_config('gameplay.befriend_xp', 5)
|
|
old_level = self.bot.levels.calculate_player_level(player)
|
|
player['xp'] = player.get('xp', 0) + xp_gained
|
|
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
|
|
|
|
# Check if player leveled up and update magazines if needed
|
|
new_level = self.bot.levels.calculate_player_level(player)
|
|
if new_level != old_level:
|
|
self.bot.levels.update_player_magazines(player)
|
|
|
|
# If config option enabled, rearm all disarmed players when duck is befriended
|
|
if self.bot.get_config('rearm_on_duck_shot', False):
|
|
self._rearm_all_disarmed_players(channel)
|
|
|
|
self.db.save_database()
|
|
return {
|
|
'success': True,
|
|
'befriended': True,
|
|
'message_key': 'bef_success',
|
|
'message_args': {
|
|
'nick': nick,
|
|
'xp_gained': xp_gained,
|
|
'ducks_befriended': player['ducks_befriended']
|
|
}
|
|
}
|
|
else:
|
|
# Failure - duck flies away, remove from channel
|
|
duck = self.ducks[channel].pop(0)
|
|
|
|
self.db.save_database()
|
|
return {
|
|
'success': True,
|
|
'befriended': False,
|
|
'message_key': 'bef_failed',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
def reload_gun(self, nick, channel, player):
|
|
"""Handle reloading a gun (switching to a new magazine)"""
|
|
if player.get('gun_confiscated', False):
|
|
return {
|
|
'success': False,
|
|
'message_key': 'reload_not_armed',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
current_ammo = player.get('current_ammo', 0)
|
|
bullets_per_mag = player.get('bullets_per_magazine', 6)
|
|
|
|
# Check if current magazine is already full
|
|
if current_ammo >= bullets_per_mag:
|
|
return {
|
|
'success': False,
|
|
'message_key': 'reload_already_loaded',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# Check if they have spare magazines
|
|
total_magazines = player.get('magazines', 1)
|
|
if total_magazines <= 1: # Only the current magazine
|
|
return {
|
|
'success': False,
|
|
'message_key': 'reload_no_chargers',
|
|
'message_args': {'nick': nick}
|
|
}
|
|
|
|
# Reload: discard current magazine and load a new full one
|
|
player['current_ammo'] = bullets_per_mag
|
|
player['magazines'] = total_magazines - 1
|
|
|
|
self.db.save_database()
|
|
return {
|
|
'success': True,
|
|
'message_key': 'reload_success',
|
|
'message_args': {
|
|
'nick': nick,
|
|
'ammo': player['current_ammo'],
|
|
'max_ammo': bullets_per_mag,
|
|
'chargers': player['magazines'] - 1 # Spare magazines (excluding current)
|
|
}
|
|
}
|
|
|
|
def _rearm_all_disarmed_players(self, channel):
|
|
"""Rearm all players who have been disarmed (gun confiscated) in the given channel"""
|
|
try:
|
|
rearmed_count = 0
|
|
for _player_name, player_data in self.db.get_players_for_channel(channel).items():
|
|
if player_data.get('gun_confiscated', False):
|
|
player_data['gun_confiscated'] = False
|
|
# Update magazines based on player level
|
|
self.bot.levels.update_player_magazines(player_data)
|
|
player_data['current_ammo'] = player_data.get('bullets_per_magazine', 6)
|
|
rearmed_count += 1
|
|
|
|
if rearmed_count > 0:
|
|
self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot")
|
|
except Exception as e:
|
|
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
|
|
|
|
def _get_active_spawn_multiplier(self):
|
|
"""Get the current spawn rate multiplier from active bread effects"""
|
|
import time
|
|
max_multiplier = 1.0
|
|
current_time = time.time()
|
|
|
|
try:
|
|
for _ch, _player_name, player_data in self.db.iter_all_players():
|
|
effects = player_data.get('temporary_effects', [])
|
|
for effect in effects:
|
|
if (effect.get('type') == 'attract_ducks' and
|
|
effect.get('expires_at', 0) > current_time):
|
|
multiplier = effect.get('spawn_multiplier', 1.0)
|
|
max_multiplier = max(max_multiplier, multiplier)
|
|
|
|
return max_multiplier
|
|
except Exception as e:
|
|
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
|
|
current_time = time.time()
|
|
|
|
try:
|
|
for _ch, player_name, player_data in self.db.iter_all_players():
|
|
effects = player_data.get('temporary_effects', [])
|
|
active_effects = []
|
|
|
|
for effect in effects:
|
|
if effect.get('expires_at', 0) > current_time:
|
|
active_effects.append(effect)
|
|
|
|
if len(active_effects) != len(effects):
|
|
player_data['temporary_effects'] = active_effects
|
|
self.logger.debug(f"Cleaned expired effects for {player_name}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error cleaning expired effects: {e}")
|
|
|
|
def _get_active_effect(self, player, effect_type: str):
|
|
"""Return the first active temporary effect dict matching type, or None."""
|
|
try:
|
|
current_time = time.time()
|
|
effects = player.get('temporary_effects', [])
|
|
if not isinstance(effects, list):
|
|
return None
|
|
for effect in effects:
|
|
if (isinstance(effect, dict) and effect.get('type') == effect_type and
|
|
effect.get('expires_at', 0) > current_time):
|
|
return effect
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
def _check_item_drop(self, player, duck_type):
|
|
"""
|
|
Check if the duck drops an item and add it to player's inventory
|
|
Returns the dropped item info or None
|
|
"""
|
|
import random
|
|
|
|
try:
|
|
# Get drop chance for this duck type
|
|
drop_chance = self.bot.get_config(f'duck_types.{duck_type}.drop_chance', 0.0)
|
|
|
|
# Roll for drop
|
|
if random.random() > drop_chance:
|
|
return None # No drop
|
|
|
|
# Get drop table for this duck type
|
|
drop_table_key = f'{duck_type}_duck_drops'
|
|
drop_table = self.bot.get_config(f'item_drops.{drop_table_key}', [])
|
|
|
|
if not drop_table:
|
|
self.logger.warning(f"No drop table found for {duck_type} duck")
|
|
return None
|
|
|
|
# Weighted random selection
|
|
total_weight = sum(item.get('weight', 1) for item in drop_table)
|
|
if total_weight <= 0:
|
|
return None
|
|
|
|
random_weight = random.randint(1, total_weight)
|
|
current_weight = 0
|
|
|
|
for drop_item in drop_table:
|
|
current_weight += drop_item.get('weight', 1)
|
|
if random_weight <= current_weight:
|
|
item_id = drop_item.get('item_id')
|
|
if item_id:
|
|
# Add item to player's inventory
|
|
inventory = player.get('inventory', {})
|
|
item_key = str(item_id)
|
|
inventory[item_key] = inventory.get(item_key, 0) + 1
|
|
player['inventory'] = inventory
|
|
|
|
# Get item info from shop
|
|
item_info = self.bot.shop.get_item(item_id)
|
|
item_name = item_info.get('name', f'Item {item_id}') if item_info else f'Item {item_id}'
|
|
|
|
self.logger.info(f"Duck dropped {item_name} for player {player.get('nick', 'Unknown')}")
|
|
|
|
return {
|
|
'item_id': item_id,
|
|
'item_name': item_name,
|
|
'duck_type': duck_type
|
|
}
|
|
break
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in _check_item_drop: {e}")
|
|
return None |