WIP: Current state before PVP combat investigation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user