combat testing and polishing in the dev console, many bug fixes
This commit is contained in:
561
public_web/app/views/combat_views.py
Normal file
561
public_web/app/views/combat_views.py
Normal file
@@ -0,0 +1,561 @@
|
||||
"""
|
||||
Combat Views
|
||||
|
||||
Routes for combat UI.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, make_response
|
||||
import structlog
|
||||
|
||||
from ..utils.api_client import get_api_client, APIError, APINotFoundError
|
||||
from ..utils.auth import require_auth_web as require_auth
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
combat_bp = Blueprint('combat', __name__, url_prefix='/combat')
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>')
|
||||
@require_auth
|
||||
def combat_view(session_id: str):
|
||||
"""
|
||||
Render the combat page for an active encounter.
|
||||
|
||||
Displays the 3-column combat interface with:
|
||||
- Left: Combatants (player + enemies) with HP/MP bars
|
||||
- Center: Combat log + action buttons
|
||||
- Right: Turn order + active effects
|
||||
"""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get combat state from API
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat is still active
|
||||
if not result.get('in_combat'):
|
||||
# Combat ended - redirect to game play
|
||||
return redirect(url_for('game.play', session_id=session_id))
|
||||
|
||||
encounter = result.get('encounter') or {}
|
||||
combat_log = result.get('combat_log', [])
|
||||
|
||||
# Get current turn combatant ID directly from API response
|
||||
current_turn_id = encounter.get('current_turn')
|
||||
|
||||
# Find if it's the player's turn
|
||||
is_player_turn = False
|
||||
player_combatant = None
|
||||
for combatant in encounter.get('combatants', []):
|
||||
if combatant.get('is_player'):
|
||||
player_combatant = combatant
|
||||
if combatant.get('combatant_id') == current_turn_id:
|
||||
is_player_turn = True
|
||||
break
|
||||
|
||||
# Format combat log entries for display
|
||||
formatted_log = []
|
||||
for entry in combat_log:
|
||||
log_entry = {
|
||||
'actor': entry.get('combatant_name', entry.get('actor', '')),
|
||||
'message': entry.get('message', ''),
|
||||
'damage': entry.get('damage'),
|
||||
'heal': entry.get('healing'),
|
||||
'is_crit': entry.get('is_critical', False),
|
||||
'type': 'player' if entry.get('is_player', False) else 'enemy'
|
||||
}
|
||||
# Detect system messages
|
||||
if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'):
|
||||
log_entry['type'] = 'system'
|
||||
formatted_log.append(log_entry)
|
||||
|
||||
return render_template(
|
||||
'game/combat.html',
|
||||
session_id=session_id,
|
||||
encounter=encounter,
|
||||
combat_log=formatted_log,
|
||||
current_turn_id=current_turn_id,
|
||||
is_player_turn=is_player_turn,
|
||||
player_combatant=player_combatant
|
||||
)
|
||||
|
||||
except APINotFoundError:
|
||||
logger.warning("combat_not_found", session_id=session_id)
|
||||
return render_template('errors/404.html', message="No active combat encounter"), 404
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat", session_id=session_id, error=str(e))
|
||||
return render_template('errors/500.html', message=str(e)), 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/action', methods=['POST'])
|
||||
@require_auth
|
||||
def combat_action(session_id: str):
|
||||
"""
|
||||
Execute a combat action (attack, defend, ability, item).
|
||||
|
||||
Returns updated combat log entries.
|
||||
"""
|
||||
client = get_api_client()
|
||||
|
||||
action_type = request.form.get('action_type', 'attack')
|
||||
ability_id = request.form.get('ability_id')
|
||||
item_id = request.form.get('item_id')
|
||||
target_id = request.form.get('target_id')
|
||||
|
||||
try:
|
||||
# Build action payload
|
||||
payload = {
|
||||
'action_type': action_type
|
||||
}
|
||||
|
||||
if ability_id:
|
||||
payload['ability_id'] = ability_id
|
||||
if item_id:
|
||||
payload['item_id'] = item_id
|
||||
if target_id:
|
||||
payload['target_id'] = target_id
|
||||
|
||||
# POST action to API
|
||||
response = client.post(f'/api/v1/combat/{session_id}/action', payload)
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat ended
|
||||
combat_ended = result.get('combat_ended', False)
|
||||
combat_status = result.get('combat_status')
|
||||
|
||||
if combat_ended:
|
||||
# API returns lowercase status values: 'victory', 'defeat', 'fled'
|
||||
status_lower = (combat_status or '').lower()
|
||||
if status_lower == 'victory':
|
||||
return render_template(
|
||||
'game/partials/combat_victory.html',
|
||||
session_id=session_id,
|
||||
rewards=result.get('rewards', {})
|
||||
)
|
||||
elif status_lower == 'defeat':
|
||||
return render_template(
|
||||
'game/partials/combat_defeat.html',
|
||||
session_id=session_id,
|
||||
gold_lost=result.get('gold_lost', 0),
|
||||
can_retry=result.get('can_retry', False)
|
||||
)
|
||||
|
||||
# Format action result for log display
|
||||
# API returns data directly in result, not nested under 'action_result'
|
||||
log_entries = []
|
||||
|
||||
# Player action entry
|
||||
player_entry = {
|
||||
'actor': 'You',
|
||||
'message': result.get('message', f'used {action_type}'),
|
||||
'type': 'player',
|
||||
'is_crit': False
|
||||
}
|
||||
|
||||
# Add damage info if present
|
||||
damage_results = result.get('damage_results', [])
|
||||
if damage_results:
|
||||
for dmg in damage_results:
|
||||
player_entry['damage'] = dmg.get('total_damage') or dmg.get('damage')
|
||||
player_entry['is_crit'] = dmg.get('is_critical', False)
|
||||
if player_entry['is_crit']:
|
||||
player_entry['type'] = 'crit'
|
||||
|
||||
# Add healing info if present
|
||||
if result.get('healing'):
|
||||
player_entry['heal'] = result.get('healing')
|
||||
player_entry['type'] = 'heal'
|
||||
|
||||
log_entries.append(player_entry)
|
||||
|
||||
# Add any effect entries
|
||||
for effect in result.get('effects_applied', []):
|
||||
log_entries.append({
|
||||
'actor': '',
|
||||
'message': effect.get('message', f'Effect applied: {effect.get("name")}'),
|
||||
'type': 'system'
|
||||
})
|
||||
|
||||
# Return log entries HTML
|
||||
resp = make_response(render_template(
|
||||
'game/partials/combat_log.html',
|
||||
combat_log=log_entries
|
||||
))
|
||||
|
||||
# Trigger enemy turn if it's no longer player's turn
|
||||
next_combatant = result.get('next_combatant_id')
|
||||
if next_combatant and not result.get('next_is_player', True):
|
||||
resp.headers['HX-Trigger'] = 'enemyTurn'
|
||||
|
||||
return resp
|
||||
|
||||
except APIError as e:
|
||||
logger.error("combat_action_failed", session_id=session_id, action_type=action_type, error=str(e))
|
||||
return f'''
|
||||
<div class="combat-log__entry combat-log__entry--system">
|
||||
<span class="log-message">Action failed: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/abilities')
|
||||
@require_auth
|
||||
def combat_abilities(session_id: str):
|
||||
"""Get abilities modal for combat."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get combat state to get player's abilities
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
encounter = result.get('encounter', {})
|
||||
|
||||
# Find player combatant
|
||||
player_combatant = None
|
||||
for combatant in encounter.get('combatants', []):
|
||||
if combatant.get('is_player'):
|
||||
player_combatant = combatant
|
||||
break
|
||||
|
||||
# Get abilities from player combatant or character
|
||||
abilities = []
|
||||
if player_combatant:
|
||||
ability_ids = player_combatant.get('abilities', [])
|
||||
current_mp = player_combatant.get('current_mp', 0)
|
||||
cooldowns = player_combatant.get('cooldowns', {})
|
||||
|
||||
# Fetch ability details (if API has ability endpoint)
|
||||
for ability_id in ability_ids:
|
||||
try:
|
||||
ability_response = client.get(f'/api/v1/abilities/{ability_id}')
|
||||
ability_data = ability_response.get('result', {})
|
||||
|
||||
# Check availability
|
||||
mp_cost = ability_data.get('mp_cost', 0)
|
||||
cooldown = cooldowns.get(ability_id, 0)
|
||||
available = current_mp >= mp_cost and cooldown == 0
|
||||
|
||||
abilities.append({
|
||||
'id': ability_id,
|
||||
'name': ability_data.get('name', ability_id),
|
||||
'description': ability_data.get('description', ''),
|
||||
'mp_cost': mp_cost,
|
||||
'cooldown': cooldown,
|
||||
'max_cooldown': ability_data.get('cooldown', 0),
|
||||
'damage_type': ability_data.get('damage_type'),
|
||||
'effect_type': ability_data.get('effect_type'),
|
||||
'available': available
|
||||
})
|
||||
except (APINotFoundError, APIError):
|
||||
# Ability not found, add basic entry
|
||||
abilities.append({
|
||||
'id': ability_id,
|
||||
'name': ability_id.replace('_', ' ').title(),
|
||||
'description': '',
|
||||
'mp_cost': 0,
|
||||
'cooldown': cooldowns.get(ability_id, 0),
|
||||
'max_cooldown': 0,
|
||||
'available': True
|
||||
})
|
||||
|
||||
return render_template(
|
||||
'game/partials/ability_modal.html',
|
||||
session_id=session_id,
|
||||
abilities=abilities
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_abilities", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="modal-overlay" onclick="closeModal()">
|
||||
<div class="modal-content modal-content--md">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Select Ability</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="items-empty">Failed to load abilities: {e}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/items')
|
||||
@require_auth
|
||||
def combat_items(session_id: str):
|
||||
"""
|
||||
Get combat items bottom sheet (consumables only).
|
||||
|
||||
Returns a bottom sheet UI with only consumable items that can be used in combat.
|
||||
"""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
# Get character inventory - filter to consumables only
|
||||
consumables = []
|
||||
if character_id:
|
||||
try:
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
|
||||
# Filter to consumable items only
|
||||
for item in inventory:
|
||||
item_type = item.get('item_type', item.get('type', ''))
|
||||
if item_type == 'consumable' or item.get('usable_in_combat', False):
|
||||
consumables.append({
|
||||
'item_id': item.get('item_id'),
|
||||
'name': item.get('name', 'Unknown Item'),
|
||||
'description': item.get('description', ''),
|
||||
'effects_on_use': item.get('effects_on_use', []),
|
||||
'rarity': item.get('rarity', 'common')
|
||||
})
|
||||
except (APINotFoundError, APIError) as e:
|
||||
logger.warning("failed_to_load_combat_inventory", character_id=character_id, error=str(e))
|
||||
|
||||
return render_template(
|
||||
'game/partials/combat_items_sheet.html',
|
||||
session_id=session_id,
|
||||
consumables=consumables,
|
||||
has_consumables=len(consumables) > 0
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_items", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="combat-items-sheet open">
|
||||
<div class="sheet-handle"></div>
|
||||
<div class="sheet-header">
|
||||
<h3>Use Item</h3>
|
||||
<button class="sheet-close" onclick="closeCombatSheet()">×</button>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="no-consumables">Failed to load items: {e}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sheet-backdrop" onclick="closeCombatSheet()"></div>
|
||||
'''
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/items/<item_id>/detail')
|
||||
@require_auth
|
||||
def combat_item_detail(session_id: str, item_id: str):
|
||||
"""Get item detail for combat bottom sheet."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
item = None
|
||||
if character_id:
|
||||
# Get inventory and find the item
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
|
||||
for inv_item in inventory:
|
||||
if inv_item.get('item_id') == item_id:
|
||||
item = inv_item
|
||||
break
|
||||
|
||||
if not item:
|
||||
return '<p>Item not found</p>', 404
|
||||
|
||||
# Format effect description
|
||||
effect_desc = item.get('description', 'Use this item')
|
||||
effects = item.get('effects_on_use', [])
|
||||
if effects:
|
||||
effect_parts = []
|
||||
for effect in effects:
|
||||
if effect.get('stat') == 'hp':
|
||||
effect_parts.append(f"Restores {effect.get('value', 0)} HP")
|
||||
elif effect.get('stat') == 'mp':
|
||||
effect_parts.append(f"Restores {effect.get('value', 0)} MP")
|
||||
elif effect.get('name'):
|
||||
effect_parts.append(effect.get('name'))
|
||||
if effect_parts:
|
||||
effect_desc = ', '.join(effect_parts)
|
||||
|
||||
return f'''
|
||||
<div class="detail-info">
|
||||
<div class="detail-name">{item.get('name', 'Item')}</div>
|
||||
<div class="detail-effect">{effect_desc}</div>
|
||||
</div>
|
||||
<button class="use-btn"
|
||||
hx-post="{url_for('combat.combat_action', session_id=session_id)}"
|
||||
hx-vals='{{"action_type": "item", "item_id": "{item_id}"}}'
|
||||
hx-target="#combat-log"
|
||||
hx-swap="beforeend"
|
||||
onclick="closeCombatSheet()">
|
||||
Use
|
||||
</button>
|
||||
'''
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_item_detail", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<p>Failed to load item: {e}</p>', 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/flee', methods=['POST'])
|
||||
@require_auth
|
||||
def combat_flee(session_id: str):
|
||||
"""Attempt to flee from combat."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.post(f'/api/v1/combat/{session_id}/flee', {})
|
||||
result = response.get('result', {})
|
||||
|
||||
if result.get('success'):
|
||||
# Flee successful - redirect to play page
|
||||
return redirect(url_for('game.play_session', session_id=session_id))
|
||||
else:
|
||||
# Flee failed - return log entry
|
||||
return f'''
|
||||
<div class="combat-log__entry combat-log__entry--system">
|
||||
<span class="log-message">{result.get('message', 'Failed to flee!')}</span>
|
||||
</div>
|
||||
'''
|
||||
|
||||
except APIError as e:
|
||||
logger.error("flee_failed", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="combat-log__entry combat-log__entry--system">
|
||||
<span class="log-message">Flee failed: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/enemy-turn', methods=['POST'])
|
||||
@require_auth
|
||||
def combat_enemy_turn(session_id: str):
|
||||
"""Execute enemy turn and return result."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.post(f'/api/v1/combat/{session_id}/enemy-turn', {})
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat ended
|
||||
combat_ended = result.get('combat_ended', False)
|
||||
combat_status = result.get('combat_status')
|
||||
|
||||
if combat_ended:
|
||||
# API returns lowercase status values: 'victory', 'defeat', 'fled'
|
||||
status_lower = (combat_status or '').lower()
|
||||
if status_lower == 'victory':
|
||||
return render_template(
|
||||
'game/partials/combat_victory.html',
|
||||
session_id=session_id,
|
||||
rewards=result.get('rewards', {})
|
||||
)
|
||||
elif status_lower == 'defeat':
|
||||
return render_template(
|
||||
'game/partials/combat_defeat.html',
|
||||
session_id=session_id,
|
||||
gold_lost=result.get('gold_lost', 0),
|
||||
can_retry=result.get('can_retry', False)
|
||||
)
|
||||
|
||||
# Format enemy action for log
|
||||
action_result = result.get('action_result', {})
|
||||
log_entries = [{
|
||||
'actor': action_result.get('actor_name', 'Enemy'),
|
||||
'message': action_result.get('message', 'attacks'),
|
||||
'type': 'enemy',
|
||||
'is_crit': action_result.get('is_critical', False)
|
||||
}]
|
||||
|
||||
# Add damage info
|
||||
damage_results = action_result.get('damage_results', [])
|
||||
if damage_results:
|
||||
log_entries[0]['damage'] = damage_results[0].get('damage')
|
||||
|
||||
# Check if it's still enemy turn (multiple enemies)
|
||||
resp = make_response(render_template(
|
||||
'game/partials/combat_log.html',
|
||||
combat_log=log_entries
|
||||
))
|
||||
|
||||
# If next combatant is also an enemy, trigger another enemy turn
|
||||
if result.get('next_combatant_id') and not result.get('next_is_player', True):
|
||||
resp.headers['HX-Trigger'] = 'enemyTurn'
|
||||
|
||||
return resp
|
||||
|
||||
except APIError as e:
|
||||
logger.error("enemy_turn_failed", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="combat-log__entry combat-log__entry--system">
|
||||
<span class="log-message">Enemy turn error: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/log')
|
||||
@require_auth
|
||||
def combat_log(session_id: str):
|
||||
"""Get current combat log."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
combat_log_data = result.get('combat_log', [])
|
||||
|
||||
# Format log entries
|
||||
formatted_log = []
|
||||
for entry in combat_log_data:
|
||||
log_entry = {
|
||||
'actor': entry.get('combatant_name', entry.get('actor', '')),
|
||||
'message': entry.get('message', ''),
|
||||
'damage': entry.get('damage'),
|
||||
'heal': entry.get('healing'),
|
||||
'is_crit': entry.get('is_critical', False),
|
||||
'type': 'player' if entry.get('is_player', False) else 'enemy'
|
||||
}
|
||||
if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'):
|
||||
log_entry['type'] = 'system'
|
||||
formatted_log.append(log_entry)
|
||||
|
||||
return render_template(
|
||||
'game/partials/combat_log.html',
|
||||
combat_log=formatted_log
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_log", session_id=session_id, error=str(e))
|
||||
return '<div class="combat-log__empty">Failed to load combat log</div>', 500
|
||||
|
||||
|
||||
@combat_bp.route('/<session_id>/results')
|
||||
@require_auth
|
||||
def combat_results(session_id: str):
|
||||
"""Display combat results (victory/defeat)."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.get(f'/api/v1/combat/{session_id}/results')
|
||||
results = response.get('result', {})
|
||||
|
||||
return render_template(
|
||||
'game/combat_results.html',
|
||||
victory=results['victory'],
|
||||
xp_gained=results['xp_gained'],
|
||||
gold_gained=results['gold_gained'],
|
||||
loot=results['loot']
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_results", session_id=session_id, error=str(e))
|
||||
return redirect(url_for('game.play_session', session_id=session_id))
|
||||
@@ -380,3 +380,652 @@ def do_travel(session_id: str):
|
||||
except APIError as e:
|
||||
logger.error("failed_to_travel", session_id=session_id, location_id=location_id, error=str(e))
|
||||
return f'<div class="error">Travel failed: {e}</div>', 500
|
||||
|
||||
|
||||
# ===== Combat Test Endpoints =====
|
||||
|
||||
@dev_bp.route('/combat')
|
||||
@require_auth
|
||||
def combat_hub():
|
||||
"""Combat testing hub - select character and enemies to start combat."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get user's characters
|
||||
characters_response = client.get('/api/v1/characters')
|
||||
result = characters_response.get('result', {})
|
||||
characters = result.get('characters', [])
|
||||
|
||||
# Get available enemy templates
|
||||
enemies = []
|
||||
try:
|
||||
enemies_response = client.get('/api/v1/combat/enemies')
|
||||
enemies = enemies_response.get('result', {}).get('enemies', [])
|
||||
except (APINotFoundError, APIError):
|
||||
# Enemies endpoint may not exist yet
|
||||
pass
|
||||
|
||||
# Get all sessions to map characters to their sessions
|
||||
sessions_in_combat = []
|
||||
character_session_map = {} # character_id -> session_id
|
||||
try:
|
||||
sessions_response = client.get('/api/v1/sessions')
|
||||
all_sessions = sessions_response.get('result', [])
|
||||
for session in all_sessions:
|
||||
# Map character to session (for dropdown)
|
||||
char_id = session.get('character_id')
|
||||
if char_id:
|
||||
character_session_map[char_id] = session.get('session_id')
|
||||
|
||||
# Track sessions in combat (for resume list)
|
||||
if session.get('in_combat') or session.get('game_state', {}).get('in_combat'):
|
||||
sessions_in_combat.append(session)
|
||||
except (APINotFoundError, APIError):
|
||||
pass
|
||||
|
||||
# Add session_id to each character for the template
|
||||
for char in characters:
|
||||
char['session_id'] = character_session_map.get(char.get('character_id'))
|
||||
|
||||
return render_template(
|
||||
'dev/combat.html',
|
||||
characters=characters,
|
||||
enemies=enemies,
|
||||
sessions_in_combat=sessions_in_combat
|
||||
)
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_hub", error=str(e))
|
||||
return render_template('dev/combat.html', characters=[], enemies=[], sessions_in_combat=[], error=str(e))
|
||||
|
||||
|
||||
@dev_bp.route('/combat/start', methods=['POST'])
|
||||
@require_auth
|
||||
def start_combat():
|
||||
"""Start a new combat encounter - returns redirect to combat session."""
|
||||
client = get_api_client()
|
||||
|
||||
session_id = request.form.get('session_id')
|
||||
enemy_ids = request.form.getlist('enemy_ids')
|
||||
|
||||
logger.info("start_combat called",
|
||||
session_id=session_id,
|
||||
enemy_ids=enemy_ids,
|
||||
form_data=dict(request.form))
|
||||
|
||||
if not session_id:
|
||||
return '<div class="error">No session selected</div>', 400
|
||||
|
||||
if not enemy_ids:
|
||||
return '<div class="error">No enemies selected</div>', 400
|
||||
|
||||
try:
|
||||
response = client.post('/api/v1/combat/start', {
|
||||
'session_id': session_id,
|
||||
'enemy_ids': enemy_ids
|
||||
})
|
||||
result = response.get('result', {})
|
||||
|
||||
# Return redirect script to combat session page
|
||||
return f'''
|
||||
<script>window.location.href = '/dev/combat/session/{session_id}';</script>
|
||||
<div class="success">Combat started! Redirecting...</div>
|
||||
'''
|
||||
except APIError as e:
|
||||
logger.error("failed_to_start_combat", session_id=session_id, error=str(e))
|
||||
return f'<div class="error">Failed to start combat: {e}</div>', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/session/<session_id>')
|
||||
@require_auth
|
||||
def combat_session(session_id: str):
|
||||
"""Combat session debug interface - full 3-column layout."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get combat state from API
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat is still active
|
||||
if not result.get('in_combat'):
|
||||
# Combat ended - redirect to combat index
|
||||
return render_template('dev/combat.html',
|
||||
message="Combat has ended. Start a new combat to continue.")
|
||||
|
||||
encounter = result.get('encounter') or {}
|
||||
combat_log = result.get('combat_log', [])
|
||||
|
||||
# Get current turn combatant ID directly from API response
|
||||
current_turn_id = encounter.get('current_turn')
|
||||
turn_order = encounter.get('turn_order', [])
|
||||
|
||||
# Find player and determine if it's player's turn
|
||||
is_player_turn = False
|
||||
player_combatant = None
|
||||
enemy_combatants = []
|
||||
for combatant in encounter.get('combatants', []):
|
||||
if combatant.get('is_player'):
|
||||
player_combatant = combatant
|
||||
if combatant.get('combatant_id') == current_turn_id:
|
||||
is_player_turn = True
|
||||
else:
|
||||
enemy_combatants.append(combatant)
|
||||
|
||||
# Format combat log entries for display
|
||||
formatted_log = []
|
||||
for entry in combat_log:
|
||||
log_entry = {
|
||||
'actor': entry.get('combatant_name', entry.get('actor', '')),
|
||||
'message': entry.get('message', ''),
|
||||
'damage': entry.get('damage'),
|
||||
'heal': entry.get('healing'),
|
||||
'is_crit': entry.get('is_critical', False),
|
||||
'type': 'player' if entry.get('is_player', False) else 'enemy'
|
||||
}
|
||||
# Detect system messages
|
||||
if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'):
|
||||
log_entry['type'] = 'system'
|
||||
formatted_log.append(log_entry)
|
||||
|
||||
return render_template(
|
||||
'dev/combat_session.html',
|
||||
session_id=session_id,
|
||||
encounter=encounter,
|
||||
combat_log=formatted_log,
|
||||
current_turn_id=current_turn_id,
|
||||
is_player_turn=is_player_turn,
|
||||
player_combatant=player_combatant,
|
||||
enemy_combatants=enemy_combatants,
|
||||
turn_order=turn_order,
|
||||
raw_state=result
|
||||
)
|
||||
|
||||
except APINotFoundError:
|
||||
logger.warning("combat_not_found", session_id=session_id)
|
||||
return render_template('dev/combat.html', error=f"No active combat for session {session_id}"), 404
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_session", session_id=session_id, error=str(e))
|
||||
return render_template('dev/combat.html', error=str(e)), 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/state')
|
||||
@require_auth
|
||||
def combat_state(session_id: str):
|
||||
"""Get combat state partial - returns refreshable state panel."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat is still active
|
||||
if not result.get('in_combat'):
|
||||
return '<div class="state-section"><h4>Combat Ended</h4><p>No active combat.</p></div>'
|
||||
|
||||
encounter = result.get('encounter') or {}
|
||||
|
||||
# Get current turn combatant ID directly from API response
|
||||
current_turn_id = encounter.get('current_turn')
|
||||
|
||||
# Separate player and enemies
|
||||
player_combatant = None
|
||||
enemy_combatants = []
|
||||
is_player_turn = False
|
||||
for combatant in encounter.get('combatants', []):
|
||||
if combatant.get('is_player'):
|
||||
player_combatant = combatant
|
||||
if combatant.get('combatant_id') == current_turn_id:
|
||||
is_player_turn = True
|
||||
else:
|
||||
enemy_combatants.append(combatant)
|
||||
|
||||
return render_template(
|
||||
'dev/partials/combat_state.html',
|
||||
session_id=session_id,
|
||||
encounter=encounter,
|
||||
player_combatant=player_combatant,
|
||||
enemy_combatants=enemy_combatants,
|
||||
current_turn_id=current_turn_id,
|
||||
is_player_turn=is_player_turn
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_get_combat_state", session_id=session_id, error=str(e))
|
||||
return f'<div class="error">Failed to load state: {e}</div>', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/action', methods=['POST'])
|
||||
@require_auth
|
||||
def combat_action(session_id: str):
|
||||
"""Execute a combat action - returns log entry HTML."""
|
||||
client = get_api_client()
|
||||
|
||||
action_type = request.form.get('action_type', 'attack')
|
||||
ability_id = request.form.get('ability_id')
|
||||
item_id = request.form.get('item_id')
|
||||
target_id = request.form.get('target_id')
|
||||
|
||||
try:
|
||||
payload = {'action_type': action_type}
|
||||
if ability_id:
|
||||
payload['ability_id'] = ability_id
|
||||
if item_id:
|
||||
payload['item_id'] = item_id
|
||||
if target_id:
|
||||
payload['target_id'] = target_id
|
||||
|
||||
response = client.post(f'/api/v1/combat/{session_id}/action', payload)
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat ended
|
||||
combat_ended = result.get('combat_ended', False)
|
||||
combat_status = result.get('combat_status')
|
||||
|
||||
if combat_ended:
|
||||
# API returns lowercase status values: 'victory', 'defeat', 'fled'
|
||||
status_lower = (combat_status or '').lower()
|
||||
if status_lower == 'victory':
|
||||
return render_template(
|
||||
'dev/partials/combat_victory.html',
|
||||
session_id=session_id,
|
||||
rewards=result.get('rewards', {})
|
||||
)
|
||||
elif status_lower == 'defeat':
|
||||
return render_template(
|
||||
'dev/partials/combat_defeat.html',
|
||||
session_id=session_id,
|
||||
gold_lost=result.get('gold_lost', 0)
|
||||
)
|
||||
|
||||
# Format action result for log
|
||||
# API returns data directly in result, not nested under 'action_result'
|
||||
log_entries = []
|
||||
|
||||
player_entry = {
|
||||
'actor': 'You',
|
||||
'message': result.get('message', f'used {action_type}'),
|
||||
'type': 'player',
|
||||
'is_crit': False
|
||||
}
|
||||
|
||||
damage_results = result.get('damage_results', [])
|
||||
if damage_results:
|
||||
for dmg in damage_results:
|
||||
player_entry['damage'] = dmg.get('total_damage') or dmg.get('damage')
|
||||
player_entry['is_crit'] = dmg.get('is_critical', False)
|
||||
if player_entry['is_crit']:
|
||||
player_entry['type'] = 'crit'
|
||||
|
||||
if result.get('healing'):
|
||||
player_entry['heal'] = result.get('healing')
|
||||
player_entry['type'] = 'heal'
|
||||
|
||||
log_entries.append(player_entry)
|
||||
|
||||
for effect in result.get('effects_applied', []):
|
||||
log_entries.append({
|
||||
'actor': '',
|
||||
'message': effect.get('message', f'Effect applied: {effect.get("name")}'),
|
||||
'type': 'system'
|
||||
})
|
||||
|
||||
# Return log entries with optional enemy turn trigger
|
||||
from flask import make_response
|
||||
resp = make_response(render_template(
|
||||
'dev/partials/combat_debug_log.html',
|
||||
combat_log=log_entries
|
||||
))
|
||||
|
||||
# Trigger enemy turn if needed
|
||||
if result.get('next_combatant_id') and not result.get('next_is_player', True):
|
||||
resp.headers['HX-Trigger'] = 'enemyTurn'
|
||||
|
||||
return resp
|
||||
|
||||
except APIError as e:
|
||||
logger.error("combat_action_failed", session_id=session_id, action_type=action_type, error=str(e))
|
||||
return f'''
|
||||
<div class="log-entry log-entry--system">
|
||||
<span class="log-message">Action failed: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/enemy-turn', methods=['POST'])
|
||||
@require_auth
|
||||
def combat_enemy_turn(session_id: str):
|
||||
"""Execute enemy turn - returns log entry HTML."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.post(f'/api/v1/combat/{session_id}/enemy-turn', {})
|
||||
result = response.get('result', {})
|
||||
|
||||
# Check if combat ended
|
||||
combat_ended = result.get('combat_ended', False)
|
||||
combat_status = result.get('combat_status')
|
||||
|
||||
if combat_ended:
|
||||
# API returns lowercase status values: 'victory', 'defeat', 'fled'
|
||||
status_lower = (combat_status or '').lower()
|
||||
if status_lower == 'victory':
|
||||
return render_template(
|
||||
'dev/partials/combat_victory.html',
|
||||
session_id=session_id,
|
||||
rewards=result.get('rewards', {})
|
||||
)
|
||||
elif status_lower == 'defeat':
|
||||
return render_template(
|
||||
'dev/partials/combat_defeat.html',
|
||||
session_id=session_id,
|
||||
gold_lost=result.get('gold_lost', 0)
|
||||
)
|
||||
|
||||
# Format enemy action for log
|
||||
# The API returns the action result directly with a complete message
|
||||
damage_results = result.get('damage_results', [])
|
||||
is_crit = damage_results[0].get('is_critical', False) if damage_results else False
|
||||
|
||||
log_entries = [{
|
||||
'actor': '', # Message already contains the actor name
|
||||
'message': result.get('message', 'Enemy attacks!'),
|
||||
'type': 'crit' if is_crit else 'enemy',
|
||||
'is_crit': is_crit,
|
||||
'damage': damage_results[0].get('total_damage') if damage_results else None
|
||||
}]
|
||||
|
||||
from flask import make_response
|
||||
resp = make_response(render_template(
|
||||
'dev/partials/combat_debug_log.html',
|
||||
combat_log=log_entries
|
||||
))
|
||||
|
||||
# Trigger another enemy turn if needed
|
||||
if result.get('next_combatant_id') and not result.get('next_is_player', True):
|
||||
resp.headers['HX-Trigger'] = 'enemyTurn'
|
||||
|
||||
return resp
|
||||
|
||||
except APIError as e:
|
||||
logger.error("enemy_turn_failed", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="log-entry log-entry--system">
|
||||
<span class="log-message">Enemy turn error: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/abilities')
|
||||
@require_auth
|
||||
def combat_abilities(session_id: str):
|
||||
"""Get abilities modal for combat."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
encounter = result.get('encounter', {})
|
||||
|
||||
player_combatant = None
|
||||
for combatant in encounter.get('combatants', []):
|
||||
if combatant.get('is_player'):
|
||||
player_combatant = combatant
|
||||
break
|
||||
|
||||
abilities = []
|
||||
if player_combatant:
|
||||
ability_ids = player_combatant.get('abilities', [])
|
||||
current_mp = player_combatant.get('current_mp', 0)
|
||||
cooldowns = player_combatant.get('cooldowns', {})
|
||||
|
||||
for ability_id in ability_ids:
|
||||
try:
|
||||
ability_response = client.get(f'/api/v1/abilities/{ability_id}')
|
||||
ability_data = ability_response.get('result', {})
|
||||
|
||||
mp_cost = ability_data.get('mp_cost', 0)
|
||||
cooldown = cooldowns.get(ability_id, 0)
|
||||
available = current_mp >= mp_cost and cooldown == 0
|
||||
|
||||
abilities.append({
|
||||
'id': ability_id,
|
||||
'name': ability_data.get('name', ability_id),
|
||||
'description': ability_data.get('description', ''),
|
||||
'mp_cost': mp_cost,
|
||||
'cooldown': cooldown,
|
||||
'max_cooldown': ability_data.get('cooldown', 0),
|
||||
'damage_type': ability_data.get('damage_type'),
|
||||
'effect_type': ability_data.get('effect_type'),
|
||||
'available': available
|
||||
})
|
||||
except (APINotFoundError, APIError):
|
||||
abilities.append({
|
||||
'id': ability_id,
|
||||
'name': ability_id.replace('_', ' ').title(),
|
||||
'description': '',
|
||||
'mp_cost': 0,
|
||||
'cooldown': cooldowns.get(ability_id, 0),
|
||||
'max_cooldown': 0,
|
||||
'available': True
|
||||
})
|
||||
|
||||
return render_template(
|
||||
'dev/partials/ability_modal.html',
|
||||
session_id=session_id,
|
||||
abilities=abilities
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_abilities", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="modal-overlay" onclick="closeModal()">
|
||||
<div class="modal-content">
|
||||
<h3>Select Ability</h3>
|
||||
<div class="error">Failed to load abilities: {e}</div>
|
||||
<button class="modal-close" onclick="closeModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/items')
|
||||
@require_auth
|
||||
def combat_items(session_id: str):
|
||||
"""Get combat items bottom sheet (consumables only)."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
consumables = []
|
||||
if character_id:
|
||||
try:
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
|
||||
for item in inventory:
|
||||
item_type = item.get('item_type', item.get('type', ''))
|
||||
if item_type == 'consumable' or item.get('usable_in_combat', False):
|
||||
consumables.append({
|
||||
'item_id': item.get('item_id'),
|
||||
'name': item.get('name', 'Unknown Item'),
|
||||
'description': item.get('description', ''),
|
||||
'effects_on_use': item.get('effects_on_use', []),
|
||||
'rarity': item.get('rarity', 'common')
|
||||
})
|
||||
except (APINotFoundError, APIError) as e:
|
||||
logger.warning("failed_to_load_combat_inventory", character_id=character_id, error=str(e))
|
||||
|
||||
return render_template(
|
||||
'dev/partials/combat_items_sheet.html',
|
||||
session_id=session_id,
|
||||
consumables=consumables,
|
||||
has_consumables=len(consumables) > 0
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_items", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="combat-items-sheet open">
|
||||
<div class="sheet-header">
|
||||
<h3>Use Item</h3>
|
||||
<button class="sheet-close" onclick="closeCombatSheet()">×</button>
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="error">Failed to load items: {e}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sheet-backdrop" onclick="closeCombatSheet()"></div>
|
||||
'''
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/items/<item_id>/detail')
|
||||
@require_auth
|
||||
def combat_item_detail(session_id: str, item_id: str):
|
||||
"""Get item detail for combat bottom sheet."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
item = None
|
||||
if character_id:
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
|
||||
for inv_item in inventory:
|
||||
if inv_item.get('item_id') == item_id:
|
||||
item = inv_item
|
||||
break
|
||||
|
||||
if not item:
|
||||
return '<p>Item not found</p>', 404
|
||||
|
||||
effect_desc = item.get('description', 'Use this item')
|
||||
effects = item.get('effects_on_use', [])
|
||||
if effects:
|
||||
effect_parts = []
|
||||
for effect in effects:
|
||||
if effect.get('stat') == 'hp':
|
||||
effect_parts.append(f"Restores {effect.get('value', 0)} HP")
|
||||
elif effect.get('stat') == 'mp':
|
||||
effect_parts.append(f"Restores {effect.get('value', 0)} MP")
|
||||
elif effect.get('name'):
|
||||
effect_parts.append(effect.get('name'))
|
||||
if effect_parts:
|
||||
effect_desc = ', '.join(effect_parts)
|
||||
|
||||
return f'''
|
||||
<div class="detail-info">
|
||||
<div class="detail-name">{item.get('name', 'Item')}</div>
|
||||
<div class="detail-effect">{effect_desc}</div>
|
||||
</div>
|
||||
<button class="use-btn"
|
||||
hx-post="/dev/combat/{session_id}/action"
|
||||
hx-vals='{{"action_type": "item", "item_id": "{item_id}"}}'
|
||||
hx-target="#combat-log"
|
||||
hx-swap="beforeend"
|
||||
onclick="closeCombatSheet()">
|
||||
Use
|
||||
</button>
|
||||
'''
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_item_detail", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<p>Failed to load item: {e}</p>', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/end', methods=['POST'])
|
||||
@require_auth
|
||||
def force_end_combat(session_id: str):
|
||||
"""Force end combat (debug action)."""
|
||||
client = get_api_client()
|
||||
|
||||
victory = request.form.get('victory', 'true').lower() == 'true'
|
||||
|
||||
try:
|
||||
response = client.post(f'/api/v1/combat/{session_id}/end', {'victory': victory})
|
||||
result = response.get('result', {})
|
||||
|
||||
if victory:
|
||||
return render_template(
|
||||
'dev/partials/combat_victory.html',
|
||||
session_id=session_id,
|
||||
rewards=result.get('rewards', {})
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
'dev/partials/combat_defeat.html',
|
||||
session_id=session_id,
|
||||
gold_lost=result.get('gold_lost', 0)
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_end_combat", session_id=session_id, error=str(e))
|
||||
return f'<div class="error">Failed to end combat: {e}</div>', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/reset-hp-mp', methods=['POST'])
|
||||
@require_auth
|
||||
def reset_hp_mp(session_id: str):
|
||||
"""Reset player HP and MP to full (debug action)."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.post(f'/api/v1/combat/{session_id}/debug/reset-hp-mp', {})
|
||||
result = response.get('result', {})
|
||||
|
||||
return f'''
|
||||
<div class="log-entry log-entry--heal">
|
||||
<span class="log-message">HP and MP restored to full! (HP: {result.get('current_hp', '?')}/{result.get('max_hp', '?')}, MP: {result.get('current_mp', '?')}/{result.get('max_mp', '?')})</span>
|
||||
</div>
|
||||
'''
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_reset_hp_mp", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="log-entry log-entry--system">
|
||||
<span class="log-message">Failed to reset HP/MP: {e}</span>
|
||||
</div>
|
||||
''', 500
|
||||
|
||||
|
||||
@dev_bp.route('/combat/<session_id>/log')
|
||||
@require_auth
|
||||
def combat_log(session_id: str):
|
||||
"""Get full combat log."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
response = client.get(f'/api/v1/combat/{session_id}/state')
|
||||
result = response.get('result', {})
|
||||
combat_log_data = result.get('combat_log', [])
|
||||
|
||||
formatted_log = []
|
||||
for entry in combat_log_data:
|
||||
log_entry = {
|
||||
'actor': entry.get('combatant_name', entry.get('actor', '')),
|
||||
'message': entry.get('message', ''),
|
||||
'damage': entry.get('damage'),
|
||||
'heal': entry.get('healing'),
|
||||
'is_crit': entry.get('is_critical', False),
|
||||
'type': 'player' if entry.get('is_player', False) else 'enemy'
|
||||
}
|
||||
if entry.get('action_type') in ('round_start', 'combat_start', 'combat_end'):
|
||||
log_entry['type'] = 'system'
|
||||
formatted_log.append(log_entry)
|
||||
|
||||
return render_template(
|
||||
'dev/partials/combat_debug_log.html',
|
||||
combat_log=formatted_log
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_combat_log", session_id=session_id, error=str(e))
|
||||
return '<div class="error">Failed to load combat log</div>', 500
|
||||
|
||||
@@ -7,7 +7,7 @@ Provides the main gameplay interface with 3-column layout:
|
||||
- Right: Accordions for history, quests, NPCs, map
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
import structlog
|
||||
|
||||
from ..utils.api_client import get_api_client, APIError, APINotFoundError
|
||||
@@ -866,6 +866,220 @@ def npc_chat_history(session_id: str, npc_id: str):
|
||||
return '<div class="history-empty">Failed to load history</div>', 500
|
||||
|
||||
|
||||
# ===== Inventory Routes =====
|
||||
|
||||
@game_bp.route('/session/<session_id>/inventory-modal')
|
||||
@require_auth
|
||||
def inventory_modal(session_id: str):
|
||||
"""
|
||||
Get inventory modal with all items.
|
||||
|
||||
Supports filtering by item type via ?filter= parameter.
|
||||
"""
|
||||
client = get_api_client()
|
||||
filter_type = request.args.get('filter', 'all')
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
inventory = []
|
||||
equipped = {}
|
||||
gold = 0
|
||||
inventory_count = 0
|
||||
inventory_max = 100
|
||||
|
||||
if character_id:
|
||||
try:
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
equipped = inv_data.get('equipped', {})
|
||||
inventory_count = inv_data.get('inventory_count', len(inventory))
|
||||
inventory_max = inv_data.get('max_inventory', 100)
|
||||
|
||||
# Get gold from character
|
||||
char_response = client.get(f'/api/v1/characters/{character_id}')
|
||||
char_data = char_response.get('result', {})
|
||||
gold = char_data.get('gold', 0)
|
||||
except (APINotFoundError, APIError) as e:
|
||||
logger.warning("failed_to_load_inventory", character_id=character_id, error=str(e))
|
||||
|
||||
# Filter inventory by type if specified
|
||||
if filter_type != 'all':
|
||||
inventory = [item for item in inventory if item.get('item_type') == filter_type]
|
||||
|
||||
return render_template(
|
||||
'game/partials/inventory_modal.html',
|
||||
session_id=session_id,
|
||||
inventory=inventory,
|
||||
equipped=equipped,
|
||||
gold=gold,
|
||||
inventory_count=inventory_count,
|
||||
inventory_max=inventory_max,
|
||||
filter=filter_type
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_inventory_modal", session_id=session_id, error=str(e))
|
||||
return f'''
|
||||
<div class="modal-overlay" onclick="closeModal()">
|
||||
<div class="modal-content inventory-modal">
|
||||
<div class="modal-header">
|
||||
<h2>Inventory</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="inventory-empty">Failed to load inventory: {e}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
@game_bp.route('/session/<session_id>/inventory/item/<item_id>')
|
||||
@require_auth
|
||||
def inventory_item_detail(session_id: str, item_id: str):
|
||||
"""Get item detail partial for HTMX swap."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
item = None
|
||||
if character_id:
|
||||
# Get inventory and find the item
|
||||
inv_response = client.get(f'/api/v1/characters/{character_id}/inventory')
|
||||
inv_data = inv_response.get('result', {})
|
||||
inventory = inv_data.get('inventory', [])
|
||||
|
||||
for inv_item in inventory:
|
||||
if inv_item.get('item_id') == item_id:
|
||||
item = inv_item
|
||||
break
|
||||
|
||||
if not item:
|
||||
return '<div class="item-detail-empty">Item not found</div>', 404
|
||||
|
||||
# Determine suggested slot for equipment
|
||||
suggested_slot = None
|
||||
item_type = item.get('item_type', '')
|
||||
if item_type == 'weapon':
|
||||
suggested_slot = 'weapon'
|
||||
elif item_type == 'armor':
|
||||
# Could be any armor slot - default to chest
|
||||
suggested_slot = 'chest'
|
||||
|
||||
return render_template(
|
||||
'game/partials/inventory_item_detail.html',
|
||||
session_id=session_id,
|
||||
item=item,
|
||||
suggested_slot=suggested_slot
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_load_item_detail", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<div class="item-detail-empty">Failed to load item: {e}</div>', 500
|
||||
|
||||
|
||||
@game_bp.route('/session/<session_id>/inventory/use', methods=['POST'])
|
||||
@require_auth
|
||||
def inventory_use(session_id: str):
|
||||
"""Use a consumable item."""
|
||||
client = get_api_client()
|
||||
item_id = request.form.get('item_id')
|
||||
|
||||
if not item_id:
|
||||
return '<div class="error">No item selected</div>', 400
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
if not character_id:
|
||||
return '<div class="error">No character found</div>', 400
|
||||
|
||||
# Use the item via API
|
||||
client.post(f'/api/v1/characters/{character_id}/inventory/use', {
|
||||
'item_id': item_id
|
||||
})
|
||||
|
||||
# Return updated character panel
|
||||
return redirect(url_for('game.character_panel', session_id=session_id))
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_use_item", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<div class="error">Failed to use item: {e}</div>', 500
|
||||
|
||||
|
||||
@game_bp.route('/session/<session_id>/inventory/equip', methods=['POST'])
|
||||
@require_auth
|
||||
def inventory_equip(session_id: str):
|
||||
"""Equip an item to a slot."""
|
||||
client = get_api_client()
|
||||
item_id = request.form.get('item_id')
|
||||
slot = request.form.get('slot')
|
||||
|
||||
if not item_id:
|
||||
return '<div class="error">No item selected</div>', 400
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
if not character_id:
|
||||
return '<div class="error">No character found</div>', 400
|
||||
|
||||
# Equip the item via API
|
||||
payload = {'item_id': item_id}
|
||||
if slot:
|
||||
payload['slot'] = slot
|
||||
|
||||
client.post(f'/api/v1/characters/{character_id}/inventory/equip', payload)
|
||||
|
||||
# Return updated character panel
|
||||
return redirect(url_for('game.character_panel', session_id=session_id))
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_equip_item", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<div class="error">Failed to equip item: {e}</div>', 500
|
||||
|
||||
|
||||
@game_bp.route('/session/<session_id>/inventory/<item_id>', methods=['DELETE'])
|
||||
@require_auth
|
||||
def inventory_drop(session_id: str, item_id: str):
|
||||
"""Drop (delete) an item from inventory."""
|
||||
client = get_api_client()
|
||||
|
||||
try:
|
||||
# Get session to find character
|
||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||
session_data = session_response.get('result', {})
|
||||
character_id = session_data.get('character_id')
|
||||
|
||||
if not character_id:
|
||||
return '<div class="error">No character found</div>', 400
|
||||
|
||||
# Delete the item via API
|
||||
client.delete(f'/api/v1/characters/{character_id}/inventory/{item_id}')
|
||||
|
||||
# Return updated inventory modal
|
||||
return redirect(url_for('game.inventory_modal', session_id=session_id))
|
||||
|
||||
except APIError as e:
|
||||
logger.error("failed_to_drop_item", session_id=session_id, item_id=item_id, error=str(e))
|
||||
return f'<div class="error">Failed to drop item: {e}</div>', 500
|
||||
|
||||
|
||||
@game_bp.route('/session/<session_id>/npc/<npc_id>/talk', methods=['POST'])
|
||||
@require_auth
|
||||
def talk_to_npc(session_id: str, npc_id: str):
|
||||
|
||||
Reference in New Issue
Block a user