Pre-combat-improvements: Combat animations, flee fixes, corpse logic updates
This commit is contained in:
@@ -6,6 +6,7 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from .services.constants import PVP_TURN_TIMEOUT
|
||||
import os
|
||||
import fcntl
|
||||
from typing import Dict, Optional
|
||||
@@ -346,6 +347,112 @@ async def check_combat_timers():
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BACKGROUND TASK: PVP COMBAT TIMERS
|
||||
# ============================================================================
|
||||
|
||||
async def check_pvp_combat_timers(manager=None):
|
||||
"""Checks for expired PvP combat turns and auto-advances them."""
|
||||
logger.info("⚔️ PvP Combat Timer task started")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
|
||||
start_time = time.time()
|
||||
all_pvp_combats = await db.get_all_pvp_combats()
|
||||
|
||||
processed = 0
|
||||
for combat in all_pvp_combats:
|
||||
try:
|
||||
# Check if combat has already ended (fled or player dead)
|
||||
if combat.get('attacker_fled') or combat.get('defender_fled'):
|
||||
continue
|
||||
|
||||
# Get both players to check HP
|
||||
attacker = await db.get_player_by_id(combat['attacker_character_id'])
|
||||
defender = await db.get_player_by_id(combat['defender_character_id'])
|
||||
|
||||
if not attacker or not defender:
|
||||
# Player doesn't exist, clean up combat
|
||||
await db.end_pvp_combat(combat['id'])
|
||||
continue
|
||||
|
||||
# Check if combat ended (someone died)
|
||||
if attacker['hp'] <= 0 or defender['hp'] <= 0:
|
||||
continue
|
||||
|
||||
# Check if turn has timed out
|
||||
turn_timeout = combat.get('turn_timeout_seconds', PVP_TURN_TIMEOUT)
|
||||
# Use imported constant instead of hardcoded 300
|
||||
turn_started = combat.get('turn_started_at', time.time())
|
||||
time_elapsed = time.time() - turn_started
|
||||
|
||||
if time_elapsed < turn_timeout:
|
||||
continue # Turn hasn't timed out yet
|
||||
|
||||
# Turn has timed out - advance to other player
|
||||
current_turn = combat.get('turn', 'attacker')
|
||||
new_turn = 'defender' if current_turn == 'attacker' else 'attacker'
|
||||
|
||||
logger.info(f"PvP turn timeout: combat {combat['id']} advancing from {current_turn} to {new_turn}")
|
||||
|
||||
# Update combat with new turn
|
||||
await db.update_pvp_combat(combat['id'], {
|
||||
'turn': new_turn,
|
||||
'turn_started_at': time.time(),
|
||||
'last_action': f"Turn timeout - {current_turn}'s turn skipped|{time.time()}"
|
||||
})
|
||||
|
||||
processed += 1
|
||||
|
||||
# Send WebSocket notifications to both players
|
||||
if manager:
|
||||
# Get updated combat data
|
||||
updated_combat = await db.get_pvp_combat_by_id(combat['id'])
|
||||
if updated_combat:
|
||||
# Calculate time remaining for new turn
|
||||
time_remaining = turn_timeout
|
||||
|
||||
# Build combat update payload
|
||||
combat_update = {
|
||||
"type": "combat_update",
|
||||
"data": {
|
||||
"pvp_combat": {
|
||||
"id": updated_combat['id'],
|
||||
"turn": new_turn,
|
||||
"time_remaining": time_remaining,
|
||||
"turn_timeout": "skipped",
|
||||
"last_action": f"Turn timeout - {current_turn}'s turn skipped"
|
||||
},
|
||||
"is_pvp": True,
|
||||
"message": f"⏱️ Turn skipped due to timeout!"
|
||||
},
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# Notify both players
|
||||
await manager.send_personal_message(
|
||||
combat['attacker_character_id'],
|
||||
combat_update
|
||||
)
|
||||
await manager.send_personal_message(
|
||||
combat['defender_character_id'],
|
||||
combat_update
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing PvP combat {combat.get('id')}: {e}")
|
||||
|
||||
if processed > 0:
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"Processed {processed} PvP combat timeouts in {elapsed:.2f}s")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in PvP combat timer check: {e}", exc_info=True)
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BACKGROUND TASK: INTERACTABLE COOLDOWN CLEANUP
|
||||
# ============================================================================
|
||||
@@ -431,7 +538,7 @@ async def cleanup_interactable_cooldowns(manager=None, world_locations=None):
|
||||
# ============================================================================
|
||||
|
||||
async def decay_corpses(manager=None):
|
||||
"""Removes old corpses.
|
||||
"""Removes old corpses and empty corpses.
|
||||
|
||||
Args:
|
||||
manager: WebSocket ConnectionManager for broadcasting decay events
|
||||
@@ -445,6 +552,7 @@ async def decay_corpses(manager=None):
|
||||
start_time = time.time()
|
||||
logger.info("Running corpse decay...")
|
||||
|
||||
# ===== TIME-BASED DECAY =====
|
||||
# Player corpses decay after 24 hours
|
||||
player_corpse_limit = time.time() - (24 * 3600)
|
||||
expired_player_corpses = await db.get_expired_player_corpses(player_corpse_limit)
|
||||
@@ -455,6 +563,20 @@ async def decay_corpses(manager=None):
|
||||
expired_npc_corpses = await db.get_expired_npc_corpses(npc_corpse_limit)
|
||||
npc_corpses_removed = await db.remove_expired_npc_corpses(npc_corpse_limit)
|
||||
|
||||
# ===== EMPTY CORPSE DECAY =====
|
||||
# Empty corpses (no loot remaining) decay immediately
|
||||
empty_player_corpses = await db.get_empty_player_corpses()
|
||||
empty_player_removed = await db.remove_empty_player_corpses()
|
||||
|
||||
empty_npc_corpses = await db.get_empty_npc_corpses()
|
||||
empty_npc_removed = await db.remove_empty_npc_corpses()
|
||||
|
||||
# Combine all decayed corpses for notification
|
||||
all_decayed_player_corpses = expired_player_corpses + empty_player_corpses
|
||||
all_decayed_npc_corpses = expired_npc_corpses + empty_npc_corpses
|
||||
total_player_removed = player_corpses_removed + empty_player_removed
|
||||
total_npc_removed = npc_corpses_removed + empty_npc_removed
|
||||
|
||||
# Notify players in locations where corpses decayed
|
||||
if manager:
|
||||
from datetime import datetime
|
||||
@@ -463,10 +585,10 @@ async def decay_corpses(manager=None):
|
||||
# Group corpses by location
|
||||
corpses_by_location = defaultdict(lambda: {"player": 0, "npc": 0})
|
||||
|
||||
for corpse in expired_player_corpses:
|
||||
for corpse in all_decayed_player_corpses:
|
||||
corpses_by_location[corpse['location_id']]["player"] += 1
|
||||
|
||||
for corpse in expired_npc_corpses:
|
||||
for corpse in all_decayed_npc_corpses:
|
||||
corpses_by_location[corpse['location_id']]["npc"] += 1
|
||||
|
||||
# Notify each location
|
||||
@@ -487,8 +609,13 @@ async def decay_corpses(manager=None):
|
||||
)
|
||||
|
||||
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")
|
||||
if total_player_removed > 0 or total_npc_removed > 0:
|
||||
logger.info(
|
||||
f"Decayed {total_player_removed} player corpses "
|
||||
f"({player_corpses_removed} expired, {empty_player_removed} empty) and "
|
||||
f"{total_npc_removed} NPC corpses "
|
||||
f"({npc_corpses_removed} expired, {empty_npc_removed} empty) in {elapsed:.2f}s"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in corpse decay: {e}", exc_info=True)
|
||||
@@ -555,13 +682,18 @@ async def process_status_effects(manager=None):
|
||||
await db.update_player(player_id, {'hp': 0, 'is_dead': True})
|
||||
deaths += 1
|
||||
|
||||
# Create player corpse
|
||||
# Only create corpse if player has items
|
||||
inventory = await db.get_inventory(player_id)
|
||||
await db.create_player_corpse(
|
||||
player_name=player['name'],
|
||||
location_id=player['location_id'],
|
||||
items=inventory
|
||||
)
|
||||
if inventory:
|
||||
import json
|
||||
await db.create_player_corpse(
|
||||
player_name=player['name'],
|
||||
location_id=player['location_id'],
|
||||
items=json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
|
||||
)
|
||||
logger.info(f"Created corpse for player {player['name']} with {len(inventory)} items")
|
||||
else:
|
||||
logger.info(f"Player {player['name']} died (status effects) with no items, skipping corpse creation")
|
||||
|
||||
# Remove status effects from dead player
|
||||
await db.remove_all_status_effects(player_id)
|
||||
@@ -698,6 +830,7 @@ async def start_background_tasks(manager=None, world_locations=None):
|
||||
asyncio.create_task(decay_dropped_items(manager)),
|
||||
asyncio.create_task(regenerate_stamina(manager)),
|
||||
asyncio.create_task(check_combat_timers()),
|
||||
asyncio.create_task(check_pvp_combat_timers(manager)),
|
||||
asyncio.create_task(decay_corpses(manager)),
|
||||
asyncio.create_task(process_status_effects(manager)),
|
||||
# Note: Interactable cooldowns are handled client-side with server validation
|
||||
|
||||
Reference in New Issue
Block a user