297 lines
13 KiB
HTML
297 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Combat - Code of Conquest{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/combat.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="combat-page">
|
|
<div class="combat-container">
|
|
{# ===== COMBAT HEADER ===== #}
|
|
<header class="combat-header">
|
|
<h1 class="combat-title">
|
|
<span class="combat-title-icon">⚔</span>
|
|
Combat Encounter
|
|
</h1>
|
|
<div class="combat-round">
|
|
<span class="round-counter">Round <strong>{{ encounter.round_number }}</strong></span>
|
|
{% if is_player_turn %}
|
|
<span class="turn-indicator turn-indicator--player">Your Turn</span>
|
|
{% else %}
|
|
<span class="turn-indicator turn-indicator--enemy">Enemy Turn</span>
|
|
{% endif %}
|
|
</div>
|
|
</header>
|
|
|
|
{# ===== LEFT COLUMN: COMBATANTS ===== #}
|
|
<aside class="combatant-panel">
|
|
{# Player Section #}
|
|
<div class="combatant-section">
|
|
<h2 class="combatant-section-title">Your Party</h2>
|
|
{% for combatant in encounter.combatants if combatant.is_player %}
|
|
<div class="combatant-card combatant-card--player {% if combatant.combatant_id == current_turn_id %}combatant-card--active{% endif %} {% if combatant.current_hp <= 0 %}combatant-card--defeated{% endif %}">
|
|
<div class="combatant-header">
|
|
<span class="combatant-name">{{ combatant.name }}</span>
|
|
<span class="combatant-level">Lv.{{ combatant.level|default(1) }}</span>
|
|
</div>
|
|
<div class="combatant-resources">
|
|
{% set hp_percent = ((combatant.current_hp / combatant.max_hp) * 100)|round|int %}
|
|
<div class="resource-bar resource-bar--hp {% if hp_percent < 25 %}low{% endif %}">
|
|
<div class="resource-bar-label">
|
|
<span class="resource-bar-name">HP</span>
|
|
<span class="resource-bar-value">{{ combatant.current_hp }} / {{ combatant.max_hp }}</span>
|
|
</div>
|
|
<div class="resource-bar-track">
|
|
<div class="resource-bar-fill" style="width: {{ hp_percent }}%"></div>
|
|
</div>
|
|
</div>
|
|
{% set mp_percent = ((combatant.current_mp / combatant.max_mp) * 100)|round|int if combatant.max_mp > 0 else 0 %}
|
|
<div class="resource-bar resource-bar--mp">
|
|
<div class="resource-bar-label">
|
|
<span class="resource-bar-name">MP</span>
|
|
<span class="resource-bar-value">{{ combatant.current_mp }} / {{ combatant.max_mp }}</span>
|
|
</div>
|
|
<div class="resource-bar-track">
|
|
<div class="resource-bar-fill" style="width: {{ mp_percent }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{# Enemies Section #}
|
|
<div class="combatant-section">
|
|
<h2 class="combatant-section-title">Enemies</h2>
|
|
{% for combatant in encounter.combatants if not combatant.is_player %}
|
|
<div class="combatant-card combatant-card--enemy {% if combatant.combatant_id == current_turn_id %}combatant-card--active{% endif %} {% if combatant.current_hp <= 0 %}combatant-card--defeated{% endif %}">
|
|
<div class="combatant-header">
|
|
<span class="combatant-name">{{ combatant.name }}</span>
|
|
<span class="combatant-level">{% if combatant.current_hp <= 0 %}Defeated{% endif %}</span>
|
|
</div>
|
|
<div class="combatant-resources">
|
|
{% set hp_percent = ((combatant.current_hp / combatant.max_hp) * 100)|round|int %}
|
|
<div class="resource-bar resource-bar--hp {% if hp_percent < 25 %}low{% endif %}">
|
|
<div class="resource-bar-label">
|
|
<span class="resource-bar-name">HP</span>
|
|
<span class="resource-bar-value">{{ combatant.current_hp }} / {{ combatant.max_hp }}</span>
|
|
</div>
|
|
<div class="resource-bar-track">
|
|
<div class="resource-bar-fill" style="width: {{ hp_percent }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</aside>
|
|
|
|
{# ===== CENTER COLUMN: COMBAT LOG + ACTIONS ===== #}
|
|
<main class="combat-main">
|
|
{# Combat Log #}
|
|
<div id="combat-log" class="combat-log" role="log" aria-live="polite" aria-label="Combat log">
|
|
{% include "game/partials/combat_log.html" %}
|
|
</div>
|
|
|
|
{# Combat Actions #}
|
|
<div id="combat-actions" class="combat-actions">
|
|
{% include "game/partials/combat_actions.html" %}
|
|
</div>
|
|
</main>
|
|
|
|
{# ===== RIGHT COLUMN: TURN ORDER + EFFECTS ===== #}
|
|
<aside class="combat-sidebar">
|
|
{# Turn Order #}
|
|
<div class="turn-order">
|
|
<h2 class="turn-order__title">Turn Order</h2>
|
|
<div class="turn-order__list">
|
|
{% for combatant_id in encounter.turn_order %}
|
|
{% set combatant = encounter.combatants|selectattr('combatant_id', 'equalto', combatant_id)|first %}
|
|
{% if combatant %}
|
|
<div class="turn-order__item {% if combatant.is_player %}turn-order__item--player{% else %}turn-order__item--enemy{% endif %} {% if combatant_id == current_turn_id %}turn-order__item--active{% endif %} {% if combatant.current_hp <= 0 %}turn-order__item--defeated{% endif %}">
|
|
<span class="turn-order__position">{{ loop.index }}</span>
|
|
<span class="turn-order__name">{{ combatant.name }}</span>
|
|
{% if combatant_id == current_turn_id %}
|
|
<span class="turn-order__check" title="Current turn">➤</span>
|
|
{% elif combatant.current_hp <= 0 %}
|
|
<span class="turn-order__check" title="Defeated">✗</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Active Effects #}
|
|
<div class="effects-panel">
|
|
<h2 class="effects-panel__title">Active Effects</h2>
|
|
{% if player_combatant and player_combatant.active_effects %}
|
|
<div class="effects-list">
|
|
{% for effect in player_combatant.active_effects %}
|
|
<div class="effect-item effect-item--{{ effect.effect_type|default('buff') }}">
|
|
<span class="effect-icon">
|
|
{% if effect.effect_type == 'shield' %}🛡
|
|
{% elif effect.effect_type == 'buff' %}⬆
|
|
{% elif effect.effect_type == 'debuff' %}⬇
|
|
{% elif effect.effect_type == 'dot' %}🔥
|
|
{% elif effect.effect_type == 'hot' %}❤
|
|
{% else %}★
|
|
{% endif %}
|
|
</span>
|
|
<span class="effect-name">{{ effect.name }}</span>
|
|
<span class="effect-duration">{{ effect.duration }} {% if effect.duration == 1 %}turn{% else %}turns{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="effects-empty">No active effects</p>
|
|
{% endif %}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
{# Modal Container for Ability selection #}
|
|
<div id="modal-container"></div>
|
|
|
|
{# Combat Items Sheet Container #}
|
|
<div id="combat-sheet-container"></div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Auto-scroll combat log to bottom on new entries
|
|
function scrollCombatLog() {
|
|
const log = document.getElementById('combat-log');
|
|
if (log) {
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Scroll on page load
|
|
document.addEventListener('DOMContentLoaded', scrollCombatLog);
|
|
|
|
// Scroll after HTMX swaps
|
|
document.body.addEventListener('htmx:afterSwap', function(event) {
|
|
if (event.detail.target.id === 'combat-log' ||
|
|
event.detail.target.closest('#combat-log')) {
|
|
scrollCombatLog();
|
|
}
|
|
});
|
|
|
|
// Close modal function
|
|
function closeModal() {
|
|
const container = document.getElementById('modal-container');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Close combat items sheet
|
|
function closeCombatSheet() {
|
|
const container = document.getElementById('combat-sheet-container');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Close modal/sheet on Escape key
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeModal();
|
|
closeCombatSheet();
|
|
}
|
|
});
|
|
|
|
// Enemy turn handling with proper chaining for multiple enemies
|
|
let enemyTurnPending = false;
|
|
|
|
function triggerEnemyTurn() {
|
|
// Prevent duplicate requests
|
|
if (enemyTurnPending) {
|
|
return;
|
|
}
|
|
enemyTurnPending = true;
|
|
|
|
setTimeout(function() {
|
|
// Use fetch instead of htmx.ajax for better control over response handling
|
|
fetch('{{ url_for("combat.combat_enemy_turn", session_id=session_id) }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'HX-Request': 'true'
|
|
},
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(function(response) {
|
|
const hasMoreEnemies = response.headers.get('HX-Trigger')?.includes('enemyTurn');
|
|
return response.text().then(function(html) {
|
|
return { html: html, hasMoreEnemies: hasMoreEnemies };
|
|
});
|
|
})
|
|
.then(function(data) {
|
|
// Append the log entry
|
|
const combatLog = document.getElementById('combat-log');
|
|
if (combatLog) {
|
|
combatLog.insertAdjacentHTML('beforeend', data.html);
|
|
combatLog.scrollTop = combatLog.scrollHeight;
|
|
}
|
|
|
|
enemyTurnPending = false;
|
|
|
|
if (data.hasMoreEnemies) {
|
|
// More enemies to go - trigger next enemy turn
|
|
triggerEnemyTurn();
|
|
} else {
|
|
// All enemies done - refresh page to update UI
|
|
setTimeout(function() {
|
|
window.location.reload();
|
|
}, 800);
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
console.error('Enemy turn failed:', error);
|
|
enemyTurnPending = false;
|
|
// Refresh anyway to recover from error state
|
|
setTimeout(function() {
|
|
window.location.reload();
|
|
}, 1000);
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
// Handle player action triggering enemy turn
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
const response = event.detail.xhr;
|
|
if (!response) return;
|
|
|
|
const triggers = response.getResponseHeader('HX-Trigger') || '';
|
|
|
|
// Only trigger enemy turn from player actions (not from our fetch calls)
|
|
if (triggers.includes('enemyTurn') && !enemyTurnPending) {
|
|
triggerEnemyTurn();
|
|
}
|
|
});
|
|
|
|
// Handle combat end redirect
|
|
document.body.addEventListener('htmx:beforeSwap', function(event) {
|
|
// If the response indicates combat ended, handle accordingly
|
|
const response = event.detail.xhr;
|
|
if (response && response.getResponseHeader('X-Combat-Ended')) {
|
|
// Let the full page swap happen for victory/defeat screen
|
|
}
|
|
});
|
|
|
|
// Auto-trigger enemy turn on page load if it's not the player's turn
|
|
{% if not is_player_turn %}
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Small delay to let the page render first
|
|
triggerEnemyTurn();
|
|
});
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %}
|