Merge branch 'bugfix/combat-abilities-fix' into dev
This commit is contained in:
@@ -189,6 +189,11 @@ def register_blueprints(app: Flask) -> None:
|
|||||||
app.register_blueprint(quests_bp)
|
app.register_blueprint(quests_bp)
|
||||||
logger.info("Quests API blueprint registered")
|
logger.info("Quests API blueprint registered")
|
||||||
|
|
||||||
|
# Import and register Abilities API blueprint
|
||||||
|
from app.api.abilities import abilities_bp
|
||||||
|
app.register_blueprint(abilities_bp)
|
||||||
|
logger.info("Abilities API blueprint registered")
|
||||||
|
|
||||||
# TODO: Register additional blueprints as they are created
|
# TODO: Register additional blueprints as they are created
|
||||||
# from app.api import marketplace
|
# from app.api import marketplace
|
||||||
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
|
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')
|
||||||
|
|||||||
129
api/app/api/abilities.py
Normal file
129
api/app/api/abilities.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
Abilities API Blueprint
|
||||||
|
|
||||||
|
This module provides API endpoints for fetching ability information:
|
||||||
|
- List all available abilities
|
||||||
|
- Get details for a specific ability
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
from app.models.abilities import AbilityLoader
|
||||||
|
from app.utils.response import (
|
||||||
|
success_response,
|
||||||
|
not_found_response,
|
||||||
|
)
|
||||||
|
from app.utils.logging import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
# Create blueprint
|
||||||
|
abilities_bp = Blueprint('abilities', __name__, url_prefix='/api/v1/abilities')
|
||||||
|
|
||||||
|
# Initialize ability loader (singleton pattern)
|
||||||
|
_ability_loader = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ability_loader() -> AbilityLoader:
|
||||||
|
"""
|
||||||
|
Get the singleton AbilityLoader instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AbilityLoader: The ability loader instance
|
||||||
|
"""
|
||||||
|
global _ability_loader
|
||||||
|
if _ability_loader is None:
|
||||||
|
_ability_loader = AbilityLoader()
|
||||||
|
return _ability_loader
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Ability Endpoints
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@abilities_bp.route('', methods=['GET'])
|
||||||
|
def list_abilities():
|
||||||
|
"""
|
||||||
|
List all available abilities.
|
||||||
|
|
||||||
|
Returns all abilities defined in the system with their full details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"abilities": [
|
||||||
|
{
|
||||||
|
"ability_id": "smite",
|
||||||
|
"name": "Smite",
|
||||||
|
"description": "Call down holy light...",
|
||||||
|
"ability_type": "spell",
|
||||||
|
"base_power": 20,
|
||||||
|
"damage_type": "holy",
|
||||||
|
"mana_cost": 10,
|
||||||
|
"cooldown": 0,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"count": 5
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
logger.info("Listing all abilities")
|
||||||
|
|
||||||
|
loader = get_ability_loader()
|
||||||
|
abilities = loader.load_all_abilities()
|
||||||
|
|
||||||
|
# Convert to list of dicts for JSON serialization
|
||||||
|
abilities_list = [ability.to_dict() for ability in abilities.values()]
|
||||||
|
|
||||||
|
logger.info("Abilities listed", count=len(abilities_list))
|
||||||
|
|
||||||
|
return success_response({
|
||||||
|
"abilities": abilities_list,
|
||||||
|
"count": len(abilities_list)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@abilities_bp.route('/<ability_id>', methods=['GET'])
|
||||||
|
def get_ability(ability_id: str):
|
||||||
|
"""
|
||||||
|
Get details for a specific ability.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ability_id: The unique identifier for the ability (e.g., "smite")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"ability_id": "smite",
|
||||||
|
"name": "Smite",
|
||||||
|
"description": "Call down holy light to smite your enemies",
|
||||||
|
"ability_type": "spell",
|
||||||
|
"base_power": 20,
|
||||||
|
"damage_type": "holy",
|
||||||
|
"scaling_stat": "wisdom",
|
||||||
|
"scaling_factor": 0.5,
|
||||||
|
"mana_cost": 10,
|
||||||
|
"cooldown": 0,
|
||||||
|
"effects_applied": [],
|
||||||
|
"is_aoe": false,
|
||||||
|
"target_count": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
404: Ability not found
|
||||||
|
"""
|
||||||
|
logger.info("Getting ability", ability_id=ability_id)
|
||||||
|
|
||||||
|
loader = get_ability_loader()
|
||||||
|
ability = loader.load_ability(ability_id)
|
||||||
|
|
||||||
|
if ability is None:
|
||||||
|
logger.warning("Ability not found", ability_id=ability_id)
|
||||||
|
return not_found_response(
|
||||||
|
message=f"Ability '{ability_id}' not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Ability retrieved", ability_id=ability_id, name=ability.name)
|
||||||
|
|
||||||
|
return success_response(ability.to_dict())
|
||||||
@@ -145,27 +145,29 @@ def combat_action(session_id: str):
|
|||||||
# API returns data directly in result, not nested under 'action_result'
|
# API returns data directly in result, not nested under 'action_result'
|
||||||
log_entries = []
|
log_entries = []
|
||||||
|
|
||||||
# Player action entry
|
# The API message is self-contained (includes actor name and damage)
|
||||||
player_entry = {
|
# Don't add separate actor/damage to avoid duplication
|
||||||
'actor': 'You',
|
message = result.get('message', f'Used {action_type}')
|
||||||
'message': result.get('message', f'used {action_type}'),
|
|
||||||
'type': 'player',
|
|
||||||
'is_crit': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add damage info if present
|
|
||||||
damage_results = result.get('damage_results', [])
|
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'):
|
if result.get('healing'):
|
||||||
player_entry['heal'] = result.get('healing')
|
entry_type = 'heal'
|
||||||
player_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)
|
log_entries.append(player_entry)
|
||||||
|
|
||||||
@@ -179,18 +181,32 @@ def combat_action(session_id: str):
|
|||||||
'type': 'system'
|
'type': 'system'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Return log entries HTML
|
# Check if it's now enemy's turn
|
||||||
resp = make_response(render_template(
|
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',
|
'game/partials/combat_log.html',
|
||||||
combat_log=log_entries
|
combat_log=log_entries
|
||||||
))
|
)
|
||||||
|
|
||||||
# Trigger enemy turn if it's no longer player's turn
|
# Add script to trigger page refresh after showing the action result
|
||||||
next_combatant = result.get('next_combatant_id')
|
# This is more reliable than headers which can be modified by HTMX
|
||||||
if next_combatant and not result.get('next_is_player', True):
|
refresh_script = '''
|
||||||
resp.headers['HX-Trigger'] = 'enemyTurn'
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1200);
|
||||||
|
</script>
|
||||||
|
'''
|
||||||
|
|
||||||
return resp
|
return log_html + refresh_script
|
||||||
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
logger.error("combat_action_failed", session_id=session_id, action_type=action_type, error=str(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_response = client.get(f'/api/v1/abilities/{ability_id}')
|
||||||
ability_data = ability_response.get('result', {})
|
ability_data = ability_response.get('result', {})
|
||||||
|
|
||||||
# Check availability
|
# Check availability (API returns 'mana_cost', template uses 'mp_cost')
|
||||||
mp_cost = ability_data.get('mp_cost', 0)
|
mp_cost = ability_data.get('mana_cost', ability_data.get('mp_cost', 0))
|
||||||
cooldown = cooldowns.get(ability_id, 0)
|
cooldown = cooldowns.get(ability_id, 0)
|
||||||
available = current_mp >= mp_cost and cooldown == 0
|
available = current_mp >= mp_cost and cooldown == 0
|
||||||
|
|
||||||
|
|||||||
@@ -263,16 +263,23 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle player action triggering enemy turn
|
// Handle player action - refresh page to update UI
|
||||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||||
const response = event.detail.xhr;
|
const response = event.detail.xhr;
|
||||||
if (!response) return;
|
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)
|
// Check custom header for combat refresh signal
|
||||||
if (triggers.includes('enemyTurn') && !enemyTurnPending) {
|
const shouldRefresh = response.getResponseHeader('X-Combat-Refresh');
|
||||||
triggerEnemyTurn();
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
hx-target="#combat-log"
|
hx-target="#combat-log"
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
hx-disabled-elt="this"
|
hx-disabled-elt="this"
|
||||||
{% if not ability.available %}disabled{% endif %}
|
hx-on::before-request="closeModal()"
|
||||||
onclick="closeModal()">
|
{% if not ability.available %}disabled{% endif %}>
|
||||||
<span class="ability-icon">
|
<span class="ability-icon">
|
||||||
{% if ability.damage_type == 'fire' %}🔥
|
{% if ability.damage_type == 'fire' %}🔥
|
||||||
{% elif ability.damage_type == 'ice' %}❄
|
{% elif ability.damage_type == 'ice' %}❄
|
||||||
|
|||||||
Reference in New Issue
Block a user