Add clover luck item and admin restart command
This commit is contained in:
9
shop.json
Normal file → Executable file
9
shop.json
Normal file → Executable file
@@ -61,6 +61,15 @@
|
||||
"price": 30,
|
||||
"description": "Change into dry clothes - allows shooting again after being soaked",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
import time
|
||||
import signal
|
||||
@@ -27,6 +28,7 @@ class DuckHuntBot:
|
||||
# Used by auto-rejoin and (in newer revisions) admin join/leave reporting.
|
||||
self.pending_joins = {}
|
||||
self.shutdown_requested = False
|
||||
self.restart_requested = False
|
||||
self.rejoin_attempts = {} # Track rejoin attempts per channel
|
||||
self.rejoin_tasks = {} # Track active rejoin tasks
|
||||
|
||||
@@ -769,6 +771,14 @@ class DuckHuntBot:
|
||||
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 not command_executed:
|
||||
self.logger.debug(f"Unknown command '{cmd}' from {nick}")
|
||||
@@ -1190,6 +1200,22 @@ class DuckHuntBot:
|
||||
for line in help_lines:
|
||||
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):
|
||||
"""Handle !use command"""
|
||||
@@ -1802,6 +1828,11 @@ class DuckHuntBot:
|
||||
|
||||
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):
|
||||
"""Close IRC connection with comprehensive error handling"""
|
||||
if not self.writer:
|
||||
|
||||
34
src/game.py
34
src/game.py
@@ -227,6 +227,16 @@ class DuckGame:
|
||||
# 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]
|
||||
@@ -407,6 +417,15 @@ class DuckGame:
|
||||
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)
|
||||
@@ -581,6 +600,21 @@ class DuckGame:
|
||||
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
|
||||
|
||||
56
src/shop.py
56
src/shop.py
@@ -389,6 +389,62 @@ class ShopManager:
|
||||
"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':
|
||||
# Add insurance protection against friendly fire
|
||||
if 'temporary_effects' not in player:
|
||||
|
||||
Reference in New Issue
Block a user