Pre-combat-rewrite: Backup current state before comprehensive combat frontend rewrite

This commit is contained in:
Joan
2026-01-09 11:07:37 +01:00
parent dc438ae4c1
commit 2875e72b20
29 changed files with 1827 additions and 332 deletions

View File

@@ -135,18 +135,24 @@ async def spawn_manager_loop(manager=None):
if manager:
from datetime import datetime
npc_def = NPCS.get(npc_id)
npc_name = npc_def.name if npc_def else npc_id.replace('_', ' ').title()
npc_name_obj = npc_def.name if npc_def else npc_id.replace('_', ' ').title()
# Handle localized name for the fallback message
if isinstance(npc_name_obj, dict):
npc_name_en = npc_name_obj.get('en', str(npc_name_obj))
else:
npc_name_en = str(npc_name_obj)
await manager.send_to_location(
location_id=location_id,
message={
"type": "location_update",
"data": {
"message": f"A {npc_name} appeared!",
"message": f"A {npc_name_en} appeared!",
"action": "enemy_spawned",
"npc_data": {
"id": enemy_data['id'],
"npc_id": npc_id,
"name": npc_name,
"name": npc_name_obj,
"type": "enemy",
"is_wandering": True,
"image_path": npc_def.image_path if npc_def else None
@@ -209,7 +215,8 @@ async def decay_dropped_items(manager=None):
"type": "location_update",
"data": {
"message": f"{count} dropped item(s) decayed",
"action": "items_decayed"
"action": "items_decayed",
"count": count
},
"timestamp": datetime.utcnow().isoformat()
}
@@ -472,7 +479,8 @@ async def decay_corpses(manager=None):
"type": "location_update",
"data": {
"message": f"{total} {corpse_type} decayed",
"action": "corpses_decayed"
"action": "corpses_decayed",
"count": total
},
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -70,7 +70,7 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
detail="No character selected. Please select a character first."
)
player = await db.get_player_by_id(character_id)
player = await db.get_character_by_id(character_id)
if player is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,

View File

@@ -358,15 +358,7 @@ async def init_db():
await conn.execute(text(index_sql))
# Player operations
async def get_player_by_id(player_id: int) -> Optional[Dict[str, Any]]:
"""Get player by internal ID"""
async with DatabaseSession() as session:
result = await session.execute(
select(players).where(players.c.id == player_id)
)
row = result.first()
return dict(row._mapping) if row else None
async def get_player_by_username(username: str) -> Optional[Dict[str, Any]]:
@@ -421,13 +413,7 @@ async def create_player(
return dict(row._mapping) if row else None
async def update_player(player_id: int, **kwargs) -> bool:
"""Update player fields - OLD FUNCTION, use update_character instead"""
async with DatabaseSession() as session:
stmt = update(characters).where(characters.c.id == player_id).values(**kwargs)
await session.execute(stmt)
await session.commit()
return True
async def update_player_location(player_id: int, location_id: str) -> bool:

View File

@@ -6,14 +6,15 @@ import random
import time
from typing import Dict, Any, Tuple, Optional, List
from . import database as db
from .services.helpers import get_locale_string, translate_travel_message, create_combat_message
async def move_player(player_id: int, direction: str, locations: Dict) -> Tuple[bool, str, Optional[str], int, int]:
async def move_player(player_id: int, direction: str, locations: Dict, locale: str = 'en') -> Tuple[bool, str, Optional[str], int, int]:
"""
Move player in a direction.
Returns: (success, message, new_location_id, stamina_cost, distance_meters)
"""
player = await db.get_player_by_id(player_id)
player = await db.get_character_by_id(player_id)
if not player:
return False, "Player not found", None, 0, 0
@@ -69,13 +70,15 @@ async def move_player(player_id: int, direction: str, locations: Dict) -> Tuple[
return False, "You're too exhausted to move. Wait for your stamina to regenerate.", None, 0, 0
# Update player location and stamina
await db.update_player(
await db.update_character(
player_id,
location_id=new_location_id,
stamina=max(0, player['stamina'] - stamina_cost)
)
return True, f"You travel {direction} to {new_location.name}.", new_location_id, stamina_cost, distance
translated_location = get_locale_string(new_location.name, locale)
travel_message = translate_travel_message(direction, translated_location, locale)
return True, travel_message, new_location_id, stamina_cost, distance
async def inspect_area(player_id: int, location, interactables_data: Dict) -> str:
@@ -216,7 +219,7 @@ async def interact_with_object(
if not item:
continue
item_name = item.name if item else item_id
item_name = get_locale_string(item.name) if item else item_id
emoji = item.emoji if item and hasattr(item, 'emoji') else ''
# Check if item has durability (unique item)
@@ -237,7 +240,7 @@ async def interact_with_object(
max_durability=item.durability,
tier=getattr(item, 'tier', None)
)
items_found.append(f"{emoji} {item_name}")
items_found.append(f"{emoji} {get_locale_string(item_name)}")
current_weight += item.weight
current_volume += item.volume
else:
@@ -252,7 +255,7 @@ async def interact_with_object(
unique_stats=base_stats
)
await db.drop_item_to_world(item_id, 1, player['location_id'], unique_item_id=unique_item_id)
items_dropped.append(f"{emoji} {item_name}")
items_dropped.append(f"{emoji} {get_locale_string(item_name)}")
else:
# Stackable items - process as before
item_weight = item.weight * quantity
@@ -262,13 +265,13 @@ async def interact_with_object(
current_volume + item_volume <= max_volume):
# Add to inventory
await db.add_item_to_inventory(player_id, item_id, quantity)
items_found.append(f"{emoji} {item_name} x{quantity}")
items_found.append(f"{emoji} {get_locale_string(item_name)} x{quantity}")
current_weight += item_weight
current_volume += item_volume
else:
# Drop to ground
await db.drop_item_to_world(item_id, quantity, player['location_id'])
items_dropped.append(f"{emoji} {item_name} x{quantity}")
items_dropped.append(f"{emoji} {get_locale_string(item_name)} x{quantity}")
# Apply damage
if damage_taken > 0:
@@ -283,7 +286,7 @@ async def interact_with_object(
await db.set_interactable_cooldown(interactable_id, action_id, 60)
# Build message
final_message = outcome.text
final_message = get_locale_string(outcome.text)
if items_dropped:
final_message += f"\n⚠️ Inventory full! Dropped to ground: {', '.join(items_dropped)}"
@@ -565,7 +568,7 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
heal_amount = int(combat['npc_max_hp'] * 0.05)
new_npc_hp = min(combat['npc_max_hp'], combat['npc_hp'] + heal_amount)
await db.update_combat(player_id, {'npc_hp': new_npc_hp})
message = f"{npc_def.name} defends and recovers {heal_amount} HP!"
message = f"{get_locale_string(npc_def.name)} defends and recovers {heal_amount} HP!"
elif intent_type == 'special':
# Strong attack (1.5x damage)
@@ -574,7 +577,7 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
actual_damage = max(1, npc_damage - armor_absorbed)
new_player_hp = max(0, player['hp'] - actual_damage)
message = f"{npc_def.name} uses a SPECIAL ATTACK for {npc_damage} damage!"
message = f"{get_locale_string(npc_def.name)} uses a SPECIAL ATTACK for {npc_damage} damage!"
if armor_absorbed > 0:
message += f" (Armor absorbed {armor_absorbed})"
@@ -589,7 +592,7 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
# Enrage bonus if NPC is below 30% HP
if combat['npc_hp'] / combat['npc_max_hp'] < 0.3:
npc_damage = int(npc_damage * 1.5)
message = f"{npc_def.name} is ENRAGED! "
message = f"{get_locale_string(npc_def.name)} is ENRAGED! "
else:
message = ""
@@ -597,7 +600,7 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
actual_damage = max(1, npc_damage - armor_absorbed)
new_player_hp = max(0, player['hp'] - actual_damage)
message += f"{npc_def.name} attacks for {npc_damage} damage!"
message += create_combat_message("enemy_attack", npc_name=npc_def.name, damage=npc_damage, armor_absorbed=armor_absorbed)
if armor_absorbed > 0:
message += f" (Armor absorbed {armor_absorbed})"

View File

@@ -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
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, create_combat_message
from .. import database as db
from ..items import ItemsManager
from .. import game_logic
@@ -147,7 +147,7 @@ async def initiate_combat(
await manager.send_personal_message(current_user['id'], {
"type": "combat_started",
"data": {
"message": f"Combat started with {npc_def.name}!",
"message": create_combat_message("combat_start", npc_name=npc_def.name),
"combat": {
"npc_id": enemy.npc_id,
"npc_name": npc_def.name,
@@ -167,7 +167,7 @@ async def initiate_combat(
message={
"type": "location_update",
"data": {
"message": f"{player['name']} entered combat with {npc_def.name}",
"message": f"{player['name']} entered combat with {get_locale_string(npc_def.name)}",
"action": "combat_started",
"player_id": player['id']
},
@@ -178,7 +178,7 @@ async def initiate_combat(
return {
"success": True,
"message": f"Combat started with {npc_def.name}!",
"message": create_combat_message("combat_start", npc_name=npc_def.name),
"combat": {
"npc_id": enemy.npc_id,
"npc_name": npc_def.name,
@@ -304,7 +304,7 @@ async def combat_action(
if new_npc_hp <= 0:
# NPC defeated
result_message += f"{npc_def.name} has been defeated!"
result_message += create_combat_message("victory", npc_name=npc_def.name)
combat_over = True
player_won = True
@@ -435,7 +435,7 @@ 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! {npc_def.name} attacks for {npc_damage} damage!"
result_message = create_combat_message("flee_fail", npc_name=npc_def.name, damage=npc_damage)
if new_player_hp <= 0:
result_message += "\nYou have been defeated!"

View File

@@ -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, calculate_crafting_stamina_cost
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, calculate_crafting_stamina_cost, get_locale_string
from .. import database as db
from ..items import ItemsManager
from .. import game_logic
@@ -156,7 +156,7 @@ async def get_craftable_items(current_user: dict = Depends(get_current_user)):
})
# Sort: craftable items first, then by tier, then by name
craftable_items.sort(key=lambda x: (not x['can_craft'], x['tier'], x['name']))
craftable_items.sort(key=lambda x: (not x['can_craft'], x['tier'], get_locale_string(x['name'])))
return {'craftable_items': craftable_items}

View File

@@ -2,7 +2,7 @@
Game Routes 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
@@ -391,8 +391,11 @@ async def spend_stat_point(
@router.get("/api/game/location")
async def get_current_location(current_user: dict = Depends(get_current_user)):
async def get_current_location(request: Request, current_user: dict = Depends(get_current_user)):
"""Get current location information"""
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
location_id = current_user['location_id']
location = LOCATIONS.get(location_id)
@@ -682,7 +685,7 @@ async def get_current_location(current_user: dict = Depends(get_current_user)):
corpses_data.append({
"id": f"npc_{corpse['id']}",
"type": "npc",
"name": f"{npc_def.name if npc_def else corpse['npc_id']} Corpse",
"name": f"{get_locale_string(npc_def.name, locale) if npc_def else corpse['npc_id']} Corpse",
"emoji": "💀",
"loot_count": len(loot),
"timestamp": corpse['death_timestamp']
@@ -719,6 +722,7 @@ async def get_current_location(current_user: dict = Depends(get_current_user)):
@router.post("/api/game/move")
async def move(
move_req: MoveRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Move player in a direction"""
@@ -756,10 +760,14 @@ async def move(
detail=f"You must wait {int(cooldown_remaining)} seconds before moving again."
)
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
success, message, new_location_id, stamina_cost, distance = await game_logic.move_player(
current_user['id'],
move_req.direction,
LOCATIONS
LOCATIONS,
locale
)
if not success:
@@ -951,9 +959,13 @@ async def inspect(current_user: dict = Depends(get_current_user)):
@router.post("/api/game/interact")
async def interact(
interact_req: InteractRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Interact with an object"""
"""Interact with an object in the game world"""
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
# Check if player is in combat
combat = await db.get_active_combat(current_user['id'])
if combat:
@@ -1026,7 +1038,7 @@ async def interact(
"instance_id": interact_req.interactable_id,
"action_id": interact_req.action_id,
"cooldown_remaining": cooldown_remaining,
"message": f"{current_user['name']} used {action_display} on {interactable_name}"
"message": f"{current_user['name']} used {get_locale_string(action_display, locale)} on {get_locale_string(interactable_name, locale)}"
},
"timestamp": datetime.utcnow().isoformat()
}
@@ -1035,6 +1047,8 @@ async def interact(
return result
@router.post("/api/game/use_item")
async def use_item(
use_req: UseItemRequest,
@@ -1159,15 +1173,19 @@ async def use_item(
@router.post("/api/game/pickup")
async def pickup(
pickup_req: PickupItemRequest,
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Pick up an item from the ground"""
# Extract locale from Accept-Language header
locale = request.headers.get('Accept-Language', 'en')
# Get item details for broadcast BEFORE picking it up (it will be removed from DB)
# pickup_req.item_id is the dropped_item database ID, not the item_id string
dropped_item = await db.get_dropped_item(pickup_req.item_id)
if dropped_item:
item_def = ITEMS_MANAGER.get_item(dropped_item['item_id'])
item_name = item_def.name if item_def else dropped_item['item_id']
item_name = get_locale_string(item_def.name, locale) if item_def else dropped_item['item_id']
else:
item_name = "item"
@@ -1392,5 +1410,5 @@ async def drop_item(
return {
"success": True,
"message": f"Dropped {item_def.emoji} {item_def.name} x{quantity}"
"message": f"Dropped {item_def.emoji} {get_locale_string(item_def.name)} x{quantity}"
}

View File

@@ -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
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string
from .. import database as db
from ..items import ItemsManager
from .. import game_logic
@@ -310,13 +310,13 @@ async def loot_corpse(
message_parts = []
for item in looted_items:
item_def = ITEMS_MANAGER.get_item(item['item_id'])
item_name = item_def.name if item_def else item['item_id']
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
message_parts.append(f"{item_def.emoji if item_def else ''} {item_name} x{item['quantity']}")
dropped_parts = []
for item in dropped_items:
item_def = ITEMS_MANAGER.get_item(item['item_id'])
item_name = item_def.name if item_def else item['item_id']
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
dropped_parts.append(f"{item.get('emoji', '📦')} {item_name} x{item['quantity']}")
message = ""
@@ -438,13 +438,13 @@ async def loot_corpse(
message_parts = []
for item in looted_items:
item_def = ITEMS_MANAGER.get_item(item['item_id'])
item_name = item_def.name if item_def else item['item_id']
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
message_parts.append(f"{item_def.emoji if item_def else ''} {item_name} x{item['quantity']}")
dropped_parts = []
for item in dropped_items:
item_def = ITEMS_MANAGER.get_item(item['item_id'])
item_name = item_def.name if item_def else item['item_id']
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
dropped_parts.append(f"{item.get('emoji', '📦')} {item_name} x{item['quantity']}")
message = ""

View File

@@ -15,6 +15,45 @@ def get_locale_string(value: Union[str, Dict[str, str]], lang: str = 'en') -> st
return str(value)
# Translation maps for backend messages
DIRECTION_TRANSLATIONS = {
'north': {'en': 'north', 'es': 'norte'},
'south': {'en': 'south', 'es': 'sur'},
'east': {'en': 'east', 'es': 'este'},
'west': {'en': 'west', 'es': 'oeste'},
'northeast': {'en': 'northeast', 'es': 'noreste'},
'northwest': {'en': 'northwest', 'es': 'noroeste'},
'southeast': {'en': 'southeast', 'es': 'sureste'},
'southwest': {'en': 'southwest', 'es': 'suroeste'},
}
def translate_travel_message(direction: str, location_name: str, lang: str = 'en') -> str:
"""Translate a travel message to the user's language."""
dir_translated = DIRECTION_TRANSLATIONS.get(direction, {}).get(lang, direction)
if lang == 'es':
return f"Viajas al {dir_translated} hacia {location_name}."
else:
return f"You travel {dir_translated} to {location_name}."
import json
def create_combat_message(message_type: str, **data) -> str:
"""Create a structured combat message with type and data.
Args:
message_type: Type of combat message (combat_start, player_attack, etc.)
**data: Dynamic data for the message (damage, npc_name, etc.)
Returns:
Dictionary with 'type' and 'data' fields
"""
return json.dumps({
"type": message_type,
"data": data
})
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
"""
Calculate distance between two points using Euclidean distance.