diff --git a/api/app/__init__.py b/api/app/__init__.py index 1f34fba..76e03f2 100644 --- a/api/app/__init__.py +++ b/api/app/__init__.py @@ -189,6 +189,11 @@ def register_blueprints(app: Flask) -> None: app.register_blueprint(quests_bp) 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 # from app.api import marketplace # app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace') diff --git a/api/app/api/abilities.py b/api/app/api/abilities.py new file mode 100644 index 0000000..79d36f2 --- /dev/null +++ b/api/app/api/abilities.py @@ -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('/', 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()) diff --git a/public_web/app/views/combat_views.py b/public_web/app/views/combat_views.py index 0988c4e..6e5f4b5 100644 --- a/public_web/app/views/combat_views.py +++ b/public_web/app/views/combat_views.py @@ -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 = ''' + + ''' - 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 diff --git a/public_web/templates/game/combat.html b/public_web/templates/game/combat.html index b72fbd0..75e0c0e 100644 --- a/public_web/templates/game/combat.html +++ b/public_web/templates/game/combat.html @@ -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); } }); diff --git a/public_web/templates/game/partials/ability_modal.html b/public_web/templates/game/partials/ability_modal.html index b0eb176..85a678c 100644 --- a/public_web/templates/game/partials/ability_modal.html +++ b/public_web/templates/game/partials/ability_modal.html @@ -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 %}> {% if ability.damage_type == 'fire' %}🔥 {% elif ability.damage_type == 'ice' %}❄