Pre-menu-integration snapshot: combat, crafting, status effects, gamedata updates

This commit is contained in:
Joan
2026-03-11 12:43:23 +01:00
parent d5afd28eb9
commit a8dc8211d5
36 changed files with 1724 additions and 404 deletions

View File

@@ -17,13 +17,14 @@ from ..services.constants import PVP_TURN_TIMEOUT
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, get_game_message
from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, get_locale_string, create_combat_message, get_game_message, get_resolved_player_effects
from .. import database as db
from ..items import ItemsManager
from .. import game_logic
from ..core.websockets import manager
from .equipment import reduce_armor_durability
from ..services import combat_engine
from ..services.status_effects import status_effects_manager
logger = logging.getLogger(__name__)
@@ -70,6 +71,27 @@ async def get_combat_status(current_user: dict = Depends(get_current_user)):
time_elapsed = time.time() - turn_started_at
turn_time_remaining = max(0, 300 - time_elapsed)
# Parse NPC status effects
npc_effects_list = []
npc_status_str = combat.get('npc_status_effects', '') or ''
if npc_status_str:
for part in npc_status_str.split('|'):
tokens = part.split(':')
effect_name = tokens[0] if len(tokens) > 0 else ''
if not effect_name:
continue
ticks = int(tokens[2]) if len(tokens) > 2 else (int(tokens[1]) if len(tokens) > 1 else 0)
info = status_effects_manager.get_effect_info(effect_name)
npc_effects_list.append({
'name': info['name'],
'icon': info['icon'],
'ticks_remaining': ticks,
'description': info['description'],
})
# Get player active buffs/debuffs (exclude cooldowns)
player_effects = await get_resolved_player_effects(current_user['id'], in_combat=True)
return {
"in_combat": True,
"combat": {
@@ -80,8 +102,11 @@ async def get_combat_status(current_user: dict = Depends(get_current_user)):
"npc_image": f"{npc_def.image_path}" if npc_def else None,
"turn": combat['turn'],
"round": combat.get('round', 1),
"turn_time_remaining": turn_time_remaining
}
"turn_time_remaining": turn_time_remaining,
"npc_effects": npc_effects_list,
"npc_intent": combat.get('npc_intent', 'attack')
},
"player_effects": player_effects
}
@@ -154,8 +179,10 @@ async def initiate_combat(
"npc_max_hp": npc_hp,
"npc_image": f"{npc_def.image_path}",
"turn": "player",
"round": 1
}
"round": 1,
"npc_intent": "attack"
},
"player_effects": await get_resolved_player_effects(current_user['id'], in_combat=True)
},
"timestamp": datetime.utcnow().isoformat()
})
@@ -185,8 +212,10 @@ async def initiate_combat(
"npc_max_hp": npc_hp,
"npc_image": f"{npc_def.image_path}",
"turn": "player",
"round": 1
}
"round": 1,
"npc_intent": "attack"
},
"player_effects": await get_resolved_player_effects(current_user['id'], in_combat=True)
}
@@ -303,15 +332,20 @@ async def combat_action(
exclude_player_id=player['id']
)
else:
# Fetch fresh combat state to capture any player buffs applied
fresh_combat = await db.get_active_combat(player['id'])
st_effects = fresh_combat.get('npc_status_effects', '') if fresh_combat else combat.get('npc_status_effects', '')
# NPC turn
npc_msgs, player_defeated = await combat_engine.execute_npc_turn(
player['id'],
{'npc_hp': new_npc_hp, 'npc_max_hp': combat['npc_max_hp'],
'npc_intent': combat.get('npc_intent', 'attack'),
'npc_status_effects': combat.get('npc_status_effects', '')},
'npc_status_effects': st_effects},
npc_def,
reduce_armor_durability,
redis_manager
redis_manager,
locale=locale
)
messages.extend(npc_msgs)
@@ -336,6 +370,7 @@ async def combat_action(
items_manager=ITEMS_MANAGER,
reduce_armor_func=reduce_armor_durability,
redis_manager=redis_manager,
locale=locale
)
if result.get('error'):
@@ -372,21 +407,28 @@ async def combat_action(
exclude_player_id=player['id']
)
else:
# Fetch fresh combat state to capture effects applied by the skill
fresh_combat = await db.get_active_combat(player['id'])
st_effects = fresh_combat.get('npc_status_effects', '') if fresh_combat else combat.get('npc_status_effects', '')
# NPC turn after skill
npc_msgs, player_defeated = await combat_engine.execute_npc_turn(
player['id'],
{'npc_hp': new_npc_hp, 'npc_max_hp': combat['npc_max_hp'],
'npc_intent': combat.get('npc_intent', 'attack'),
'npc_status_effects': combat.get('npc_status_effects', '')},
'npc_status_effects': st_effects},
npc_def,
reduce_armor_durability,
redis_manager
redis_manager,
locale=locale
)
messages.extend(npc_msgs)
if player_defeated:
await db.remove_non_persistent_effects(player['id'])
combat_over = True
else:
await db.update_combat(player['id'], {'npc_hp': new_npc_hp})
# ── USE ITEM ──
elif req.action == 'use_item':
@@ -421,15 +463,20 @@ async def combat_action(
messages.extend(victory['messages'])
quest_updates = victory.get('quest_updates', [])
elif not combat_over:
# Fetch fresh combat state to capture effects applied by the item
fresh_combat = await db.get_active_combat(player['id'])
st_effects = fresh_combat.get('npc_status_effects', '') if fresh_combat else combat.get('npc_status_effects', '')
# NPC turn after item use
npc_msgs, player_defeated = await combat_engine.execute_npc_turn(
player['id'],
{'npc_hp': result.get('target_hp', combat['npc_hp']), 'npc_max_hp': combat['npc_max_hp'],
'npc_intent': combat.get('npc_intent', 'attack'),
'npc_status_effects': combat.get('npc_status_effects', '')},
'npc_status_effects': st_effects},
npc_def,
reduce_armor_durability,
redis_manager
redis_manager,
locale=locale
)
messages.extend(npc_msgs)
@@ -440,6 +487,38 @@ async def combat_action(
# Update NPC HP from throwable damage
if result.get('target_hp') is not None and result['target_hp'] != combat['npc_hp']:
await db.update_combat(player['id'], {'npc_hp': result['target_hp']})
# ── DEFEND ──
elif req.action == 'defend':
result = await combat_engine.execute_defend(
player_id=player['id'],
player=player,
player_stats=stats,
is_pvp=False,
locale=locale,
)
messages.extend(result['messages'])
# Fetch fresh combat state since defend could've updated stats (stamina)
fresh_combat = await db.get_active_combat(player['id'])
st_effects = fresh_combat.get('npc_status_effects', '') if fresh_combat else combat.get('npc_status_effects', '')
# NPC turn after defend
npc_msgs, player_defeated = await combat_engine.execute_npc_turn(
player['id'],
{'npc_hp': combat['npc_hp'], 'npc_max_hp': combat['npc_max_hp'],
'npc_intent': combat.get('npc_intent', 'attack'),
'npc_status_effects': st_effects},
npc_def,
reduce_armor_durability,
redis_manager,
locale=locale
)
messages.extend(npc_msgs)
if player_defeated:
await db.remove_non_persistent_effects(player['id'])
combat_over = True
# ── FLEE ──
elif req.action == 'flee':
@@ -491,6 +570,7 @@ async def combat_action(
# ── Build response ──
updated_combat = None
npc_effects_list = []
if not combat_over:
raw_combat = await db.get_active_combat(current_user['id'])
if raw_combat:
@@ -499,6 +579,23 @@ async def combat_action(
turn_started_at = raw_combat.get('turn_started_at', 0)
turn_time_remaining = max(0, 300 - (time.time() - turn_started_at))
# Parse NPC status effects string into a list
npc_status_str = raw_combat.get('npc_status_effects', '') or ''
if npc_status_str:
for part in npc_status_str.split('|'):
tokens = part.split(':')
effect_name = tokens[0] if len(tokens) > 0 else ''
if not effect_name:
continue
ticks = int(tokens[2]) if len(tokens) > 2 else (int(tokens[1]) if len(tokens) > 1 else 0)
info = status_effects_manager.get_effect_info(effect_name)
npc_effects_list.append({
'name': info['name'],
'icon': info['icon'],
'ticks_remaining': ticks,
'description': info['description'],
})
updated_combat = {
"npc_id": raw_combat['npc_id'],
"npc_name": npc_def.name,
@@ -507,13 +604,75 @@ async def combat_action(
"npc_image": f"{npc_def.image_path}",
"turn": raw_combat['turn'],
"round": raw_combat.get('round', 1),
"turn_time_remaining": turn_time_remaining
"turn_time_remaining": turn_time_remaining,
"npc_effects": npc_effects_list,
"npc_intent": raw_combat.get('npc_intent', 'attack')
}
# Get player active buffs/debuffs (exclude cooldowns)
player_effects = []
if not combat_over:
from ..services.skills import skills_manager
all_effects = await db.get_player_effects(current_user['id'])
for eff in all_effects:
if eff.get('effect_type') == 'cooldown':
continue
resolved = status_effects_manager.resolve_player_effect(
eff.get('effect_name', ''),
eff.get('effect_icon', ''),
eff.get('source', ''),
skills_manager
)
player_effects.append({
'name': resolved['name'],
'icon': resolved['icon'],
'ticks_remaining': eff.get('ticks_remaining', 0),
'type': eff.get('effect_type', 'buff'),
'description': resolved['description'],
})
updated_player = await db.get_player_by_id(current_user['id'])
if not updated_player:
updated_player = current_user
equipment_slots = await db.get_all_equipment(current_user['id'])
equipment = {}
for slot, item_data in equipment_slots.items():
if item_data and item_data['item_id']:
inv_item = await db.get_inventory_item_by_id(item_data['item_id'])
if inv_item:
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
if item_def:
# Get unique item data if this is a unique item
durability = None
max_durability = None
tier = None
unique_stats = None
if inv_item.get('unique_item_id'):
unique_item = await db.get_unique_item(inv_item['unique_item_id'])
if unique_item:
durability = unique_item.get('durability')
max_durability = unique_item.get('max_durability')
tier = unique_item.get('tier')
unique_stats = unique_item.get('unique_stats')
equipment[slot] = {
"inventory_id": item_data['item_id'],
"item_id": item_def.id,
"name": item_def.name,
"description": item_def.description,
"emoji": item_def.emoji,
"image_path": item_def.image_path,
"durability": durability if durability is not None else None,
"max_durability": max_durability if max_durability is not None else None,
"tier": tier if tier is not None else None,
"unique_stats": unique_stats,
"stats": item_def.stats,
"encumbrance": item_def.encumbrance,
"weapon_effects": item_def.weapon_effects if hasattr(item_def, 'weapon_effects') else {}
}
if slot not in equipment:
equipment[slot] = None
return {
"success": True,
"messages": messages,
@@ -526,6 +685,8 @@ async def combat_action(
"xp": updated_player['xp'],
"level": updated_player['level']
},
"player_effects": player_effects,
"equipment": equipment,
"quest_updates": quest_updates
}
@@ -887,6 +1048,7 @@ async def pvp_combat_action(
items_manager=ITEMS_MANAGER,
reduce_armor_func=reduce_armor_durability,
redis_manager=redis_manager,
locale=locale
)
if result.get('error'):
@@ -978,6 +1140,25 @@ async def pvp_combat_action(
'last_action': f"{last_action_text}|{time.time()}"
})
# ── DEFEND ──
elif req.action == 'defend':
result = await combat_engine.execute_defend(
player_id=current_player['id'],
player=current_player,
player_stats=current_player_stats,
is_pvp=True,
locale=locale,
)
messages.extend(result['messages'])
last_action_text = f"{current_player['name']} took a defensive stance!"
# Switch turns
await db.update_pvp_combat(pvp_combat['id'], {
'turn': 'defender' if is_attacker else 'attacker',
'turn_started_at': time.time(),
'last_action': f"{last_action_text}|{time.time()}"
})
# ── FLEE ──
elif req.action == 'flee':
result = await combat_engine.execute_flee_pvp(