WIP: Current state before PVP combat investigation

This commit is contained in:
Joan
2026-02-03 12:19:28 +01:00
parent 7f42fd6b7f
commit 0b0a23f500
36 changed files with 2423 additions and 1472 deletions

View File

@@ -2,7 +2,7 @@
Combat router.
Auto-generated from main.py migration.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from fastapi import APIRouter, HTTPException, Depends, status, Request
from fastapi.security import HTTPAuthorizationCredentials
from typing import Optional, Dict, Any
from datetime import datetime
@@ -12,7 +12,7 @@ import logging
from ..core.security import get_current_user, security, verify_internal_key
from ..services.models import *
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, create_combat_message
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, create_combat_message, get_game_message
from .. import database as db
from ..items import ItemsManager
from .. import game_logic
@@ -80,6 +80,7 @@ async def get_combat_status(current_user: dict = Depends(get_current_user)):
@router.post("/api/game/combat/initiate")
async def initiate_combat(
req: InitiateCombatRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Start combat with a wandering enemy"""
@@ -88,6 +89,9 @@ async def initiate_combat(
sys.path.insert(0, '/app')
from data.npcs import NPCS
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
# Check if already in combat
existing_combat = await db.get_active_combat(current_user['id'])
if existing_combat:
@@ -147,7 +151,7 @@ async def initiate_combat(
await manager.send_personal_message(current_user['id'], {
"type": "combat_started",
"data": {
"message": create_combat_message("combat_start", origin="neutral", npc_name=npc_def.name),
"messages": [create_combat_message("combat_start", origin="neutral", npc_name=npc_def.name)],
"combat": {
"npc_id": enemy.npc_id,
"npc_name": npc_def.name,
@@ -167,7 +171,7 @@ async def initiate_combat(
message={
"type": "location_update",
"data": {
"message": f"{player['name']} entered combat with {get_locale_string(npc_def.name)}",
"message": get_game_message('player_entered_combat', locale, player_name=player['name'], npc_name=get_locale_string(npc_def.name, locale)),
"action": "combat_started",
"player_id": player['id']
},
@@ -178,7 +182,7 @@ async def initiate_combat(
return {
"success": True,
"message": create_combat_message("combat_start", origin="neutral", npc_name=npc_def.name),
"messages": [create_combat_message("combat_start", origin="neutral", npc_name=npc_def.name)],
"combat": {
"npc_id": enemy.npc_id,
"npc_name": npc_def.name,
@@ -194,6 +198,7 @@ async def initiate_combat(
@router.post("/api/game/combat/action")
async def combat_action(
req: CombatActionRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Perform a combat action"""
@@ -202,6 +207,9 @@ async def combat_action(
sys.path.insert(0, '/app')
from data.npcs import NPCS
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
# Get active combat
combat = await db.get_active_combat(current_user['id'])
if not combat:
@@ -238,7 +246,7 @@ async def combat_action(
player = current_user # current_user is already the character dict
npc_def = NPCS.get(combat['npc_id'])
result_message = ""
messages = []
combat_over = False
player_won = False
@@ -278,12 +286,20 @@ async def combat_action(
damage = max(1, base_damage + strength_bonus + level_bonus + weapon_damage + variance)
if attack_failed:
result_message = f"Your attack misses due to heavy encumbrance! "
messages.append(create_combat_message(
"player_miss",
origin="player",
reason="encumbrance"
))
new_npc_hp = combat['npc_hp']
else:
# Apply damage to NPC
new_npc_hp = max(0, combat['npc_hp'] - damage)
result_message = f"You attack for {damage} damage! "
messages.append(create_combat_message(
"player_attack",
origin="player",
damage=damage
))
# Apply weapon effects
if weapon_effects and 'bleeding' in weapon_effects:
@@ -292,26 +308,42 @@ async def combat_action(
# Apply bleeding effect (would need combat effects table, for now just bonus damage)
bleed_damage = bleeding.get('damage', 0)
new_npc_hp = max(0, new_npc_hp - bleed_damage)
result_message += f"💉 Bleeding effect! +{bleed_damage} damage! "
messages.append(create_combat_message(
"effect_bleeding",
origin="player",
damage=bleed_damage
))
# Decrease weapon durability (from unique_item)
if weapon_inv_id and inv_item.get('unique_item_id'):
new_durability = await db.decrease_unique_item_durability(inv_item['unique_item_id'], 1)
if new_durability is None:
# Weapon broke (unique_item was deleted, cascades to inventory)
result_message += "\n⚠️ Your weapon broke! "
messages.append(create_combat_message(
"weapon_broke",
origin="player",
item_name=weapon_def.name
))
await db.unequip_item(player['id'], 'weapon')
if new_npc_hp <= 0:
# NPC defeated
result_message += f"Victory! Defeated {get_locale_string(npc_def.name)}"
messages.append(create_combat_message(
"victory",
origin="neutral",
npc_name=npc_def.name
))
combat_over = True
player_won = True
# Award XP
xp_gained = npc_def.xp_reward
new_xp = player['xp'] + xp_gained
result_message += f"\n+{xp_gained} XP"
messages.append(create_combat_message(
"xp_gain",
origin="player",
amount=xp_gained
))
await db.update_player(player['id'], xp=new_xp)
@@ -321,8 +353,12 @@ async def combat_action(
# Check for level up
level_up_result = await game_logic.check_and_apply_level_up(player['id'])
if level_up_result['leveled_up']:
result_message += f"\n🎉 Level Up! You are now level {level_up_result['new_level']}!"
result_message += f"\n+{level_up_result['levels_gained']} stat point(s) to spend!"
messages.append(create_combat_message(
"level_up",
origin="player",
level=level_up_result['new_level'],
stat_points=level_up_result['levels_gained']
))
# Create corpse with loot
import json
@@ -361,7 +397,7 @@ async def combat_action(
message={
"type": "location_update",
"data": {
"message": f"{player['name']} defeated {npc_def.name}",
"message": get_game_message('player_defeated_enemy_broadcast', locale, player_name=player['name'], npc_name=get_locale_string(npc_def.name, locale)),
"action": "combat_ended",
"player_id": player['id'],
"corpse_created": True
@@ -373,13 +409,13 @@ async def combat_action(
else:
# NPC's turn - use shared logic
npc_attack_message, player_defeated = await game_logic.npc_attack(
npc_attack_messages, player_defeated = await game_logic.npc_attack(
player['id'],
{'npc_hp': new_npc_hp, 'npc_max_hp': combat['npc_max_hp']},
npc_def,
reduce_armor_durability
)
result_message += f"\n{npc_attack_message}"
messages.extend(npc_attack_messages)
if player_defeated:
combat_over = True
@@ -392,7 +428,10 @@ async def combat_action(
elif req.action == 'flee':
# 50% chance to flee
if random.random() < 0.5:
result_message = "You successfully fled from combat!"
messages.append(create_combat_message(
"flee_success",
origin="player"
))
combat_over = True
player_won = False # Fled, not won
@@ -423,7 +462,7 @@ async def combat_action(
message={
"type": "location_update",
"data": {
"message": f"{player['name']} fled from combat",
"message": get_game_message('player_fled_broadcast', locale, player_name=player['name']),
"action": "combat_fled",
"player_id": player['id']
},
@@ -435,10 +474,20 @@ async def combat_action(
# Failed to flee, NPC attacks
npc_damage = random.randint(npc_def.damage_min, npc_def.damage_max)
new_player_hp = max(0, player['hp'] - npc_damage)
result_message = f"Failed to flee! {get_locale_string(npc_def.name)} attacks for {npc_damage} damage!"
messages.append(create_combat_message(
"flee_fail",
origin="enemy",
npc_name=npc_def.name,
damage=npc_damage
))
if new_player_hp <= 0:
result_message += "\nYou have been defeated!"
messages.append(create_combat_message(
"player_defeated",
origin="neutral",
npc_name=npc_def.name
))
combat_over = True
await db.update_player(player['id'], hp=0, is_dead=True)
await db.update_player_statistics(player['id'], deaths=1, failed_flees=1, damage_taken=npc_damage, increment=True)
@@ -509,7 +558,7 @@ async def combat_action(
message={
"type": "location_update",
"data": {
"message": f"{player['name']} was defeated in combat",
"message": get_game_message('player_defeated_broadcast', locale, player_name=player['name']),
"action": "player_died",
"player_id": player['id'],
"corpse": corpse_data
@@ -558,7 +607,7 @@ async def combat_action(
return {
"success": True,
"message": result_message,
"messages": messages,
"combat_over": combat_over,
"player_won": player_won if combat_over else None,
"combat": updated_combat if updated_combat else None,
@@ -574,6 +623,7 @@ async def combat_action(
@router.post("/api/game/pvp/initiate")
async def initiate_pvp_combat(
req: PvPCombatInitiateRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Initiate PvP combat with another player"""
@@ -582,6 +632,9 @@ async def initiate_pvp_combat(
if not attacker:
raise HTTPException(status_code=404, detail="Player not found")
# Extract locale
locale = request.headers.get('Accept-Language', 'en')
# Check if attacker is already in combat
existing_combat = await db.get_active_combat(attacker['id'])
if existing_combat:
@@ -637,7 +690,7 @@ async def initiate_pvp_combat(
await manager.send_personal_message(attacker['id'], {
"type": "combat_started",
"data": {
"message": f"You have initiated combat with {defender['name']}! They get the first turn.",
"message": get_game_message('pvp_initiated_attacker', locale, defender=defender['name']),
"pvp_combat": pvp_combat
},
"timestamp": datetime.utcnow().isoformat()
@@ -646,7 +699,7 @@ async def initiate_pvp_combat(
await manager.send_personal_message(defender['id'], {
"type": "combat_started",
"data": {
"message": f"{attacker['name']} has challenged you to PvP combat! It's your turn.",
"message": get_game_message('pvp_challenged_defender', locale, attacker=attacker['name']),
"pvp_combat": pvp_combat
},
"timestamp": datetime.utcnow().isoformat()
@@ -654,7 +707,7 @@ async def initiate_pvp_combat(
return {
"success": True,
"message": f"You have initiated combat with {defender['name']}! They get the first turn.",
"message": get_game_message('pvp_initiated_attacker', locale, defender=defender['name']),
"pvp_combat": pvp_combat
}
@@ -737,11 +790,15 @@ class PvPAcknowledgeRequest(BaseModel):
@router.post("/api/game/pvp/acknowledge")
async def acknowledge_pvp_combat(
req: PvPAcknowledgeRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Acknowledge PvP combat end"""
await db.acknowledge_pvp_combat(req.combat_id, current_user['id'])
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
# Broadcast to location that player has returned
player = current_user # current_user is already the character dict
if player:
@@ -752,7 +809,7 @@ async def acknowledge_pvp_combat(
"data": {
"player_id": player['id'],
"username": player['name'],
"message": f"{player['name']} has returned from PvP combat."
"message": get_game_message('player_returned_pvp', locale, player_name=player['name'])
},
"timestamp": datetime.utcnow().isoformat()
},
@@ -770,12 +827,16 @@ class PvPCombatActionRequest(BaseModel):
@router.post("/api/game/pvp/action")
async def pvp_combat_action(
req: PvPCombatActionRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Perform a PvP combat action"""
import random
import time
# Extract locale
locale = request.headers.get('Accept-Language', 'en')
# Get PvP combat
pvp_combat = await db.get_pvp_combat_by_player(current_user['id'])
if not pvp_combat:
@@ -795,10 +856,13 @@ async def pvp_combat_action(
current_player = attacker if is_attacker else defender
opponent = defender if is_attacker else attacker
result_message = ""
messages = []
combat_over = False
winner_id = None
# Track the last action string for DB history
last_action_text = ""
if req.action == 'attack':
# Calculate damage (similar to PvE)
base_damage = 5
@@ -822,7 +886,11 @@ async def pvp_combat_action(
if inv_item.get('unique_item_id'):
new_durability = await db.decrease_unique_item_durability(inv_item['unique_item_id'], 1)
if new_durability is None:
result_message += "⚠️ Your weapon broke! "
messages.append(create_combat_message(
"weapon_broke",
origin="player",
item_name=weapon_def.name
))
await db.unequip_item(current_player['id'], 'weapon')
variance = random.randint(-2, 2)
@@ -832,24 +900,42 @@ async def pvp_combat_action(
armor_absorbed, broken_armor = await reduce_armor_durability(opponent['id'], damage)
actual_damage = max(1, damage - armor_absorbed)
# Structure the attack message
messages.append(create_combat_message(
"player_attack",
origin="player",
damage=damage,
armor_absorbed=armor_absorbed
))
# Update opponent HP (use actual player HP, not pvp_combat fields)
new_opponent_hp = max(0, opponent['hp'] - actual_damage)
# Update opponent's HP in database
await db.update_player(opponent['id'], hp=new_opponent_hp)
# Store message with attacker's username so both players can see it correctly
stored_message = f"{current_player['name']} attacks {opponent['name']} for {damage} damage!"
# Construct summary string for DB history/passive player
last_action_text = f"{current_player['name']} attacks {opponent['name']} for {damage} damage!"
if armor_absorbed > 0:
stored_message += f" (Armor absorbed {armor_absorbed})"
last_action_text += f" (Armor absorbed {armor_absorbed})"
for broken in broken_armor:
stored_message += f"\n💔 {opponent['name']}'s {broken['emoji']} {broken['name']} broke!"
messages.append(create_combat_message(
"item_broken",
origin="enemy", # Belongs to opponent
item_name=broken['name'],
emoji=broken['emoji']
))
last_action_text += f"\n💔 {opponent['name']}'s {broken['emoji']} {broken['name']} broke!"
# Check if opponent defeated
if new_opponent_hp <= 0:
stored_message += f"\n🏆 {current_player['name']} has defeated {opponent['name']}!"
result_message = "Combat victory!" # Simple message, details in stored_message
last_action_text += f"\n🏆 {current_player['name']} has defeated {opponent['name']}!"
messages.append(create_combat_message(
"victory",
origin="neutral",
npc_name=opponent['name']
))
combat_over = True
winner_id = current_player['id']
@@ -921,7 +1007,7 @@ async def pvp_combat_action(
message={
"type": "location_update",
"data": {
"message": f"{opponent['name']} was defeated by {current_player['name']} in PvP combat",
"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
@@ -933,8 +1019,7 @@ async def pvp_combat_action(
# End PvP combat
await db.end_pvp_combat(pvp_combat['id'])
else:
# Combat continues - don't return detailed message, it's in stored_message
result_message = "" # Empty message, frontend will show stored_message from polling
# Combat continues
# Update PvP statistics for attack
await db.update_player_statistics(current_player['id'],
@@ -953,7 +1038,7 @@ async def pvp_combat_action(
updates = {
'turn': 'defender' if is_attacker else 'attacker',
'turn_started_at': time.time(),
'last_action': f"{stored_message}|{time.time()}" # Add timestamp for uniqueness
'last_action': f"{last_action_text}|{time.time()}" # Add timestamp for uniqueness
}
# No need to update HP in pvp_combat - we use player HP directly
@@ -963,14 +1048,19 @@ async def pvp_combat_action(
elif req.action == 'flee':
# 50% chance to flee from PvP
if random.random() < 0.5:
result_message = f"You successfully fled from {opponent['name']}!"
last_action_text = f"{current_player['name']} fled from combat!"
messages.append(create_combat_message(
"flee_success",
origin="player"
))
combat_over = True
# Mark as fled, store last action with timestamp, and end combat
flee_field = 'attacker_fled' if is_attacker else 'defender_fled'
await db.update_pvp_combat(pvp_combat['id'], {
flee_field: True,
'last_action': f"{current_player['name']} fled from combat!|{time.time()}"
'last_action': f"{last_action_text}|{time.time()}"
})
await db.end_pvp_combat(pvp_combat['id'])
await db.update_player_statistics(current_player['id'],
@@ -979,11 +1069,17 @@ async def pvp_combat_action(
)
else:
# Failed to flee, skip turn
result_message = f"Failed to flee from {opponent['name']}!"
last_action_text = f"{current_player['name']} tried to flee but failed!"
messages.append(create_combat_message(
"flee_fail",
origin="player",
reason="chance"
))
await db.update_pvp_combat(pvp_combat['id'], {
'turn': 'defender' if is_attacker else 'attacker',
'turn_started_at': time.time(),
'last_action': f"{current_player['name']} tried to flee but failed!|{time.time()}"
'last_action': f"{last_action_text}|{time.time()}"
})
await db.update_player_statistics(current_player['id'],
pvp_failed_flees=1,
@@ -1041,20 +1137,22 @@ async def pvp_combat_action(
await manager.send_personal_message(player_id, {
"type": "combat_update",
"data": {
"message": result_message if player_id == current_user['id'] else "",
"log_entry": result_message if player_id == current_user['id'] else "", # Append to combat log
"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']
"defender_hp": fresh_defender['hp'],
"messages": messages if player_id == current_user['id'] else []
},
"timestamp": datetime.utcnow().isoformat()
})
return {
"success": True,
"message": result_message,
"messages": messages,
"combat_over": combat_over,
"winner_id": winner_id
"winner_id": winner_id,
"pvp_combat": updated_pvp
}