202 lines
9.1 KiB
Python
202 lines
9.1 KiB
Python
"""
|
|
Background tasks for the bot.
|
|
Handles periodic maintenance, regeneration, and processing.
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
from bot import database
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def decay_dropped_items(shutdown_event):
|
|
"""A background task that periodically cleans up old dropped items."""
|
|
while not shutdown_event.is_set():
|
|
try:
|
|
# Wait for 5 minutes before the next cleanup
|
|
await asyncio.wait_for(shutdown_event.wait(), timeout=300)
|
|
except asyncio.TimeoutError:
|
|
start_time = time.time()
|
|
logger.info("Running item decay task...")
|
|
|
|
# Set decay time to 1 hour (3600 seconds)
|
|
decay_seconds = 3600
|
|
timestamp_limit = int(time.time()) - decay_seconds
|
|
items_removed = await database.remove_expired_dropped_items(timestamp_limit)
|
|
|
|
elapsed = time.time() - start_time
|
|
if items_removed > 0:
|
|
logger.info(f"Decayed and removed {items_removed} old items in {elapsed:.2f}s")
|
|
|
|
|
|
async def regenerate_stamina(shutdown_event):
|
|
"""A background task that periodically regenerates stamina for all players."""
|
|
while not shutdown_event.is_set():
|
|
try:
|
|
# Wait for 5 minutes before the next regeneration cycle
|
|
await asyncio.wait_for(shutdown_event.wait(), timeout=300)
|
|
except asyncio.TimeoutError:
|
|
start_time = time.time()
|
|
logger.info("Running stamina regeneration...")
|
|
|
|
players_updated = await database.regenerate_all_players_stamina()
|
|
|
|
elapsed = time.time() - start_time
|
|
if players_updated > 0:
|
|
logger.info(f"Regenerated stamina for {players_updated} players in {elapsed:.2f}s")
|
|
|
|
# Alert if regeneration is taking too long (potential scaling issue)
|
|
if elapsed > 5.0:
|
|
logger.warning(f"⚠️ Stamina regeneration took {elapsed:.2f}s (threshold: 5s) - check database load!")
|
|
|
|
|
|
async def check_combat_timers(shutdown_event):
|
|
"""A background task that checks for idle combat turns and auto-attacks."""
|
|
while not shutdown_event.is_set():
|
|
try:
|
|
# Wait for 30 seconds before next check
|
|
await asyncio.wait_for(shutdown_event.wait(), timeout=30)
|
|
except asyncio.TimeoutError:
|
|
start_time = time.time()
|
|
# Check for combats idle for more than 5 minutes (300 seconds)
|
|
idle_threshold = time.time() - 300
|
|
idle_combats = await database.get_all_idle_combats(idle_threshold)
|
|
|
|
if idle_combats:
|
|
logger.info(f"Processing {len(idle_combats)} idle combats...")
|
|
|
|
for combat in idle_combats:
|
|
try:
|
|
from bot import combat as combat_logic
|
|
# Force end player's turn and let NPC attack
|
|
if combat['turn'] == 'player':
|
|
await database.update_combat(combat['player_id'], {
|
|
'turn': 'npc',
|
|
'turn_started_at': time.time()
|
|
})
|
|
# NPC attacks
|
|
await combat_logic.npc_attack(combat['player_id'])
|
|
except Exception as e:
|
|
logger.error(f"Error processing idle combat: {e}")
|
|
|
|
# Log performance for monitoring
|
|
if idle_combats:
|
|
elapsed = time.time() - start_time
|
|
logger.info(f"Processed {len(idle_combats)} idle combats in {elapsed:.2f}s")
|
|
|
|
# Warn if taking too long (potential scaling issue)
|
|
if elapsed > 10.0:
|
|
logger.warning(f"⚠️ Combat timer check took {elapsed:.2f}s (threshold: 10s) - consider batching!")
|
|
|
|
|
|
async def decay_corpses(shutdown_event):
|
|
"""A background task that removes old corpses."""
|
|
while not shutdown_event.is_set():
|
|
try:
|
|
# Wait for 10 minutes before next cleanup
|
|
await asyncio.wait_for(shutdown_event.wait(), timeout=600)
|
|
except asyncio.TimeoutError:
|
|
start_time = time.time()
|
|
logger.info("Running corpse decay...")
|
|
|
|
# Player corpses decay after 24 hours
|
|
player_corpse_limit = time.time() - (24 * 3600)
|
|
player_corpses_removed = await database.remove_expired_player_corpses(player_corpse_limit)
|
|
|
|
# NPC corpses decay after 2 hours
|
|
npc_corpse_limit = time.time() - (2 * 3600)
|
|
npc_corpses_removed = await database.remove_expired_npc_corpses(npc_corpse_limit)
|
|
|
|
elapsed = time.time() - start_time
|
|
if player_corpses_removed > 0 or npc_corpses_removed > 0:
|
|
logger.info(f"Decayed {player_corpses_removed} player corpses and {npc_corpses_removed} NPC corpses in {elapsed:.2f}s")
|
|
|
|
|
|
async def process_status_effects(shutdown_event):
|
|
"""
|
|
A background task that applies damage from persistent status effects.
|
|
Runs every 5 minutes to process status effect ticks.
|
|
"""
|
|
while not shutdown_event.is_set():
|
|
try:
|
|
# Wait for 5 minutes before next processing cycle
|
|
await asyncio.wait_for(shutdown_event.wait(), timeout=300)
|
|
except asyncio.TimeoutError:
|
|
start_time = time.time()
|
|
logger.info("Running status effects processor...")
|
|
|
|
try:
|
|
# Decrement all status effect ticks and get affected players
|
|
affected_players = await database.decrement_all_status_effect_ticks()
|
|
|
|
if not affected_players:
|
|
elapsed = time.time() - start_time
|
|
logger.info(f"No active status effects to process ({elapsed:.3f}s)")
|
|
continue
|
|
|
|
# Process each affected player
|
|
deaths = 0
|
|
damage_dealt = 0
|
|
|
|
for player_id in affected_players:
|
|
try:
|
|
# Get current status effects (after decrement)
|
|
effects = await database.get_player_status_effects(player_id)
|
|
|
|
if not effects:
|
|
continue
|
|
|
|
# Calculate total damage
|
|
from bot.status_utils import calculate_status_damage
|
|
total_damage = calculate_status_damage(effects)
|
|
|
|
if total_damage > 0:
|
|
damage_dealt += total_damage
|
|
player = await database.get_player(player_id)
|
|
|
|
if not player or player['is_dead']:
|
|
continue
|
|
|
|
new_hp = max(0, player['hp'] - total_damage)
|
|
|
|
# Check if player died from status effects
|
|
if new_hp <= 0:
|
|
await database.update_player(player_id, {'hp': 0, 'is_dead': True})
|
|
deaths += 1
|
|
|
|
# Create player corpse
|
|
inventory = await database.get_inventory(player_id)
|
|
await database.create_player_corpse(
|
|
player_name=player['name'],
|
|
location_id=player['location_id'],
|
|
items=inventory
|
|
)
|
|
|
|
# Remove status effects from dead player
|
|
await database.remove_all_status_effects(player_id)
|
|
|
|
logger.info(f"Player {player['name']} (ID: {player_id}) died from status effects")
|
|
else:
|
|
# Apply damage
|
|
await database.update_player(player_id, {'hp': new_hp})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing status effects for player {player_id}: {e}")
|
|
|
|
elapsed = time.time() - start_time
|
|
logger.info(
|
|
f"Processed status effects for {len(affected_players)} players "
|
|
f"({damage_dealt} total damage, {deaths} deaths) in {elapsed:.3f}s"
|
|
)
|
|
|
|
# Warn if taking too long (potential scaling issue)
|
|
if elapsed > 5.0:
|
|
logger.warning(
|
|
f"⚠️ Status effects processing took {elapsed:.3f}s (threshold: 5s) "
|
|
f"- {len(affected_players)} players affected"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in status effects processor: {e}")
|