combat testing and polishing in the dev console, many bug fixes

This commit is contained in:
2025-11-27 20:37:53 -06:00
parent 94c4ca9e95
commit dd92cf5991
45 changed files with 8157 additions and 1106 deletions

View File

@@ -0,0 +1,258 @@
{% 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">&#9876;</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">&#10148;</span>
{% elif combatant.current_hp <= 0 %}
<span class="turn-order__check" title="Defeated">&#10007;</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' %}&#128737;
{% elif effect.effect_type == 'buff' %}&#11014;
{% elif effect.effect_type == 'debuff' %}&#11015;
{% elif effect.effect_type == 'dot' %}&#128293;
{% elif effect.effect_type == 'hot' %}&#10084;
{% else %}&#9733;
{% 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 %}