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,
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"""
|
||||||
|
|||||||
34
src/game.py
34
src/game.py
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
56
src/shop.py
56
src/shop.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user