Add clover luck item and admin restart command

This commit is contained in:
3nd3r
2025-12-30 23:19:37 -06:00
parent 38d9159f50
commit 735c46b8c2
4 changed files with 130 additions and 0 deletions

9
shop.json Normal file → Executable file
View File

@@ -61,6 +61,15 @@
"price": 30, "price": 30,
"description": "Change into dry clothes - allows shooting again after being soaked", "description": "Change into dry clothes - allows shooting again after being soaked",
"type": "dry_clothes" "type": "dry_clothes"
},
"10": {
"name": "4-Leaf Clover",
"price": 100,
"description": "Crazy luck for 10 minutes - greatly boosts hit & befriend success",
"type": "clover_luck",
"duration": 600,
"min_hit_chance": 0.95,
"min_befriend_chance": 0.95
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
import os import os
import sys
import ssl import ssl
import time import time
import signal import signal
@@ -27,6 +28,7 @@ class DuckHuntBot:
# Used by auto-rejoin and (in newer revisions) admin join/leave reporting. # Used by auto-rejoin and (in newer revisions) admin join/leave reporting.
self.pending_joins = {} self.pending_joins = {}
self.shutdown_requested = False self.shutdown_requested = False
self.restart_requested = False
self.rejoin_attempts = {} # Track rejoin attempts per channel self.rejoin_attempts = {} # Track rejoin attempts per channel
self.rejoin_tasks = {} # Track active rejoin tasks self.rejoin_tasks = {} # Track active rejoin tasks
@@ -768,6 +770,14 @@ class DuckHuntBot:
fallback=None, fallback=None,
logger=self.logger logger=self.logger
) )
elif cmd in ("reloadbot", "restartbot") and self.is_admin(user):
command_executed = True
await self.error_recovery.safe_execute_async(
lambda: self.handle_reloadbot(nick, channel),
fallback=None,
logger=self.logger
)
# If no command was executed, it might be an unknown command # If no command was executed, it might be an unknown command
if not command_executed: if not command_executed:
@@ -1189,6 +1199,22 @@ class DuckHuntBot:
# Send all help lines as PM # Send all help lines as PM
for line in help_lines: for line in help_lines:
self.send_message(nick, line) self.send_message(nick, line)
async def handle_reloadbot(self, nick, channel):
"""Admin-only: restart the bot process via PM to apply code changes."""
# PM-only to avoid accidental public restarts
if channel.startswith('#'):
self.send_message(channel, f"{nick} > Use this command in PM only.")
return
self.send_message(nick, "Restarting bot now...")
try:
self.db.save_database()
except Exception:
pass
self.restart_requested = True
self.shutdown_requested = True
async def handle_use(self, nick, channel, player, args): async def handle_use(self, nick, channel, player, args):
@@ -1801,6 +1827,11 @@ class DuckHuntBot:
await self._close_connection() await self._close_connection()
self.logger.info("Bot shutdown complete") self.logger.info("Bot shutdown complete")
# If restart was requested (admin command), re-exec the process.
if self.restart_requested:
self.logger.warning("Restart requested; re-executing process...")
os.execv(sys.executable, [sys.executable] + sys.argv)
async def _close_connection(self): async def _close_connection(self):
"""Close IRC connection with comprehensive error handling""" """Close IRC connection with comprehensive error handling"""

View File

@@ -227,6 +227,16 @@ class DuckGame:
# Calculate hit chance using level-modified accuracy # Calculate hit chance using level-modified accuracy
modified_accuracy = self.bot.levels.get_modified_accuracy(player) modified_accuracy = self.bot.levels.get_modified_accuracy(player)
hit_chance = modified_accuracy / 100.0 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: if random.random() < hit_chance:
# Hit! Get the duck and reveal its type # Hit! Get the duck and reveal its type
duck = self.ducks[channel][0] duck = self.ducks[channel][0]
@@ -406,6 +416,15 @@ class DuckGame:
# Apply level-based modification to befriend rate # Apply level-based modification to befriend rate
level_modified_rate = self.bot.levels.get_modified_befriend_rate(player, base_rate) level_modified_rate = self.bot.levels.get_modified_befriend_rate(player, base_rate)
success_rate = level_modified_rate / 100.0 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: if random.random() < success_rate:
# Success - befriend the duck # Success - befriend the duck
@@ -580,6 +599,21 @@ class DuckGame:
except Exception as e: except Exception as e:
self.logger.error(f"Error cleaning expired effects: {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): def _check_item_drop(self, player, duck_type):
""" """

View File

@@ -388,6 +388,62 @@ class ShopManager:
"spawn_multiplier": spawn_multiplier, "spawn_multiplier": spawn_multiplier,
"duration": duration // 60 # return duration in minutes "duration": duration // 60 # return duration in minutes
} }
elif item_type == 'clover_luck':
# Temporarily boost hit + befriend success rates
if 'temporary_effects' not in player or not isinstance(player.get('temporary_effects'), list):
player['temporary_effects'] = []
duration = item.get('duration', 600) # seconds
try:
duration = int(duration)
except (ValueError, TypeError):
duration = 600
duration = max(30, min(duration, 86400))
try:
min_hit = float(item.get('min_hit_chance', 0.95))
except (ValueError, TypeError):
min_hit = 0.95
try:
min_bef = float(item.get('min_befriend_chance', 0.95))
except (ValueError, TypeError):
min_bef = 0.95
min_hit = max(0.0, min(min_hit, 1.0))
min_bef = max(0.0, min(min_bef, 1.0))
now = time.time()
expires_at = now + duration
# If an existing clover effect is active, extend it instead of stacking.
for effect in player['temporary_effects']:
if isinstance(effect, dict) and effect.get('type') == 'clover_luck' and effect.get('expires_at', 0) > now:
effect['expires_at'] = max(effect.get('expires_at', now), now) + duration
effect['min_hit_chance'] = max(float(effect.get('min_hit_chance', 0.0) or 0.0), min_hit)
effect['min_befriend_chance'] = max(float(effect.get('min_befriend_chance', 0.0) or 0.0), min_bef)
return {
"type": "clover_luck",
"duration": duration // 60,
"min_hit_chance": min_hit,
"min_befriend_chance": min_bef,
"extended": True
}
effect = {
'type': 'clover_luck',
'min_hit_chance': min_hit,
'min_befriend_chance': min_bef,
'expires_at': expires_at
}
player['temporary_effects'].append(effect)
return {
"type": "clover_luck",
"duration": duration // 60,
"min_hit_chance": min_hit,
"min_befriend_chance": min_bef,
"extended": False
}
elif item_type == 'insurance': elif item_type == 'insurance':
# Add insurance protection against friendly fire # Add insurance protection against friendly fire