WIP: Current state before PVP combat investigation

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

View File

@@ -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