WIP: Current state before PVP combat investigation
This commit is contained in:
125
api/database.py
125
api/database.py
@@ -2190,3 +2190,128 @@ async def remove_expired_dropped_items(timestamp_limit: float) -> int:
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
return result.rowcount
|
||||
|
||||
# ============================================================================
|
||||
# PVP COMBAT FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
async def get_pvp_combat_by_id(combat_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get PVP combat by ID."""
|
||||
async with DatabaseSession() as session:
|
||||
stmt = select(pvp_combats).where(pvp_combats.c.id == combat_id)
|
||||
result = await session.execute(stmt)
|
||||
row = result.first()
|
||||
return dict(row._mapping) if row else None
|
||||
|
||||
|
||||
async def get_pvp_combat_by_player(character_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get active PVP combat for a player (either as attacker or defender)."""
|
||||
async with DatabaseSession() as session:
|
||||
stmt = select(pvp_combats).where(
|
||||
and_(
|
||||
or_(
|
||||
pvp_combats.c.attacker_character_id == character_id,
|
||||
pvp_combats.c.defender_character_id == character_id
|
||||
),
|
||||
# If acknowledged by both, it's effectively over for query purposes
|
||||
# But here we want the active one.
|
||||
# Logic: If I am attacker, and I haven't acknowledged => active
|
||||
# If I am defender, and I haven't acknowledged => active
|
||||
# Simplified: Just return the record, caller handles logic.
|
||||
)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
# There should only be one active combat at a time per player
|
||||
row = result.first()
|
||||
return dict(row._mapping) if row else None
|
||||
|
||||
|
||||
async def create_pvp_combat(
|
||||
attacker_id: int,
|
||||
defender_id: int,
|
||||
location_id: str,
|
||||
turn_timeout: int = 300
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new PVP combat encounter."""
|
||||
import time
|
||||
async with DatabaseSession() as session:
|
||||
current_time = time.time()
|
||||
|
||||
# Get names for denormalization
|
||||
attacker_res = await session.execute(select(characters.c.name).where(characters.c.id == attacker_id))
|
||||
defender_res = await session.execute(select(characters.c.name).where(characters.c.id == defender_id))
|
||||
|
||||
attacker_name = attacker_res.scalar() or "Unknown"
|
||||
defender_name = defender_res.scalar() or "Unknown"
|
||||
|
||||
stmt = insert(pvp_combats).values(
|
||||
attacker_character_id=attacker_id,
|
||||
defender_character_id=defender_id,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name,
|
||||
location_id=location_id,
|
||||
started_at=current_time,
|
||||
updated_at=current_time,
|
||||
turn='defender', # Defender goes first usually, or random? 'initiator pays price?'
|
||||
# Requirement says: "You have initiated combat... They get the first turn."
|
||||
turn_started_at=current_time,
|
||||
turn_timeout_seconds=turn_timeout,
|
||||
attacker_acknowledged=False,
|
||||
defender_acknowledged=False
|
||||
).returning(pvp_combats)
|
||||
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
return dict(result.fetchone()._mapping)
|
||||
|
||||
|
||||
async def update_pvp_combat(combat_id: int, updates: Dict[str, Any]) -> bool:
|
||||
"""Update PVP combat state."""
|
||||
import time
|
||||
updates['updated_at'] = time.time()
|
||||
|
||||
async with DatabaseSession() as session:
|
||||
stmt = update(pvp_combats).where(
|
||||
pvp_combats.c.id == combat_id
|
||||
).values(**updates)
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
|
||||
async def acknowledge_pvp_combat(combat_id: int, player_id: int) -> bool:
|
||||
"""Player acknowledges combat end."""
|
||||
async with DatabaseSession() as session:
|
||||
# First check who this player is
|
||||
stmt = select(pvp_combats).where(pvp_combats.c.id == combat_id)
|
||||
result = await session.execute(stmt)
|
||||
combat = result.first()
|
||||
|
||||
if not combat:
|
||||
return False
|
||||
|
||||
updates = {}
|
||||
if combat.attacker_character_id == player_id:
|
||||
updates['attacker_acknowledged'] = True
|
||||
elif combat.defender_character_id == player_id:
|
||||
updates['defender_acknowledged'] = True
|
||||
else:
|
||||
return False
|
||||
|
||||
stmt = update(pvp_combats).where(
|
||||
pvp_combats.c.id == combat_id
|
||||
).values(**updates)
|
||||
|
||||
await session.execute(stmt)
|
||||
|
||||
# Check if both acknowledged, then delete?
|
||||
# Or just keep it. We have acknowledge flags.
|
||||
# If both acknowledged, maybe delete to clean up?
|
||||
# Let's check updated flags
|
||||
if (updates.get('attacker_acknowledged') or combat.attacker_acknowledged) and \
|
||||
(updates.get('defender_acknowledged') or combat.defender_acknowledged):
|
||||
delete_stmt = delete(pvp_combats).where(pvp_combats.c.id == combat_id)
|
||||
await session.execute(delete_stmt)
|
||||
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
from .services.helpers import get_locale_string, translate_travel_message, create_combat_message, get_game_message
|
||||
|
||||
|
||||
async def move_player(player_id: int, direction: str, locations: Dict, locale: str = 'en') -> Tuple[bool, str, Optional[str], int, int]:
|
||||
@@ -67,7 +67,7 @@ async def move_player(player_id: int, direction: str, locations: Dict, locale: s
|
||||
|
||||
# Check stamina
|
||||
if player['stamina'] < stamina_cost:
|
||||
return False, "You're too exhausted to move. Wait for your stamina to regenerate.", None, 0, 0
|
||||
return False, get_game_message('exhausted_move', locale), None, 0, 0
|
||||
|
||||
# Update player location and stamina
|
||||
await db.update_character(
|
||||
@@ -81,7 +81,7 @@ async def move_player(player_id: int, direction: str, locations: Dict, locale: s
|
||||
return True, travel_message, new_location_id, stamina_cost, distance
|
||||
|
||||
|
||||
async def inspect_area(player_id: int, location, interactables_data: Dict) -> str:
|
||||
async def inspect_area(player_id: int, location, interactables_data: Dict, locale: str = 'en') -> str:
|
||||
"""
|
||||
Inspect the current area and return detailed information.
|
||||
Returns formatted text with interactables and their actions.
|
||||
@@ -92,18 +92,18 @@ async def inspect_area(player_id: int, location, interactables_data: Dict) -> st
|
||||
|
||||
# Check if player has enough stamina
|
||||
if player['stamina'] < 1:
|
||||
return "You're too exhausted to inspect the area thoroughly. Wait for your stamina to regenerate."
|
||||
return get_game_message('exhausted_inspect', locale)
|
||||
|
||||
# Deduct stamina
|
||||
await db.update_player_stamina(player_id, player['stamina'] - 1)
|
||||
|
||||
# Build inspection message
|
||||
lines = [f"🔍 **Inspecting {location.name}**\n"]
|
||||
lines = [get_game_message('inspecting_title', locale, name=location.name)]
|
||||
lines.append(location.description)
|
||||
lines.append("")
|
||||
|
||||
if location.interactables:
|
||||
lines.append("**Interactables:**")
|
||||
lines.append(get_game_message('interactables_title', locale))
|
||||
for interactable in location.interactables:
|
||||
lines.append(f"• **{interactable.name}**")
|
||||
if interactable.actions:
|
||||
@@ -112,13 +112,13 @@ async def inspect_area(player_id: int, location, interactables_data: Dict) -> st
|
||||
lines.append("")
|
||||
|
||||
if location.npcs:
|
||||
lines.append(f"**NPCs:** {', '.join(location.npcs)}")
|
||||
lines.append(f"{get_game_message('npcs_title', locale)} {', '.join(location.npcs)}")
|
||||
lines.append("")
|
||||
|
||||
# Check for dropped items
|
||||
dropped_items = await db.get_dropped_items(location.id)
|
||||
if dropped_items:
|
||||
lines.append("**Items on ground:**")
|
||||
lines.append(get_game_message('items_ground_title', locale))
|
||||
for item in dropped_items:
|
||||
lines.append(f"• {item['item_id']} x{item['quantity']}")
|
||||
|
||||
@@ -130,7 +130,8 @@ async def interact_with_object(
|
||||
interactable_id: str,
|
||||
action_id: str,
|
||||
location,
|
||||
items_manager
|
||||
items_manager,
|
||||
locale: str = 'en'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Interact with an object using a specific action.
|
||||
@@ -148,7 +149,7 @@ async def interact_with_object(
|
||||
break
|
||||
|
||||
if not interactable:
|
||||
return {"success": False, "message": "Object not found"}
|
||||
return {"success": False, "message": get_game_message('object_not_found', locale)}
|
||||
|
||||
# Find the action
|
||||
action = None
|
||||
@@ -158,13 +159,13 @@ async def interact_with_object(
|
||||
break
|
||||
|
||||
if not action:
|
||||
return {"success": False, "message": "Action not found"}
|
||||
return {"success": False, "message": get_game_message('action_not_found', locale)}
|
||||
|
||||
# Check stamina
|
||||
if player['stamina'] < action.stamina_cost:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Not enough stamina. Need {action.stamina_cost}, have {player['stamina']}."
|
||||
"message": get_game_message('not_enough_stamina', locale, cost=action.stamina_cost, current=player['stamina'])
|
||||
}
|
||||
|
||||
# Check cooldown for this specific action
|
||||
@@ -173,7 +174,7 @@ async def interact_with_object(
|
||||
remaining = int(cooldown_expiry - time.time())
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"This action is still on cooldown. Wait {remaining} seconds."
|
||||
"message": get_game_message('cooldown_wait', locale, seconds=remaining)
|
||||
}
|
||||
|
||||
# Deduct stamina
|
||||
@@ -199,7 +200,7 @@ async def interact_with_object(
|
||||
if not outcome:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Action has no defined outcomes"
|
||||
"message": get_game_message('action_no_outcomes', locale)
|
||||
}
|
||||
|
||||
# Process outcome
|
||||
@@ -219,7 +220,7 @@ async def interact_with_object(
|
||||
if not item:
|
||||
continue
|
||||
|
||||
item_name = get_locale_string(item.name) if item else item_id
|
||||
item_name = get_locale_string(item.name, locale) if item else item_id
|
||||
emoji = item.emoji if item and hasattr(item, 'emoji') else ''
|
||||
|
||||
# Check if item has durability (unique item)
|
||||
@@ -240,7 +241,7 @@ async def interact_with_object(
|
||||
max_durability=item.durability,
|
||||
tier=getattr(item, 'tier', None)
|
||||
)
|
||||
items_found.append(f"{emoji} {get_locale_string(item_name)}")
|
||||
items_found.append(f"{emoji} {item_name}")
|
||||
current_weight += item.weight
|
||||
current_volume += item.volume
|
||||
else:
|
||||
@@ -255,7 +256,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} {get_locale_string(item_name)}")
|
||||
items_dropped.append(f"{emoji} {item_name}")
|
||||
else:
|
||||
# Stackable items - process as before
|
||||
item_weight = item.weight * quantity
|
||||
@@ -265,13 +266,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} {get_locale_string(item_name)} x{quantity}")
|
||||
items_found.append(f"{emoji} {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} {get_locale_string(item_name)} x{quantity}")
|
||||
items_dropped.append(f"{emoji} {item_name} x{quantity}")
|
||||
|
||||
# Apply damage
|
||||
if damage_taken > 0:
|
||||
@@ -286,9 +287,9 @@ async def interact_with_object(
|
||||
await db.set_interactable_cooldown(interactable_id, action_id, 60)
|
||||
|
||||
# Build message
|
||||
final_message = get_locale_string(outcome.text)
|
||||
final_message = get_locale_string(outcome.text, locale)
|
||||
if items_dropped:
|
||||
final_message += f"\n⚠️ Inventory full! Dropped to ground: {', '.join(items_dropped)}"
|
||||
final_message += f"\n⚠️ {get_game_message('inventory_full', locale)}! {get_game_message('dropped_to_ground', locale)}: {', '.join(items_dropped)}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -302,7 +303,7 @@ async def interact_with_object(
|
||||
}
|
||||
|
||||
|
||||
async def use_item(player_id: int, item_id: str, items_manager) -> Dict[str, Any]:
|
||||
async def use_item(player_id: int, item_id: str, items_manager, locale: str = 'en') -> Dict[str, Any]:
|
||||
"""
|
||||
Use an item from inventory.
|
||||
Returns: {success, message, effects}
|
||||
@@ -320,7 +321,7 @@ async def use_item(player_id: int, item_id: str, items_manager) -> Dict[str, Any
|
||||
break
|
||||
|
||||
if not item_entry:
|
||||
return {"success": False, "message": "You don't have this item"}
|
||||
return {"success": False, "message": get_game_message('no_item', locale)}
|
||||
|
||||
# Get item data
|
||||
item = items_manager.get_item(item_id)
|
||||
@@ -328,7 +329,7 @@ async def use_item(player_id: int, item_id: str, items_manager) -> Dict[str, Any
|
||||
return {"success": False, "message": "Item not found in game data"}
|
||||
|
||||
if not item.consumable:
|
||||
return {"success": False, "message": "This item cannot be used"}
|
||||
return {"success": False, "message": get_game_message('cannot_use', locale)}
|
||||
|
||||
# Apply item effects
|
||||
effects = {}
|
||||
@@ -366,7 +367,7 @@ async def use_item(player_id: int, item_id: str, items_manager) -> Dict[str, Any
|
||||
await db.update_player_statistics(player_id, **stat_updates)
|
||||
|
||||
# Build message
|
||||
msg = f"Used {item.name}"
|
||||
msg = f"{get_game_message('item_used', locale, name=get_locale_string(item.name, locale))}"
|
||||
if effects_msg:
|
||||
msg += f" ({', '.join(effects_msg)})"
|
||||
|
||||
@@ -377,7 +378,7 @@ async def use_item(player_id: int, item_id: str, items_manager) -> Dict[str, Any
|
||||
}
|
||||
|
||||
|
||||
async def pickup_item(player_id: int, item_id: int, location_id: str, quantity: int = None, items_manager=None) -> Dict[str, Any]:
|
||||
async def pickup_item(player_id: int, item_id: int, location_id: str, quantity: int = None, items_manager=None, locale: str = 'en') -> Dict[str, Any]:
|
||||
"""
|
||||
Pick up an item from the ground.
|
||||
item_id is the dropped_item id, not the item_id field.
|
||||
@@ -389,7 +390,7 @@ async def pickup_item(player_id: int, item_id: int, location_id: str, quantity:
|
||||
dropped_item = await db.get_dropped_item(item_id)
|
||||
|
||||
if not dropped_item:
|
||||
return {"success": False, "message": "Item not found on ground"}
|
||||
return {"success": False, "message": get_game_message('item_not_found_ground', locale)}
|
||||
|
||||
# Get item definition
|
||||
item_def = items_manager.get_item(dropped_item['item_id']) if items_manager else None
|
||||
@@ -402,7 +403,7 @@ async def pickup_item(player_id: int, item_id: int, location_id: str, quantity:
|
||||
pickup_qty = available_qty
|
||||
else:
|
||||
if quantity < 1:
|
||||
return {"success": False, "message": "Invalid quantity"}
|
||||
return {"success": False, "message": get_game_message('invalid_quantity', locale)}
|
||||
pickup_qty = quantity
|
||||
|
||||
# Get player and calculate capacity
|
||||
@@ -423,13 +424,13 @@ async def pickup_item(player_id: int, item_id: int, location_id: str, quantity:
|
||||
if new_weight > max_weight:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"⚠️ Item too heavy! {item_def.emoji} {item_def.name} x{pickup_qty} ({item_weight:.1f}kg) would exceed capacity. Current: {current_weight:.1f}/{max_weight:.1f}kg"
|
||||
"message": get_game_message('item_too_heavy', locale, emoji=item_def.emoji, name=get_locale_string(item_def.name, locale), qty=pickup_qty, weight=item_weight, current=current_weight, max=max_weight)
|
||||
}
|
||||
|
||||
if new_volume > max_volume:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"⚠️ Item too large! {item_def.emoji} {item_def.name} x{pickup_qty} ({item_volume:.1f}L) would exceed capacity. Current: {current_volume:.1f}/{max_volume:.1f}L"
|
||||
"message": get_game_message('item_too_large', locale, emoji=item_def.emoji, name=get_locale_string(item_def.name, locale), qty=pickup_qty, volume=item_volume, current=current_volume, max=max_volume)
|
||||
}
|
||||
|
||||
# Items fit - update dropped item quantity or remove it
|
||||
@@ -449,7 +450,7 @@ async def pickup_item(player_id: int, item_id: int, location_id: str, quantity:
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Picked up {item_def.emoji} {item_def.name} x{pickup_qty}"
|
||||
"message": f"{get_game_message('picked_up', locale)} {item_def.emoji} {get_locale_string(item_def.name, locale)} x{pickup_qty}"
|
||||
}
|
||||
|
||||
|
||||
@@ -538,19 +539,16 @@ def generate_npc_intent(npc_def, combat_state: dict) -> dict:
|
||||
return intent
|
||||
|
||||
|
||||
async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -> Tuple[str, bool]:
|
||||
async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -> Tuple[List[dict], bool]:
|
||||
"""
|
||||
Execute NPC turn based on PREVIOUS intent, then generate NEXT intent.
|
||||
Returns: (messages_list, player_defeated)
|
||||
"""
|
||||
player = await db.get_player_by_id(player_id)
|
||||
if not player:
|
||||
return "Player not found", True
|
||||
return [], True
|
||||
|
||||
# Parse current intent (stored in DB as string or JSON, assuming simple string for now or we parse it)
|
||||
# For now, let's assume simple string "attack", "defend", "special" stored in npc_intent
|
||||
# If we want more complex data, we should use JSON, but the migration added VARCHAR.
|
||||
# Let's stick to simple string for the column, but we can store "type:value" if needed.
|
||||
|
||||
current_intent_str = combat.get('npc_intent', 'attack')
|
||||
# Handle legacy/null
|
||||
if not current_intent_str:
|
||||
@@ -558,17 +556,22 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
|
||||
intent_type = current_intent_str
|
||||
|
||||
message = ""
|
||||
messages = []
|
||||
actual_damage = 0
|
||||
|
||||
# EXECUTE INTENT
|
||||
if intent_type == 'defend':
|
||||
# NPC defends - maybe heals or takes less damage next turn?
|
||||
# For simplicity: Heals 5% HP
|
||||
# NPC defends - heals 5% HP
|
||||
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"{get_locale_string(npc_def.name)} defends and recovers {heal_amount} HP!"
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"enemy_defend",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
heal=heal_amount
|
||||
))
|
||||
|
||||
elif intent_type == 'special':
|
||||
# Strong attack (1.5x damage)
|
||||
@@ -577,54 +580,78 @@ 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"{get_locale_string(npc_def.name)} uses a SPECIAL ATTACK for {npc_damage} damage!"
|
||||
if armor_absorbed > 0:
|
||||
message += f" (Armor absorbed {armor_absorbed})"
|
||||
messages.append(create_combat_message(
|
||||
"enemy_special",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
message += f"\n💔 Your {armor['emoji']} {armor['name']} broke!"
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
|
||||
else: # Default 'attack'
|
||||
npc_damage = random.randint(npc_def.damage_min, npc_def.damage_max)
|
||||
|
||||
# Enrage bonus if NPC is below 30% HP
|
||||
if combat['npc_hp'] / combat['npc_max_hp'] < 0.3:
|
||||
is_enraged = combat['npc_hp'] / combat['npc_max_hp'] < 0.3
|
||||
if is_enraged:
|
||||
npc_damage = int(npc_damage * 1.5)
|
||||
message = f"{get_locale_string(npc_def.name)} is ENRAGED! "
|
||||
else:
|
||||
message = ""
|
||||
messages.append(create_combat_message(
|
||||
"enemy_enraged",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
new_player_hp = max(0, player['hp'] - actual_damage)
|
||||
|
||||
message += f"{get_locale_string(npc_def.name)} attacks for {npc_damage} damage!"
|
||||
if armor_absorbed > 0:
|
||||
message += f" (Armor absorbed {armor_absorbed})"
|
||||
messages.append(create_combat_message(
|
||||
"enemy_attack",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
message += f"\n💔 Your {armor['emoji']} {armor['name']} broke!"
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
|
||||
# GENERATE NEXT INTENT
|
||||
# We need to update the combat state with the new HP values first to make good decisions
|
||||
# But we can just use the values we calculated.
|
||||
|
||||
# Check if player defeated
|
||||
player_defeated = False
|
||||
if player['hp'] - actual_damage <= 0 and intent_type != 'defend': # Check HP after damage
|
||||
# Re-fetch to be sure or just trust calculation
|
||||
if new_player_hp <= 0:
|
||||
message += "\nYou have been defeated!"
|
||||
messages.append(create_combat_message(
|
||||
"player_defeated",
|
||||
origin="neutral",
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
player_defeated = True
|
||||
await db.update_player(player_id, hp=0, is_dead=True)
|
||||
await db.update_player_statistics(player_id, deaths=1, damage_taken=actual_damage, increment=True)
|
||||
await db.end_combat(player_id)
|
||||
return message, player_defeated
|
||||
return messages, player_defeated
|
||||
|
||||
if not player_defeated:
|
||||
if actual_damage > 0:
|
||||
@@ -648,4 +675,4 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
'npc_intent': next_intent['type']
|
||||
})
|
||||
|
||||
return message, player_defeated
|
||||
return messages, player_defeated
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Authentication router.
|
||||
Handles user registration, login, and profile retrieval.
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from fastapi import APIRouter, HTTPException, Depends, status, Request
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..services.helpers import get_game_message
|
||||
|
||||
from ..core.security import create_access_token, hash_password, verify_password, get_current_user
|
||||
from ..services.models import UserRegister, UserLogin
|
||||
from .. import database as db
|
||||
@@ -205,10 +207,12 @@ async def get_account(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
@router.post("/change-email")
|
||||
async def change_email(
|
||||
request: "ChangeEmailRequest",
|
||||
req: Request,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Change account email address"""
|
||||
from ..services.models import ChangeEmailRequest
|
||||
locale = req.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Get account
|
||||
account_id = current_user.get("account_id")
|
||||
@@ -250,7 +254,7 @@ async def change_email(
|
||||
# Update email
|
||||
try:
|
||||
await db.update_account_email(account_id, request.new_email)
|
||||
return {"message": "Email updated successfully", "new_email": request.new_email}
|
||||
return {"message": get_game_message('email_updated', locale), "new_email": request.new_email}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -261,10 +265,12 @@ async def change_email(
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
request: "ChangePasswordRequest",
|
||||
req: Request,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Change account password"""
|
||||
from ..services.models import ChangePasswordRequest
|
||||
locale = req.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Get account
|
||||
account_id = current_user.get("account_id")
|
||||
@@ -305,7 +311,7 @@ async def change_password(
|
||||
new_password_hash = hash_password(request.new_password)
|
||||
await db.update_account_password(account_id, new_password_hash)
|
||||
|
||||
return {"message": "Password updated successfully"}
|
||||
return {"message": get_game_message('password_updated', locale)}
|
||||
|
||||
|
||||
@router.post("/steam-login")
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Character management router.
|
||||
Handles character creation, selection, and deletion.
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from fastapi import APIRouter, HTTPException, Depends, status, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from ..services.helpers import get_game_message
|
||||
|
||||
from ..core.security import decode_token, create_access_token, security
|
||||
from ..services.models import CharacterCreate, CharacterSelect
|
||||
from .. import database as db
|
||||
@@ -51,10 +53,12 @@ async def list_characters(credentials: HTTPAuthorizationCredentials = Depends(se
|
||||
@router.post("")
|
||||
async def create_character_endpoint(
|
||||
character: CharacterCreate,
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||
):
|
||||
"""Create a new character"""
|
||||
token = credentials.credentials
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
payload = decode_token(token)
|
||||
account_id = payload.get("account_id")
|
||||
|
||||
@@ -120,7 +124,7 @@ async def create_character_endpoint(
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Character created successfully",
|
||||
"message": get_game_message('character_created', locale),
|
||||
"character": {
|
||||
"id": new_character["id"],
|
||||
"name": new_character["name"],
|
||||
@@ -203,10 +207,12 @@ async def select_character(
|
||||
@router.delete("/{character_id}")
|
||||
async def delete_character_endpoint(
|
||||
character_id: int,
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||
):
|
||||
"""Delete a character"""
|
||||
token = credentials.credentials
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
payload = decode_token(token)
|
||||
account_id = payload.get("account_id")
|
||||
|
||||
@@ -234,5 +240,5 @@ async def delete_character_endpoint(
|
||||
await db.delete_character(character_id)
|
||||
|
||||
return {
|
||||
"message": f"Character '{character['name']}' deleted successfully"
|
||||
"message": get_game_message('character_deleted', locale, name=character['name'])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
Equipment 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, calculate_crafting_stamina_cost
|
||||
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, calculate_crafting_stamina_cost, get_game_message, get_locale_string
|
||||
from .. import database as db
|
||||
from ..items import ItemsManager
|
||||
from .. import game_logic
|
||||
@@ -41,10 +41,12 @@ router = APIRouter(tags=["equipment"])
|
||||
@router.post("/api/game/equip")
|
||||
async def equip_item(
|
||||
equip_req: EquipItemRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Equip an item from inventory"""
|
||||
player_id = current_user['id']
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Get the inventory item
|
||||
inv_item = await db.get_inventory_item_by_id(equip_req.inventory_id)
|
||||
@@ -107,9 +109,9 @@ async def equip_item(
|
||||
|
||||
# Build message
|
||||
if unequipped_item_name:
|
||||
message = f"Unequipped {unequipped_item_name}, equipped {item_def.name}"
|
||||
message = get_game_message('unequip_equip', locale, old=unequipped_item_name, new=get_locale_string(item_def.name, locale))
|
||||
else:
|
||||
message = f"Equipped {item_def.name}"
|
||||
message = get_game_message('equipped', locale, item=get_locale_string(item_def.name, locale))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -122,10 +124,12 @@ async def equip_item(
|
||||
@router.post("/api/game/unequip")
|
||||
async def unequip_item(
|
||||
unequip_req: UnequipItemRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Unequip an item from equipment slot"""
|
||||
player_id = current_user['id']
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Check if slot is valid
|
||||
valid_slots = ['head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack']
|
||||
@@ -190,7 +194,7 @@ async def unequip_item(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Unequipped {item_def.name} (dropped to ground - inventory full)",
|
||||
"message": get_game_message('unequip_dropped', locale, item=get_locale_string(item_def.name, locale)),
|
||||
"dropped": True
|
||||
}
|
||||
|
||||
@@ -200,7 +204,7 @@ async def unequip_item(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Unequipped {item_def.name}",
|
||||
"message": get_game_message('unequipped', locale, item=get_locale_string(item_def.name, locale)),
|
||||
"dropped": False
|
||||
}
|
||||
|
||||
@@ -241,10 +245,12 @@ async def get_equipment(current_user: dict = Depends(get_current_user)):
|
||||
@router.post("/api/game/repair_item")
|
||||
async def repair_item(
|
||||
repair_req: RepairItemRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Repair an item using materials at a workbench location"""
|
||||
player_id = current_user['id']
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Get player's location
|
||||
player = await db.get_player_by_id(player_id)
|
||||
@@ -358,7 +364,7 @@ async def repair_item(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Repaired {item_def.name}! Restored {repair_amount} durability.",
|
||||
"message": get_game_message('repaired_success', locale, item=get_locale_string(item_def.name, locale), amount=repair_amount),
|
||||
"item_name": item_def.name,
|
||||
"old_durability": current_durability,
|
||||
"new_durability": new_durability,
|
||||
|
||||
@@ -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
|
||||
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, get_game_message
|
||||
from .. import database as db
|
||||
from ..items import ItemsManager
|
||||
from .. import game_logic
|
||||
@@ -757,7 +757,7 @@ async def move(
|
||||
if cooldown_remaining > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"You must wait {int(cooldown_remaining)} seconds before moving again."
|
||||
detail=get_game_message('move_cooldown', locale, seconds=int(cooldown_remaining))
|
||||
)
|
||||
|
||||
# Extract locale from Accept-Language header
|
||||
@@ -870,7 +870,7 @@ async def move(
|
||||
response["encounter"] = {
|
||||
"triggered": True,
|
||||
"enemy_id": enemy_id,
|
||||
"message": f"⚠️ An enemy ambushes you upon arrival!",
|
||||
"message": get_game_message('enemy_ambush', locale),
|
||||
"combat": combat_data
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ async def move(
|
||||
{
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} left the area",
|
||||
"message": get_game_message('player_left', locale, player_name=player['name']),
|
||||
"action": "player_left",
|
||||
"player_id": current_user['id'],
|
||||
"player_name": player['name']
|
||||
@@ -897,7 +897,7 @@ async def move(
|
||||
{
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} arrived",
|
||||
"message": get_game_message('player_arrived', locale, player_name=player['name']),
|
||||
"action": "player_arrived",
|
||||
"player_id": current_user['id'],
|
||||
"player_name": player['name'],
|
||||
@@ -930,8 +930,11 @@ async def move(
|
||||
|
||||
|
||||
@router.post("/api/game/inspect")
|
||||
async def inspect(current_user: dict = Depends(get_current_user)):
|
||||
async def inspect(request: Request, current_user: dict = Depends(get_current_user)):
|
||||
"""Inspect the current area"""
|
||||
# Extract locale from Accept-Language header
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
location_id = current_user['location_id']
|
||||
location = LOCATIONS.get(location_id)
|
||||
|
||||
@@ -947,7 +950,8 @@ async def inspect(current_user: dict = Depends(get_current_user)):
|
||||
message = await game_logic.inspect_area(
|
||||
current_user['id'],
|
||||
location,
|
||||
{} # interactables_data - not needed with new structure
|
||||
{}, # interactables_data - not needed with new structure
|
||||
locale
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -971,7 +975,7 @@ async def interact(
|
||||
if combat:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot interact with objects while in combat"
|
||||
detail=get_game_message('interact_in_combat', locale)
|
||||
)
|
||||
|
||||
location_id = current_user['location_id']
|
||||
@@ -988,7 +992,8 @@ async def interact(
|
||||
interact_req.interactable_id,
|
||||
interact_req.action_id,
|
||||
location,
|
||||
ITEMS_MANAGER
|
||||
ITEMS_MANAGER,
|
||||
locale
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
@@ -1052,6 +1057,7 @@ async def interact(
|
||||
@router.post("/api/game/use_item")
|
||||
async def use_item(
|
||||
use_req: UseItemRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Use an item from inventory"""
|
||||
@@ -1064,10 +1070,14 @@ async def use_item(
|
||||
combat = await db.get_active_combat(current_user['id'])
|
||||
in_combat = combat is not None
|
||||
|
||||
# Extract locale from Accept-Language header
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
result = await game_logic.use_item(
|
||||
current_user['id'],
|
||||
use_req.item_id,
|
||||
ITEMS_MANAGER
|
||||
ITEMS_MANAGER,
|
||||
locale
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
@@ -1087,10 +1097,10 @@ async def use_item(
|
||||
npc_damage = int(npc_damage * 1.5)
|
||||
|
||||
new_player_hp = max(0, player['hp'] - npc_damage)
|
||||
combat_message = f"\n{npc_def.name} attacks for {npc_damage} damage!"
|
||||
combat_message = get_game_message('combat_enemy_attack', locale, name=npc_def.name, damage=npc_damage)
|
||||
|
||||
if new_player_hp <= 0:
|
||||
combat_message += "\nYou have been defeated!"
|
||||
combat_message += get_game_message('combat_defeated', locale)
|
||||
await db.update_player(current_user['id'], hp=0, is_dead=True)
|
||||
await db.end_combat(current_user['id'])
|
||||
result['combat_over'] = True
|
||||
@@ -1149,7 +1159,7 @@ async def use_item(
|
||||
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 # Send full corpse data
|
||||
@@ -1194,7 +1204,8 @@ async def pickup(
|
||||
pickup_req.item_id,
|
||||
current_user['location_id'],
|
||||
pickup_req.quantity,
|
||||
ITEMS_MANAGER
|
||||
ITEMS_MANAGER,
|
||||
locale
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
@@ -1214,7 +1225,7 @@ async def pickup(
|
||||
{
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} picked up {quantity}x {item_name}",
|
||||
"message": f"{player['name']} {get_game_message('picked_up', locale).lower()} {quantity}x {item_name}",
|
||||
"action": "item_picked_up"
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
@@ -1336,6 +1347,7 @@ async def get_inventory(current_user: dict = Depends(get_current_user)):
|
||||
@router.post("/api/game/item/drop")
|
||||
async def drop_item(
|
||||
drop_req: dict,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Drop an item from inventory"""
|
||||
@@ -1343,6 +1355,9 @@ async def drop_item(
|
||||
item_id = drop_req.get('item_id') # This is the item_id string like "energy_bar"
|
||||
quantity = drop_req.get('quantity', 1)
|
||||
|
||||
# Extract locale from Accept-Language header
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Get player to know their location
|
||||
player = await db.get_player_by_id(player_id)
|
||||
if not player:
|
||||
@@ -1400,7 +1415,7 @@ async def drop_item(
|
||||
message={
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} dropped {item_def.emoji} {item_def.name} x{quantity}",
|
||||
"message": get_game_message('dropped_item_success', locale, emoji=item_def.emoji, name=get_locale_string(item_def.name, locale), qty=quantity).replace('You', player['name']).replace('Has tirado', f"{player['name']} ha tirado"),
|
||||
"action": "item_dropped"
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
@@ -1410,5 +1425,5 @@ async def drop_item(
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Dropped {item_def.emoji} {get_locale_string(item_def.name)} x{quantity}"
|
||||
"message": get_game_message('dropped_item_success', locale, emoji=item_def.emoji, name=get_locale_string(item_def.name, locale), qty=quantity)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
Loot 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
|
||||
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, get_game_message
|
||||
from .. import database as db
|
||||
from ..items import ItemsManager
|
||||
from .. import game_logic
|
||||
@@ -42,6 +42,7 @@ router = APIRouter(tags=["loot"])
|
||||
@router.get("/api/game/corpse/{corpse_id}")
|
||||
async def get_corpse_details(
|
||||
corpse_id: str,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get detailed information about a corpse's lootable items"""
|
||||
@@ -50,6 +51,9 @@ async def get_corpse_details(
|
||||
sys.path.insert(0, '/app')
|
||||
from data.npcs import NPCS
|
||||
|
||||
# Extract locale
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Parse corpse ID
|
||||
corpse_type, corpse_db_id = corpse_id.split('_', 1)
|
||||
corpse_db_id = int(corpse_db_id)
|
||||
@@ -99,7 +103,7 @@ async def get_corpse_details(
|
||||
return {
|
||||
'corpse_id': corpse_id,
|
||||
'type': 'npc',
|
||||
'name': f"{npc_def.name if npc_def else corpse['npc_id']} Corpse",
|
||||
'name': get_game_message('corpse_name_npc', locale, name=get_locale_string(npc_def.name, locale) if npc_def else corpse['npc_id']),
|
||||
'loot_items': loot_items,
|
||||
'total_items': len(loot_items)
|
||||
}
|
||||
@@ -137,7 +141,7 @@ async def get_corpse_details(
|
||||
return {
|
||||
'corpse_id': corpse_id,
|
||||
'type': 'player',
|
||||
'name': f"{corpse['player_name']}'s Corpse",
|
||||
'name': get_game_message('corpse_name_player', locale, name=corpse['player_name']),
|
||||
'loot_items': loot_items,
|
||||
'total_items': len(loot_items)
|
||||
}
|
||||
@@ -149,6 +153,7 @@ async def get_corpse_details(
|
||||
@router.post("/api/game/loot_corpse")
|
||||
async def loot_corpse(
|
||||
req: LootCorpseRequest,
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Loot a corpse (NPC or player) - can loot specific item by index or all items"""
|
||||
@@ -158,6 +163,9 @@ async def loot_corpse(
|
||||
sys.path.insert(0, '/app')
|
||||
from data.npcs import NPCS
|
||||
|
||||
# Extract locale
|
||||
locale = request.headers.get('Accept-Language', 'en')
|
||||
|
||||
# Parse corpse ID
|
||||
corpse_type, corpse_db_id = req.corpse_id.split('_', 1)
|
||||
corpse_db_id = int(corpse_db_id)
|
||||
@@ -310,26 +318,26 @@ async def loot_corpse(
|
||||
message_parts = []
|
||||
for item in looted_items:
|
||||
item_def = ITEMS_MANAGER.get_item(item['item_id'])
|
||||
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
|
||||
item_name = get_locale_string(item_def.name, locale) 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 = get_locale_string(item_def.name) if item_def else item['item_id']
|
||||
item_name = get_locale_string(item_def.name, locale) if item_def else item['item_id']
|
||||
dropped_parts.append(f"{item.get('emoji', '📦')} {item_name} x{item['quantity']}")
|
||||
|
||||
message = ""
|
||||
if message_parts:
|
||||
message = "Looted: " + ", ".join(message_parts)
|
||||
message = get_game_message('looted_items_start', locale) + ", ".join(message_parts)
|
||||
if dropped_parts:
|
||||
if message:
|
||||
message += "\n"
|
||||
message += "⚠️ Backpack full! Dropped on ground: " + ", ".join(dropped_parts)
|
||||
message += get_game_message('backpack_full_drop', locale) + ", ".join(dropped_parts)
|
||||
if not message_parts and not dropped_parts:
|
||||
message = "Nothing could be looted"
|
||||
message = get_game_message('nothing_looted', locale)
|
||||
if remaining_loot and req.item_index is None:
|
||||
message += f"\n{len(remaining_loot)} item(s) require tools to extract"
|
||||
message += "\n" + get_game_message('items_require_tools', locale, count=len(remaining_loot))
|
||||
|
||||
# Broadcast to location about corpse looting
|
||||
if len(remaining_loot) == 0:
|
||||
@@ -339,7 +347,7 @@ async def loot_corpse(
|
||||
message={
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} fully looted an NPC corpse",
|
||||
"message": get_game_message('full_loot_broadcast', locale, player_name=player['name']),
|
||||
"action": "corpse_looted"
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
@@ -438,24 +446,24 @@ async def loot_corpse(
|
||||
message_parts = []
|
||||
for item in looted_items:
|
||||
item_def = ITEMS_MANAGER.get_item(item['item_id'])
|
||||
item_name = get_locale_string(item_def.name) if item_def else item['item_id']
|
||||
item_name = get_locale_string(item_def.name, locale) 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 = get_locale_string(item_def.name) if item_def else item['item_id']
|
||||
item_name = get_locale_string(item_def.name, locale) if item_def else item['item_id']
|
||||
dropped_parts.append(f"{item.get('emoji', '📦')} {item_name} x{item['quantity']}")
|
||||
|
||||
message = ""
|
||||
if message_parts:
|
||||
message = "Looted: " + ", ".join(message_parts)
|
||||
message = get_game_message('looted_items_start', locale) + ", ".join(message_parts)
|
||||
if dropped_parts:
|
||||
if message:
|
||||
message += "\n"
|
||||
message += "⚠️ Backpack full! Dropped on ground: " + ", ".join(dropped_parts)
|
||||
message += get_game_message('backpack_full_drop', locale) + ", ".join(dropped_parts)
|
||||
if not message_parts and not dropped_parts:
|
||||
message = "Nothing could be looted"
|
||||
message = get_game_message('nothing_looted', locale)
|
||||
|
||||
# Broadcast to location about corpse looting
|
||||
if len(remaining_items) == 0:
|
||||
@@ -465,7 +473,7 @@ async def loot_corpse(
|
||||
message={
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} fully looted {corpse['player_name']}'s corpse",
|
||||
"message": get_game_message('player_corpse_emptied_broadcast', locale, player_name=player['name'], corpse_name=corpse['player_name']),
|
||||
"action": "player_corpse_emptied",
|
||||
"corpse_id": req.corpse_id
|
||||
},
|
||||
@@ -480,7 +488,7 @@ async def loot_corpse(
|
||||
message={
|
||||
"type": "location_update",
|
||||
"data": {
|
||||
"message": f"{player['name']} looted from {corpse['player_name']}'s corpse",
|
||||
"message": get_game_message('player_corpse_looted_broadcast', locale, player_name=player['name'], corpse_name=corpse['player_name']),
|
||||
"action": "player_corpse_looted",
|
||||
"corpse_id": req.corpse_id,
|
||||
"remaining_items": remaining_items,
|
||||
|
||||
@@ -15,7 +15,97 @@ def get_locale_string(value: Union[str, Dict[str, str]], lang: str = 'en') -> st
|
||||
return str(value)
|
||||
|
||||
|
||||
|
||||
# Translation maps for backend messages
|
||||
GAME_MESSAGES = {
|
||||
# Pickup
|
||||
'picked_up': {'en': 'Picked up', 'es': 'Has cogido'},
|
||||
'inventory_full': {'en': 'Inventory full', 'es': 'Inventario lleno'},
|
||||
'dropped_to_ground': {'en': 'Dropped to ground', 'es': 'Tirado al suelo'},
|
||||
'item_too_heavy': {
|
||||
'en': "⚠️ Item too heavy! {emoji} {name} x{qty} ({weight:.1f}kg) would exceed capacity. Current: {current:.1f}/{max:.1f}kg",
|
||||
'es': "⚠️ ¡Objeto muy pesado! {emoji} {name} x{qty} ({weight:.1f}kg) excedería la capacidad. Actual: {current:.1f}/{max:.1f}kg"
|
||||
},
|
||||
'item_too_large': {
|
||||
'en': "⚠️ Item too large! {emoji} {name} x{qty} ({volume:.1f}L) would exceed capacity. Current: {current:.1f}/{max:.1f}L",
|
||||
'es': "⚠️ ¡Objeto muy grande! {emoji} {name} x{qty} ({volume:.1f}L) excedería la capacidad. Actual: {current:.1f}/{max:.1f}L"
|
||||
},
|
||||
'item_not_found_ground': {'en': "Item not found on ground", 'es': "Objeto no encontrado en el suelo"},
|
||||
'invalid_quantity': {'en': "Invalid quantity", 'es': "Cantidad inválida"},
|
||||
'dropped_item_success': {'en': 'Dropped {emoji} {name} x{qty}', 'es': 'Has tirado {emoji} {name} x{qty}'},
|
||||
|
||||
# Movement
|
||||
'cannot_go_direction': {'en': "You cannot go {direction} from here.", 'es': "No puedes ir al {direction} desde aquí."},
|
||||
'exhausted_move': {'en': "You're too exhausted to move. Wait for your stamina to regenerate.", 'es': "Estás demasiado exhausto para moverte. Espera a recuperar stamina."},
|
||||
'move_cooldown': {'en': 'You must wait {seconds} seconds before moving again.', 'es': 'Debes esperar {seconds} segundos antes de moverte de nuevo.'},
|
||||
'enemy_ambush': {'en': '⚠️ An enemy ambushes you upon arrival!', 'es': '⚠️ ¡Un enemigo te tiende una emboscada al llegar!'},
|
||||
'player_left': {'en': '{player_name} left the area', 'es': '{player_name} abandonó el área'},
|
||||
'player_arrived': {'en': '{player_name} arrived', 'es': '{player_name} ha llegado'},
|
||||
'player_defeated_broadcast': {'en': '{player_name} was defeated in combat', 'es': '{player_name} fue derrotado en combate'},
|
||||
'player_defeated_enemy_broadcast': {'en': '{player_name} defeated {npc_name}', 'es': '{player_name} derrotó a {npc_name}'},
|
||||
'player_fled_broadcast': {'en': '{player_name} fled from combat', 'es': '{player_name} huyó del combate'},
|
||||
'player_entered_combat': {'en': '{player_name} entered combat with {npc_name}', 'es': '{player_name} entró en combate con {npc_name}'},
|
||||
'player_returned_pvp': {'en': '{player_name} has returned from PvP combat.', 'es': '{player_name} ha regresado del combate PvP.'},
|
||||
|
||||
'player_entered_combat': {'en': '{player_name} entered combat with {npc_name}', 'es': '{player_name} entró en combate con {npc_name}'},
|
||||
'player_returned_pvp': {'en': '{player_name} has returned from PvP combat.', 'es': '{player_name} ha regresado del combate PvP.'},
|
||||
'pvp_defeat_broadcast': {'en': '{opponent} was defeated by {winner} in PvP combat', 'es': '{opponent} fue derrotado por {winner} en combate PvP'},
|
||||
'pvp_initiated_attacker': {'en': "You have initiated combat with {defender}! They get the first turn.", 'es': "¡Has iniciado combate con {defender}! Tiene el primer turno."},
|
||||
'pvp_challenged_defender': {'en': "{attacker} has challenged you to PvP combat! It's your turn.", 'es': "¡{attacker} te ha desafiado a combate PvP! Es tu turno."},
|
||||
|
||||
# Loot
|
||||
'corpse_name_npc': {'en': "{name} Corpse", 'es': "Cadáver de {name}"},
|
||||
'corpse_name_player': {'en': "{name}'s Corpse", 'es': "Cadáver de {name}"},
|
||||
'looted_items_start': {'en': "Looted: ", 'es': "Saqueado: "},
|
||||
'backpack_full_drop': {'en': "⚠️ Backpack full! Dropped on ground: ", 'es': "⚠️ ¡Mochila llena! Tirado al suelo: "},
|
||||
'nothing_looted': {'en': "Nothing could be looted", 'es': "No se pudo saquear nada"},
|
||||
'items_require_tools': {'en': "{count} item(s) require tools to extract", 'es': "{count} objeto(s) requieren herramientas"},
|
||||
'full_loot_broadcast': {'en': "{player_name} fully looted an NPC corpse", 'es': "{player_name} saqueó completamente un cadáver de NPC"},
|
||||
'player_corpse_emptied_broadcast': {'en': "{player_name} fully looted {corpse_name}'s corpse", 'es': "{player_name} vació el cadáver de {corpse_name}"},
|
||||
'player_corpse_looted_broadcast': {'en': "{player_name} looted from {corpse_name}'s corpse", 'es': "{player_name} saqueó del cadáver de {corpse_name}"},
|
||||
|
||||
# Equipment
|
||||
'unequip_equip': {'en': "Unequipped {old}, equipped {new}", 'es': "Desequipado {old}, equipado {new}"},
|
||||
'equipped': {'en': "Equipped {item}", 'es': "Equipado {item}"},
|
||||
'unequipped': {'en': "Unequipped {item}", 'es': "Desequipado {item}"},
|
||||
'unequip_dropped': {'en': "Unequipped {item} (dropped to ground - inventory full)", 'es': "Desequipado {item} (tirado al suelo - inventario lleno)"},
|
||||
'repaired_success': {'en': "Repaired {item}! Restored {amount} durability.", 'es': "¡Reparado {item}! Restaurados {amount} puntos de durabilidad."},
|
||||
|
||||
# Characters/Auth
|
||||
'character_created': {'en': "Character created successfully", 'es': "Personaje creado con éxito"},
|
||||
'character_deleted': {'en': "Character '{name}' deleted successfully", 'es': "Personaje '{name}' eliminado con éxito"},
|
||||
'email_updated': {'en': "Email updated successfully", 'es': "Email actualizado con éxito"},
|
||||
'password_updated': {'en': "Password updated successfully", 'es': "Contraseña actualizada con éxito"},
|
||||
|
||||
# Inspection
|
||||
'exhausted_inspect': {'en': "You're too exhausted to inspect the area thoroughly. Wait for your stamina to regenerate.", 'es': "Estás demasiado exhausto para inspeccionar. Espera a recuperar stamina."},
|
||||
'inspecting_title': {'en': "🔍 **Inspecting {name}**\n", 'es': "🔍 **Inspeccionando {name}**\n"},
|
||||
'interactables_title': {'en': "**Interactables:**", 'es': "**Objetos interactuables:**"},
|
||||
'npcs_title': {'en': "**NPCs:**", 'es': "**NPCs:**"},
|
||||
'items_ground_title': {'en': "**Items on ground:**", 'es': "**Objetos en el suelo:**"},
|
||||
|
||||
# Interaction
|
||||
'not_enough_stamina': {'en': "Not enough stamina. Need {cost}, have {current}.", 'es': "No tienes suficiente stamina. Necesitas {cost}, tienes {current}."},
|
||||
'cooldown_wait': {'en': "This action is still on cooldown. Wait {seconds} seconds.", 'es': "Esta acción está en enfriamiento. Espera {seconds} segundos."},
|
||||
'object_not_found': {'en': "Object not found", 'es': "Objeto no encontrado"},
|
||||
'action_not_found': {'en': "Action not found", 'es': "Acción no encontrada"},
|
||||
'action_no_outcomes': {'en': "Action has no defined outcomes", 'es': "La acción no tiene resultados definidos"},
|
||||
|
||||
# Item Usage
|
||||
'item_used': {'en': "Used {name}", 'es': "Usado {name}"},
|
||||
'no_item': {'en': "You don't have this item", 'es': "No tienes este objeto"},
|
||||
'cannot_use': {'en': "This item cannot be used", 'es': "Este objeto no se puede usar"}
|
||||
}
|
||||
|
||||
def get_game_message(key: str, lang: str = 'en', **kwargs) -> str:
|
||||
"""Get and format a localized game message."""
|
||||
messages = GAME_MESSAGES.get(key, {})
|
||||
template = messages.get(lang) or messages.get('en') or key
|
||||
try:
|
||||
return template.format(**kwargs)
|
||||
except KeyError:
|
||||
return template
|
||||
|
||||
DIRECTION_TRANSLATIONS = {
|
||||
'north': {'en': 'north', 'es': 'norte'},
|
||||
'south': {'en': 'south', 'es': 'sur'},
|
||||
@@ -39,8 +129,8 @@ def translate_travel_message(direction: str, location_name: str, lang: str = 'en
|
||||
|
||||
import json
|
||||
|
||||
def create_combat_message(message_type: str, origin: str = "neutral", **data) -> str:
|
||||
"""Create a structured combat message with type, origin, and data.
|
||||
def create_combat_message(message_type: str, origin: str = "neutral", **data) -> dict:
|
||||
"""Create a structured combat message object.
|
||||
|
||||
Args:
|
||||
message_type: Type of combat message (combat_start, player_attack, etc.)
|
||||
@@ -48,13 +138,13 @@ def create_combat_message(message_type: str, origin: str = "neutral", **data) ->
|
||||
**data: Dynamic data for the message (damage, npc_name, etc.)
|
||||
|
||||
Returns:
|
||||
JSON string with 'type', 'origin', and 'data' fields
|
||||
Dictionary with 'type', 'origin', and 'data' fields
|
||||
"""
|
||||
return json.dumps({
|
||||
return {
|
||||
"type": message_type,
|
||||
"origin": origin,
|
||||
"data": data
|
||||
})
|
||||
}
|
||||
|
||||
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user