From dd06c9377f9d657209c0a661439c810384f9ad71 Mon Sep 17 00:00:00 2001 From: 3nd3r Date: Sun, 28 Dec 2025 16:48:56 -0600 Subject: [PATCH] Revert to simple version: Remove new ducks and items - Removed new duck types (concrete, diamond, holy_grail, explosive, poisonous, etc.) - Removed new shop items (sniper rifle, duck radar, bread, splash water, etc.) - Removed status effects system (eliminated, poisoned, wet, etc.) - Removed item drops and temporary effects - Kept only original 3 duck types: normal, golden, fast - Kept original simple shop - KEPT BUG FIX: Golden duck XP now awarded on each hit - KEPT BUG FIX: Message sanitization preserves IRC codes - Simpler, more stable bot with core improvements --- messages.json | 16 -- shop.json | 50 ------ src/game.py | 412 ++++++++++++-------------------------------------- 3 files changed, 98 insertions(+), 380 deletions(-) diff --git a/messages.json b/messages.json index a45c883..68aa721 100644 --- a/messages.json +++ b/messages.json @@ -81,22 +81,6 @@ "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.", - "use_perfect_aim": "{nick} > You line up the perfect shot. {green}[Perfect aim for {duration_minutes} minutes]{reset}", - "use_duck_radar": "{nick} > Duck Radar online. I'll DM you when a duck spawns here. {green}[{duration_hours} hours]{reset}", - "use_summon_duck": "{nick} > You call out to the pond... a duck should show up in {channel}.", - "use_summon_duck_delayed": "{nick} > You set a decoy. A duck should show up in {channel} in about {delay_minutes} minutes.", - "radar_alert": "Duck Radar: A duck has spawned in {channel}!", - - "bang_hit_concrete": "{nick} > {red}*BANG*{reset} You hit a CONCRETE DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_concrete_killed": "{nick} > {red}*BANG*{reset} You shattered the CONCRETE DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_holy_grail": "{nick} > {red}*BANG*{reset} You hit the HOLY GRAIL DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_holy_grail_killed": "{nick} > {red}*BANG*{reset} You claimed the HOLY GRAIL DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_diamond": "{nick} > {red}*BANG*{reset} You hit a DIAMOND DUCK! [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_diamond_killed": "{nick} > {red}*BANG*{reset} You bagged the DIAMOND DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bang_hit_explosive": "{nick} > {red}*BANG*{reset} You shot an EXPLOSIVE DUCK! {red}[BOOM - eliminated for 2 hours]{reset} {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]", - "bef_poisoned": "{nick} > You befriended the duck... but it was poisonous! {red}[Poisoned for {duration_hours} hours]{reset}", - "player_eliminated": "{nick} > You're eliminated and can't hunt right now.", - "player_poisoned": "{nick} > You're poisoned and can't hunt right now.", "gift_success_generic": "{nick} > Successfully gave {item_name} to {target_nick}!", "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.", diff --git a/shop.json b/shop.json index 6299125..d226d05 100644 --- a/shop.json +++ b/shop.json @@ -61,56 +61,6 @@ "price": 30, "description": "Change into dry clothes - allows shooting again after being soaked", "type": "dry_clothes" - }, - - "10": { - "name": "Sniper Rifle", - "price": 200, - "description": "Perfect aim - your shots won't miss for 30 minutes", - "type": "perfect_aim", - "duration": 1800 - }, - "11": { - "name": "Sniper Scope", - "price": 350, - "description": "Perfect aim - your shots won't miss for 60 minutes", - "type": "perfect_aim", - "duration": 3600 - }, - "12": { - "name": "Duck Whistle", - "price": 120, - "description": "Instantly summons a duck (if none are present)", - "type": "summon_duck", - "delay": 0 - }, - "13": { - "name": "Duck Caller", - "price": 200, - "description": "Instantly summons a duck (if none are present)", - "type": "summon_duck", - "delay": 0 - }, - "14": { - "name": "Duck Horn", - "price": 300, - "description": "Instantly summons a duck (if none are present)", - "type": "summon_duck", - "delay": 0 - }, - "15": { - "name": "Duck Decoy", - "price": 80, - "description": "Summons a duck in 1 hour (if none are present)", - "type": "summon_duck", - "delay": 3600 - }, - "16": { - "name": "Duck Radar", - "price": 150, - "description": "DM alert when a duck spawns in this channel (lasts 6 hours)", - "type": "duck_radar", - "duration": 21600 } } } \ No newline at end of file diff --git a/src/game.py b/src/game.py index 096a80c..f618fd2 100644 --- a/src/game.py +++ b/src/game.py @@ -19,17 +19,6 @@ class DuckGame: self.logger = logging.getLogger('DuckHuntBot.Game') self.spawn_task = None self.timeout_task = None - - def _get_effects_list(self, player: dict): - """Return a sanitized list of temporary effect dicts for a player.""" - effects = player.get('temporary_effects', []) - if not isinstance(effects, list): - player['temporary_effects'] = [] - return [] - cleaned = [e for e in effects if isinstance(e, dict)] - if len(cleaned) != len(effects): - player['temporary_effects'] = cleaned - return cleaned async def start_game_loops(self): """Start the game loops""" @@ -45,20 +34,12 @@ class DuckGame: """Duck spawning loop with responsive shutdown""" try: while True: - # Pick a target channel first so spawn multipliers are per-channel - channels = list(self.bot.channels_joined) - if not channels: - await asyncio.sleep(1) - continue - - channel = random.choice(channels) - # 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(channel) + 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) @@ -70,8 +51,10 @@ class DuckGame: for _ in range(wait_time): await asyncio.sleep(1) - # Spawn duck in the chosen channel (if still joined) - if channel in self.bot.channels_joined: + # 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: @@ -128,164 +111,62 @@ class DuckGame: if channel not in self.ducks: self.ducks[channel] = [] - # Don't spawn if there are already ducks present + # Don't spawn if there's already a duck if self.ducks[channel]: return - - duck_type = self._choose_duck_type() - - # Special spawns that create multiple normal ducks. - if duck_type in ('couple', 'family'): - count = 2 if duck_type == 'couple' else random.randint(3, 4) - for _ in range(count): - duck = self._create_duck(channel, 'normal') - self.ducks[channel].append(duck) - self.logger.info(f"{duck_type} spawned {count} ducks in {channel}") + + # 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: - duck = self._create_duck(channel, duck_type) - self.ducks[channel].append(duck) - hp = duck.get('max_hp', 1) - if duck_type != 'normal': - self.logger.info(f"{duck_type} duck (hidden) spawned in {channel} with {hp} HP") - else: - self.logger.info(f"Normal duck spawned in {channel}") - - # Notify players with Duck Radar - try: - for player_name, player_data in self.db.get_players_for_channel(channel).items(): - if self._has_active_effect(player_data, 'duck_radar'): - self.bot.send_message(player_name, self.bot.messages.get('radar_alert', channel=channel)) - except Exception: - pass + # 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') - if not self.bot.send_message(channel, message): - self.logger.warning(f"Failed to send duck spawn message to {channel}") - - async def force_spawn_duck(self, channel, duck_type): - """Force spawn a specific duck type in a channel (admin/items), even if ducks already exist.""" - if channel not in self.ducks: - self.ducks[channel] = [] - - duck_type = (duck_type or 'normal').lower() - - if duck_type in ('couple', 'family'): - count = 2 if duck_type == 'couple' else random.randint(3, 4) - for _ in range(count): - self.ducks[channel].append(self._create_duck(channel, 'normal')) - else: - self.ducks[channel].append(self._create_duck(channel, duck_type)) - - # Notify players with Duck Radar - try: - for player_name, player_data in self.db.get_players_for_channel(channel).items(): - if self._has_active_effect(player_data, 'duck_radar'): - self.bot.send_message(player_name, self.bot.messages.get('radar_alert', channel=channel)) - except Exception: - pass - - if not self.bot.send_message(channel, self.bot.messages.get('duck_spawn')): - self.logger.warning(f"Failed to send forced duck spawn message to {channel}") - - def _choose_duck_type(self): - """Choose a duck type using duck_types.*.chance (with legacy fallbacks).""" - try: - duck_types = self.bot.get_config('duck_types', {}) or {} - if not isinstance(duck_types, dict): - duck_types = {} - - weighted = [] - total = 0.0 - - for dtype, cfg in duck_types.items(): - if dtype == 'normal': - continue - chance = None - if isinstance(cfg, dict): - chance = cfg.get('chance') - - # Legacy fallbacks - if chance is None and dtype == 'golden': - chance = self.bot.get_config('golden_duck_chance', None) - if chance is None and dtype == 'fast': - chance = self.bot.get_config('fast_duck_chance', None) - - try: - chance = float(chance) - except (TypeError, ValueError): - chance = 0.0 - - if chance > 0: - weighted.append((dtype, chance)) - total += chance - - if total <= 0: - return 'normal' - - r = random.random() - if r >= min(1.0, total): - return 'normal' - - pick = random.random() * total - cumulative = 0.0 - for dtype, weight in weighted: - cumulative += weight - if pick <= cumulative: - return dtype - return weighted[-1][0] - except Exception: - return 'normal' - - def _create_duck(self, channel, duck_type): - """Create a duck dict for a given type.""" - cfg = self.bot.get_config(f'duck_types.{duck_type}', {}) or {} - if not isinstance(cfg, dict): - cfg = {} - - # Legacy golden HP keys - if duck_type == 'golden' and ('min_hp' not in cfg and 'max_hp' not in cfg): - cfg = dict(cfg) - cfg['min_hp'] = self.bot.get_config('golden_duck_min_hp', 3) - cfg['max_hp'] = self.bot.get_config('golden_duck_max_hp', 5) - - min_hp = cfg.get('min_hp', cfg.get('hp', 1)) - max_hp = cfg.get('max_hp', cfg.get('hp', 1)) - try: - min_hp = int(min_hp) - max_hp = int(max_hp) - except (TypeError, ValueError): - min_hp = 1 - max_hp = 1 - min_hp = max(1, min_hp) - max_hp = max(min_hp, max_hp) - hp = random.randint(min_hp, max_hp) - - return { - 'id': f"{duck_type}_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.ducks[channel].append(duck) + self.bot.send_message(channel, message) def shoot_duck(self, nick, channel, player): """Handle shooting at a duck""" - # Status effects - if self._has_active_effect(player, 'eliminated'): - return { - 'success': False, - 'message_key': 'player_eliminated', - 'message_args': {'nick': nick} - } - if self._has_active_effect(player, 'poisoned'): - return { - 'success': False, - 'message_key': 'player_poisoned', - 'message_args': {'nick': nick} - } - # Check if gun is confiscated if player.get('gun_confiscated', False): return { @@ -346,76 +227,58 @@ class DuckGame: # Calculate hit chance using level-modified accuracy modified_accuracy = self.bot.levels.get_modified_accuracy(player) hit_chance = modified_accuracy / 100.0 - if self._has_active_effect(player, 'perfect_aim'): - hit_chance = 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') - - # Multi-HP ducks: treat as "boss" style. - if duck.get('max_hp', 1) > 1: + + if duck_type == 'golden': + # Golden duck - multi-hit with high XP duck['current_hp'] -= 1 - per_hit_xp = self._get_duck_xp_per_hit(duck_type) - + xp_gained = self.bot.get_config('golden_duck_xp', 15) + if duck['current_hp'] > 0: - # Award XP for hitting (but not killing) the duck - player['xp'] = player.get('xp', 0) + per_hit_xp + # Still alive, reveal it's golden but don't remove + # Award XP for hitting (but not killing) the golden duck + player['xp'] = player.get('xp', 0) + xp_gained - accuracy_gain = self.bot.get_config('gameplay.accuracy_gain_on_hit', self.bot.get_config('accuracy_gain_on_hit', 1)) - max_accuracy = self.bot.get_config('gameplay.max_accuracy', self.bot.get_config('max_accuracy', 100)) - player['accuracy'] = min(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) + accuracy_gain, max_accuracy) + 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() - - # Choose appropriate message for multi-HP duck types - message_key = { - 'golden': 'bang_hit_golden', - 'concrete': 'bang_hit_concrete', - 'holy_grail': 'bang_hit_holy_grail', - 'diamond': 'bang_hit_diamond' - }.get(duck_type, 'bang_hit_golden') # Default to golden format for unknown types - return { 'success': True, 'hit': True, - 'message_key': message_key, + 'message_key': 'bang_hit_golden', 'message_args': { 'nick': nick, 'hp_remaining': duck['current_hp'], - 'xp_gained': per_hit_xp, - 'ducks_shot': player.get('ducks_shot', 0) + 'xp_gained': xp_gained } } - - # Killed! + 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 = per_hit_xp * int(duck.get('max_hp', 1)) - message_key = { - 'golden': 'bang_hit_golden_killed', - 'concrete': 'bang_hit_concrete_killed', - 'holy_grail': 'bang_hit_holy_grail_killed', - 'diamond': 'bang_hit_diamond_killed' - }.get(duck_type, 'bang_hit_golden_killed') + xp_gained = self.bot.get_config('fast_duck_xp', 12) + message_key = 'bang_hit_fast' else: - # Single-HP ducks + # Normal duck self.ducks[channel].pop(0) - xp_gained = self._get_duck_xp_per_hit(duck_type) - message_key = { - 'normal': 'bang_hit', - 'fast': 'bang_hit_fast', - 'explosive': 'bang_hit_explosive' - }.get(duck_type, 'bang_hit') - - if duck_type == 'explosive': - self._add_temporary_effect(player, 'eliminated', 2 * 3600) + 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('gameplay.accuracy_gain_on_hit', self.bot.get_config('accuracy_gain_on_hit', 1)) - max_accuracy = self.bot.get_config('gameplay.max_accuracy', self.bot.get_config('max_accuracy', 100)) - player['accuracy'] = min(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) + accuracy_gain, max_accuracy) + 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) @@ -424,7 +287,7 @@ class DuckGame: # 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) + self._rearm_all_disarmed_players() # Check for item drops dropped_item = self._check_item_drop(player, duck_type) @@ -464,7 +327,7 @@ class DuckGame: 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(): + 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): @@ -525,20 +388,6 @@ class DuckGame: def befriend_duck(self, nick, channel, player): """Handle befriending a duck""" - # Status effects - if self._has_active_effect(player, 'eliminated'): - return { - 'success': False, - 'message_key': 'player_eliminated', - 'message_args': {'nick': nick} - } - if self._has_active_effect(player, 'poisoned'): - return { - 'success': False, - 'message_key': 'player_poisoned', - 'message_args': {'nick': nick} - } - # Check for duck if channel not in self.ducks or not self.ducks[channel]: return { @@ -564,34 +413,6 @@ class DuckGame: if random.random() < success_rate: # Success - befriend the duck duck = self.ducks[channel].pop(0) - - duck_type = duck.get('duck_type', 'normal') - - # Poison effects - if duck_type == 'poisonous': - self._add_temporary_effect(player, 'poisoned', 2 * 3600) - self.db.save_database() - return { - 'success': True, - 'befriended': True, - 'message_key': 'bef_poisoned', - 'message_args': { - 'nick': nick, - 'duration_hours': 2 - } - } - if duck_type == 'radioactive': - self._add_temporary_effect(player, 'poisoned', 8 * 3600) - self.db.save_database() - return { - 'success': True, - 'befriended': True, - 'message_key': 'bef_poisoned', - 'message_args': { - 'nick': nick, - 'duration_hours': 8 - } - } # Lower XP gain than shooting xp_gained = self.bot.get_config('gameplay.befriend_xp', 5) @@ -606,7 +427,7 @@ class DuckGame: # 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._rearm_all_disarmed_players() self.db.save_database() return { @@ -676,11 +497,11 @@ class DuckGame: } } - def _rearm_all_disarmed_players(self, channel): - """Rearm all players who have been disarmed (gun confiscated) in a channel""" + def _rearm_all_disarmed_players(self): + """Rearm all players who have been disarmed (gun confiscated)""" try: rearmed_count = 0 - for player_name, player_data in self.db.get_players_for_channel(channel).items(): + for player_name, player_data in self.db.players.items(): if player_data.get('gun_confiscated', False): player_data['gun_confiscated'] = False # Update magazines based on player level @@ -693,15 +514,15 @@ class DuckGame: except Exception as e: self.logger.error(f"Error in _rearm_all_disarmed_players: {e}") - def _get_active_spawn_multiplier(self, channel): - """Get the current spawn rate multiplier from active bread effects in a channel""" + 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 player_name, player_data in self.db.get_players_for_channel(channel).items(): - effects = self._get_effects_list(player_data) + for player_name, player_data in self.db.players.items(): + effects = player_data.get('temporary_effects', []) for effect in effects: if (effect.get('type') == 'attract_ducks' and effect.get('expires_at', 0) > current_time): @@ -717,8 +538,8 @@ class DuckGame: """Check if player has wet clothes that prevent shooting""" import time current_time = time.time() - - effects = self._get_effects_list(player) + + effects = player.get('temporary_effects', []) for effect in effects: if (effect.get('type') == 'wet_clothes' and effect.get('expires_at', 0) > current_time): @@ -731,7 +552,7 @@ class DuckGame: current_time = time.time() try: - effects = self._get_effects_list(player) + effects = player.get('temporary_effects', []) for effect in effects: if (effect.get('type') == 'insurance' and effect.get('protection') == protection_type and @@ -748,8 +569,8 @@ class DuckGame: current_time = time.time() try: - for _channel, player_name, player_data in self.db.iter_all_players(): - effects = self._get_effects_list(player_data) + for player_name, player_data in self.db.players.items(): + effects = player_data.get('temporary_effects', []) active_effects = [] for effect in effects: @@ -778,13 +599,12 @@ class DuckGame: if random.random() > drop_chance: return None # No drop - # Get drop table for this duck type (fallback to normal) + # 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: - drop_table = self.bot.get_config('item_drops.normal_duck_drops', []) if not drop_table: + self.logger.warning(f"No drop table found for {duck_type} duck") return None # Weighted random selection @@ -823,40 +643,4 @@ class DuckGame: except Exception as e: self.logger.error(f"Error in _check_item_drop: {e}") - return None - - def _has_active_effect(self, player, effect_type): - import time - current_time = time.time() - effects = self._get_effects_list(player) - for effect in effects: - if effect.get('type') == effect_type and effect.get('expires_at', 0) > current_time: - return True - return False - - def _add_temporary_effect(self, player, effect_type, duration_seconds): - import time - # Normalize effects storage in case legacy data stored it as a dict/string. - self._get_effects_list(player) - duration_seconds = int(duration_seconds) - duration_seconds = max(1, min(duration_seconds, 7 * 24 * 3600)) # cap 7 days - player['temporary_effects'].append({ - 'type': effect_type, - 'expires_at': time.time() + duration_seconds - }) - - def _get_duck_xp_per_hit(self, duck_type): - """Get XP value for a duck type (supports duck_types.*.xp and legacy keys).""" - xp = self.bot.get_config(f'duck_types.{duck_type}.xp', None) - if xp is None: - # Legacy keys - if duck_type == 'golden': - xp = self.bot.get_config('golden_duck_xp', 15) - elif duck_type == 'fast': - xp = self.bot.get_config('fast_duck_xp', 12) - else: - xp = self.bot.get_config('normal_duck_xp', 10) - try: - return int(xp) - except (TypeError, ValueError): - return 10 \ No newline at end of file + return None \ No newline at end of file