259 lines
11 KiB
HTML
259 lines
11 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.remaining_duration }} {% if effect.remaining_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();
|
|
}
|
|
});
|
|
|
|
// Guard against duplicate enemy turn requests
|
|
let enemyTurnPending = false;
|
|
let enemyTurnTimeout = null;
|
|
|
|
function triggerEnemyTurn() {
|
|
// Prevent duplicate requests
|
|
if (enemyTurnPending) {
|
|
return;
|
|
}
|
|
|
|
// Clear any pending timeout
|
|
if (enemyTurnTimeout) {
|
|
clearTimeout(enemyTurnTimeout);
|
|
}
|
|
|
|
enemyTurnPending = true;
|
|
enemyTurnTimeout = setTimeout(function() {
|
|
htmx.ajax('POST', '{{ url_for("combat.combat_enemy_turn", session_id=session_id) }}', {
|
|
target: '#combat-log',
|
|
swap: 'beforeend'
|
|
}).then(function() {
|
|
enemyTurnPending = false;
|
|
}).catch(function() {
|
|
enemyTurnPending = false;
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
// Handle enemy turn polling
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
// Check if we need to trigger enemy turn
|
|
const response = event.detail.xhr;
|
|
if (response && response.getResponseHeader('HX-Trigger')) {
|
|
const triggers = response.getResponseHeader('HX-Trigger');
|
|
if (triggers && triggers.includes('enemyTurn')) {
|
|
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
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|