Root Cause

When using combat abilities (like "smite"), the web frontend was calling GET /api/v1/abilities/{ability_id} to fetch ability details for display, but this endpoint didn't exist, causing a 404 error.
  Additionally, after fixing that, the ability would execute but:

  1. Modal onclick issue: The onclick="closeModal()" on ability buttons was removing the button from the DOM before HTMX could fire the request
  2. Field name mismatch: The API returns mana_cost but the frontend expected mp_cost
  3. Duplicate text in combat log: The web view was adding "You" as actor and damage separately, but the API message already contained both
  4. Page not auto-refreshing: Various attempts to use HX-Trigger headers failed due to HTMX overwriting them

  Fixes Made

  1. Created /api/app/api/abilities.py - New abilities API endpoint with GET /api/v1/abilities and GET /api/v1/abilities/<ability_id>
  2. Modified /api/app/__init__.py - Registered the new abilities blueprint
  3. Modified /public_web/templates/game/partials/ability_modal.html - Changed onclick="closeModal()" to hx-on::before-request="closeModal()" so HTMX captures the request before modal closes
  4. Modified /public_web/app/views/combat_views.py:
    - Fixed mp_cost → mana_cost field name lookup
    - Removed duplicate actor/damage from combat log entries (API message is self-contained)
    - Added inline script to trigger page refresh after combat actions
  5. Modified /public_web/templates/game/combat.html - Updated JavaScript for combat action handling (though final fix was server-side script injection)
This commit is contained in:
2025-11-29 19:05:39 -06:00
parent 06ef8f6f0b
commit f9e463bfc6
5 changed files with 192 additions and 35 deletions

View File

@@ -145,27 +145,29 @@ def combat_action(session_id: str):
# 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
# The API message is self-contained (includes actor name and damage)
# Don't add separate actor/damage to avoid duplication
message = result.get('message', f'Used {action_type}')
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
# Determine entry type based on damage results
entry_type = 'player'
is_crit = False
if damage_results:
is_crit = any(dmg.get('is_critical', False) for dmg in damage_results)
if is_crit:
entry_type = 'crit'
if result.get('healing'):
player_entry['heal'] = result.get('healing')
player_entry['type'] = 'heal'
entry_type = 'heal'
player_entry = {
'actor': '', # API message already includes character name
'message': message,
'type': entry_type,
'is_crit': is_crit
# Don't add 'damage' - it's already in the message
}
log_entries.append(player_entry)
@@ -179,18 +181,32 @@ def combat_action(session_id: str):
'type': 'system'
})
# Return log entries HTML
resp = make_response(render_template(
# Check if it's now enemy's turn
next_combatant = result.get('next_combatant_id')
next_is_player = result.get('next_is_player', True)
logger.info("combat_action_result",
next_combatant=next_combatant,
next_is_player=next_is_player,
combat_ended=combat_ended)
# Render log entries
log_html = 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'
# Add script to trigger page refresh after showing the action result
# This is more reliable than headers which can be modified by HTMX
refresh_script = '''
<script>
setTimeout(function() {
window.location.reload();
}, 1200);
</script>
'''
return resp
return log_html + refresh_script
except APIError as e:
logger.error("combat_action_failed", session_id=session_id, action_type=action_type, error=str(e))
@@ -233,8 +249,8 @@ def combat_abilities(session_id: str):
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)
# Check availability (API returns 'mana_cost', template uses 'mp_cost')
mp_cost = ability_data.get('mana_cost', ability_data.get('mp_cost', 0))
cooldown = cooldowns.get(ability_id, 0)
available = current_mp >= mp_cost and cooldown == 0

View File

@@ -263,16 +263,23 @@
}, 1000);
}
// Handle player action triggering enemy turn
// Handle player action - refresh page to update UI
document.body.addEventListener('htmx:afterRequest', function(event) {
const response = event.detail.xhr;
if (!response) return;
const triggers = response.getResponseHeader('HX-Trigger') || '';
// Check response status - only refresh on success
if (response.status !== 200) return;
// Only trigger enemy turn from player actions (not from our fetch calls)
if (triggers.includes('enemyTurn') && !enemyTurnPending) {
triggerEnemyTurn();
// Check custom header for combat refresh signal
const shouldRefresh = response.getResponseHeader('X-Combat-Refresh');
console.log('X-Combat-Refresh header:', shouldRefresh);
if (shouldRefresh === 'true') {
// Short delay to let user see their action result
setTimeout(function() {
window.location.reload();
}, 1000);
}
});

View File

@@ -16,8 +16,8 @@
hx-target="#combat-log"
hx-swap="beforeend"
hx-disabled-elt="this"
{% if not ability.available %}disabled{% endif %}
onclick="closeModal()">
hx-on::before-request="closeModal()"
{% if not ability.available %}disabled{% endif %}>
<span class="ability-icon">
{% if ability.damage_type == 'fire' %}&#128293;
{% elif ability.damage_type == 'ice' %}&#10052;