Pre-combat-improvements: Combat animations, flee fixes, corpse logic updates

This commit is contained in:
Joan
2026-02-03 19:48:37 +01:00
parent 0b0a23f500
commit e6747b1d05
29 changed files with 827 additions and 243 deletions

View File

@@ -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