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

@@ -9,6 +9,7 @@ from datetime import datetime
import random
import json
import logging
from ..services.constants import PVP_TURN_TIMEOUT
from ..core.security import get_current_user, security, verify_internal_key
from ..services.models import *
@@ -430,7 +431,8 @@ async def combat_action(
if random.random() < 0.5:
messages.append(create_combat_message(
"flee_success",
origin="player"
origin="player",
message=get_game_message('flee_success_text', locale, name=player['name'])
))
combat_over = True
player_won = False # Fled, not won
@@ -479,7 +481,8 @@ async def combat_action(
"flee_fail",
origin="enemy",
npc_name=npc_def.name,
damage=npc_damage
damage=npc_damage,
message=get_game_message('flee_fail_text', locale, name=player['name'])
))
if new_player_hp <= 0:
@@ -509,30 +512,35 @@ async def combat_action(
'tier': inv_item.get('tier')
})
logger.info(f"Creating player corpse (failed flee) for {player['name']} at {combat['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=player['name'],
location_id=combat['location_id'],
items=json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
)
logger.info(f"Successfully created player corpse (failed flee): ID={corpse_id}, player={player['name']}, location={combat['location_id']}, items_count={len(inventory_items)}")
# Clear player's inventory (items are now in corpse)
await db.clear_inventory(player['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{player['name']}'s Corpse",
"emoji": "⚰️",
"player_name": player['name'],
"loot_count": len(inventory_items),
"items": inventory_items,
"timestamp": time_module.time()
}
# Only create corpse if player has items
corpse_data = None
if inventory_items:
logger.info(f"Creating player corpse (failed flee) for {player['name']} at {combat['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=player['name'],
location_id=combat['location_id'],
items=json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
)
logger.info(f"Successfully created player corpse (failed flee): ID={corpse_id}, player={player['name']}, location={combat['location_id']}, items_count={len(inventory_items)}")
# Clear player's inventory (items are now in corpse)
await db.clear_inventory(player['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{player['name']}'s Corpse",
"emoji": "⚰️",
"player_name": player['name'],
"loot_count": len(inventory_items),
"items": inventory_items,
"timestamp": time_module.time()
}
else:
logger.info(f"Player {player['name']} died (failed flee) with no items, skipping corpse creation")
# Respawn enemy if from wandering
if combat.get('from_wandering_enemy'):
@@ -551,18 +559,21 @@ async def combat_action(
await db.end_combat(player['id'])
# Broadcast to location that player died and corpse appeared
# Broadcast to location that player died (and corpse if created)
logger.info(f"Broadcasting player_died (failed flee) to location {combat['location_id']} for player {player['name']}")
broadcast_data = {
"message": get_game_message('player_defeated_broadcast', locale, player_name=player['name']),
"action": "player_died",
"player_id": player['id']
}
if corpse_data:
broadcast_data["corpse"] = corpse_data
await manager.send_to_location(
location_id=combat['location_id'],
message={
"type": "location_update",
"data": {
"message": get_game_message('player_defeated_broadcast', locale, player_name=player['name']),
"action": "player_died",
"player_id": player['id'],
"corpse": corpse_data
},
"data": broadcast_data,
"timestamp": datetime.utcnow().isoformat()
},
exclude_player_id=player['id']
@@ -667,7 +678,6 @@ async def initiate_pvp_combat(
if not location or location.danger_level < 3:
raise HTTPException(status_code=400, detail="PvP combat is only allowed in dangerous zones (danger level >= 3)")
# Check level difference (+/- 3 levels)
level_diff = abs(attacker['level'] - defender['level'])
if level_diff > 3:
raise HTTPException(
@@ -678,9 +688,9 @@ async def initiate_pvp_combat(
# Create PvP combat
pvp_combat = await db.create_pvp_combat(
attacker_id=attacker['id'],
defender_id=defender['id'],
defender_id=req.target_player_id,
location_id=attacker['location_id'],
turn_timeout=300 # 5 minutes
turn_timeout=PVP_TURN_TIMEOUT
)
# Track PvP combat initiation
@@ -705,6 +715,22 @@ async def initiate_pvp_combat(
"timestamp": datetime.utcnow().isoformat()
})
# Broadcast to location that PvP combat started - both players should be removed from view
await manager.send_to_location(
attacker['location_id'],
{
"type": "location_update",
"data": {
"message": get_game_message('pvp_combat_started_broadcast', locale, attacker=attacker['name'], defender=defender['name']),
"action": "pvp_combat_started",
"players_in_combat": [attacker['id'], defender['id']],
"player_left_ids": [attacker['id'], defender['id']] # Remove both from location view
},
"timestamp": datetime.utcnow().isoformat()
},
exclude_player_id=None # Send to everyone including combatants
)
return {
"success": True,
"message": get_game_message('pvp_initiated_attacker', locale, defender=defender['name']),
@@ -760,14 +786,16 @@ async def get_pvp_combat_status(current_user: dict = Depends(get_current_user)):
"username": attacker['name'],
"level": attacker['level'],
"hp": attacker['hp'], # Use actual player HP
"max_hp": attacker['max_hp']
"max_hp": attacker['max_hp'],
"image": "/images/characters/default.webp"
},
"defender": {
"id": defender['id'],
"username": defender['name'],
"level": defender['level'],
"hp": defender['hp'], # Use actual player HP
"max_hp": defender['max_hp']
"max_hp": defender['max_hp'],
"image": "/images/characters/default.webp"
},
"is_attacker": is_attacker,
"your_turn": your_turn,
@@ -959,30 +987,35 @@ async def pvp_combat_action(
'tier': inv_item.get('tier')
})
logger.info(f"Creating player corpse (PvP death) for {opponent['name']} at {opponent['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=opponent['name'],
location_id=opponent['location_id'],
items=json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
)
logger.info(f"Successfully created player corpse (PvP death): ID={corpse_id}, player={opponent['name']}, location={opponent['location_id']}, items_count={len(inventory_items)}")
# Clear opponent's inventory (items are now in corpse)
await db.clear_inventory(opponent['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{opponent['name']}'s Corpse",
"emoji": "⚰️",
"player_name": opponent['name'],
"loot_count": len(inventory_items),
"items": inventory_items,
"timestamp": time_module.time()
}
# Only create corpse if opponent has items
corpse_data = None
if inventory_items:
logger.info(f"Creating player corpse (PvP death) for {opponent['name']} at {opponent['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=opponent['name'],
location_id=opponent['location_id'],
items=json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
)
logger.info(f"Successfully created player corpse (PvP death): ID={corpse_id}, player={opponent['name']}, location={opponent['location_id']}, items_count={len(inventory_items)}")
# Clear opponent's inventory (items are now in corpse)
await db.clear_inventory(opponent['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{opponent['name']}'s Corpse",
"emoji": "⚰️",
"player_name": opponent['name'],
"loot_count": len(inventory_items),
"items": inventory_items,
"timestamp": time_module.time()
}
else:
logger.info(f"Player {opponent['name']} died (PvP death) with no items, skipping corpse creation")
# Update PvP statistics for both players
await db.update_player_statistics(opponent['id'],
@@ -1000,18 +1033,21 @@ async def pvp_combat_action(
increment=True
)
# Broadcast to location that player died and corpse appeared
# Broadcast to location that player died (and corpse if created)
logger.info(f"Broadcasting player_died (PvP death) to location {opponent['location_id']} for player {opponent['name']}")
broadcast_data = {
"message": get_game_message('pvp_defeat_broadcast', locale, opponent=opponent['name'], winner=current_player['name']),
"action": "player_died",
"player_id": opponent['id']
}
if corpse_data:
broadcast_data["corpse"] = corpse_data
await manager.send_to_location(
location_id=opponent['location_id'],
message={
"type": "location_update",
"data": {
"message": get_game_message('pvp_defeat_broadcast', locale, opponent=opponent['name'], winner=current_player['name']),
"action": "player_died",
"player_id": opponent['id'],
"corpse": corpse_data
},
"data": broadcast_data,
"timestamp": datetime.utcnow().isoformat()
}
)
@@ -1048,10 +1084,11 @@ async def pvp_combat_action(
elif req.action == 'flee':
# 50% chance to flee from PvP
if random.random() < 0.5:
last_action_text = f"{current_player['name']} fled from combat!"
last_action_text = get_game_message('flee_success_text', locale, name=current_player['name'])
messages.append(create_combat_message(
"flee_success",
origin="player"
origin="player",
message=last_action_text
))
combat_over = True
@@ -1069,11 +1106,12 @@ async def pvp_combat_action(
)
else:
# Failed to flee, skip turn
last_action_text = f"{current_player['name']} tried to flee but failed!"
last_action_text = get_game_message('flee_fail_text', locale, name=current_player['name'])
messages.append(create_combat_message(
"flee_fail",
origin="player",
reason="chance"
reason="chance",
message=last_action_text
))
await db.update_pvp_combat(pvp_combat['id'], {
@@ -1113,14 +1151,16 @@ async def pvp_combat_action(
"username": fresh_attacker['name'],
"level": fresh_attacker['level'],
"hp": fresh_attacker['hp'],
"max_hp": fresh_attacker['max_hp']
"max_hp": fresh_attacker['max_hp'],
"image": "/images/characters/default.webp"
},
"defender": {
"id": fresh_defender['id'],
"username": fresh_defender['name'],
"level": fresh_defender['level'],
"hp": fresh_defender['hp'],
"max_hp": fresh_defender['max_hp']
"max_hp": fresh_defender['max_hp'],
"image": "/images/characters/default.webp"
},
"is_attacker": is_attacker,
"your_turn": your_turn,
@@ -1134,18 +1174,68 @@ async def pvp_combat_action(
"defender_fled": updated_pvp.get('defender_fled', False)
}
# Determine which player object to send as "player" data for global state updates
player_data = {
"id": fresh_attacker['id'],
"username": fresh_attacker['name'],
"level": fresh_attacker['level'],
"hp": fresh_attacker['hp'],
"max_hp": fresh_attacker['max_hp'],
"xp": fresh_attacker['xp'],
"max_xp": fresh_attacker['level'] * 1000
} if is_attacker else {
"id": fresh_defender['id'],
"username": fresh_defender['name'],
"level": fresh_defender['level'],
"hp": fresh_defender['hp'],
"max_hp": fresh_defender['max_hp'],
"xp": fresh_defender['xp'],
"max_xp": fresh_defender['level'] * 1000
}
# Process messages for this player
# Use actor_id (current_player['id']) to identify who performed the action
# If I am NOT the actor, then the action was done BY an enemy against me.
# So I swap 'player' origin (Actor) to 'enemy' origin (Attacker from my perspective).
actor_id = current_player['id']
import copy
player_messages = []
is_actor = (player_id == actor_id)
# For the victim (non-actor), we strip the pre-generated text messages so frontend can generate
# "Enemy hit you" instead of "Alice hit Bob"
if not is_actor:
msgs_copy = copy.deepcopy(messages)
for m in msgs_copy:
if m.get('origin') == 'player':
m['origin'] = 'enemy'
elif m.get('origin') == 'enemy':
m['origin'] = 'player'
player_messages.append(m)
else:
player_messages = messages
# Send separate payloads
# For actor: keep full text
# For victim: strip main message text so frontend uses data to render "Enemy hit you"
payload_data = {
"message": last_action_text if is_actor else None, # key refactor: hide text for victim
"log_entry": last_action_text if is_actor else None,
"pvp_combat": enriched_pvp,
"combat_over": combat_over,
"winner_id": winner_id,
"player": player_data,
"attacker_hp": fresh_attacker['hp'],
"defender_hp": fresh_defender['hp'],
"messages": player_messages
}
await manager.send_personal_message(player_id, {
"type": "combat_update",
"data": {
"message": last_action_text if player_id == current_user['id'] else "",
"log_entry": last_action_text if player_id == current_user['id'] else "", # Append to combat log
"pvp_combat": enriched_pvp,
"combat_over": combat_over,
"winner_id": winner_id,
"attacker_hp": fresh_attacker['hp'],
"defender_hp": fresh_defender['hp'],
"messages": messages if player_id == current_user['id'] else []
},
"data": payload_data,
"timestamp": datetime.utcnow().isoformat()
})

View File

@@ -1124,46 +1124,54 @@ async def use_item(
'tier': inv_item.get('tier')
})
# Store minimal data in database
db_items = json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
# Only create corpse if player has items
corpse_data = None
if inventory_items:
# Store minimal data in database
db_items = json.dumps([{'item_id': i['item_id'], 'quantity': i['quantity']} for i in inventory])
logger.info(f"Creating player corpse for {player['name']} at {player['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=player['name'],
location_id=player['location_id'],
items=db_items
)
logger.info(f"Successfully created player corpse: ID={corpse_id}, player={player['name']}, location={player['location_id']}, items_count={len(inventory_items)}")
# Clear player's inventory (items are now in corpse)
await db.clear_inventory(current_user['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{player['name']}'s Corpse",
"emoji": "⚰️",
"player_name": player['name'],
"loot_count": len(inventory_items),
"items": inventory_items, # Full item list for UI
"timestamp": time_module.time()
}
else:
logger.info(f"Player {player['name']} died (use_item combat) with no items, skipping corpse creation")
logger.info(f"Creating player corpse for {player['name']} at {player['location_id']} with {len(inventory_items)} items")
corpse_id = await db.create_player_corpse(
player_name=player['name'],
location_id=player['location_id'],
items=db_items
)
logger.info(f"Successfully created player corpse: ID={corpse_id}, player={player['name']}, location={player['location_id']}, items_count={len(inventory_items)}")
# Clear player's inventory (items are now in corpse)
await db.clear_inventory(current_user['id'])
# Build corpse data for broadcast
corpse_data = {
"id": f"player_{corpse_id}",
"type": "player",
"name": f"{player['name']}'s Corpse",
"emoji": "⚰️",
"player_name": player['name'],
"loot_count": len(inventory_items),
"items": inventory_items, # Full item list for UI
"timestamp": time_module.time()
}
# Broadcast to location that player died and corpse appeared
# Broadcast to location that player died (and corpse if created)
logger.info(f"Broadcasting player_died to location {player['location_id']} for player {player['name']}")
broadcast_data = {
"message": get_game_message('player_defeated_broadcast', locale, player_name=player['name']),
"action": "player_died",
"player_id": player['id']
}
if corpse_data:
broadcast_data["corpse"] = corpse_data
await manager.send_to_location(
location_id=player['location_id'],
message={
"type": "location_update",
"data": {
"message": get_game_message('player_defeated_broadcast', locale, player_name=player['name']),
"action": "player_died",
"player_id": player['id'],
"corpse": corpse_data # Send full corpse data
},
"data": broadcast_data,
"timestamp": datetime.utcnow().isoformat()
},
exclude_player_id=current_user['id']