feat: Sistema de combate completo con tarjetas de personajes y animaciones

- Tarjetas de héroes y monstruos con tokens circulares
- Sistema de selección: héroe + monstruo para atacar
- Botón de ATACAR en tarjeta de monstruo
- Animación de muerte: fade-out + hundimiento (1.5s)
- Visualización de estadísticas completas (WS, BS, S, T, W, I, A, Mov)
- Placeholder cuando no hay héroe seleccionado
- Tokens de héroes y monstruos en formato circular
- Deselección correcta de monstruos
- Fix: paso de gameEngine a CombatMechanics para callbacks de muerte
This commit is contained in:
2026-01-06 18:43:09 +01:00
parent 3efbf8d5fb
commit 7b28fcf1b0
22 changed files with 1513 additions and 25 deletions

View File

@@ -20,7 +20,7 @@ export class CombatMechanics {
* @param {Object} defender
* @returns {Object} Result log
*/
static resolveMeleeAttack(attacker, defender) {
static resolveMeleeAttack(attacker, defender, gameEngine = null) {
const log = {
attackerId: attacker.id,
defenderId: defender.id,
@@ -88,7 +88,7 @@ export class CombatMechanics {
}
// 6. Apply Damage to Defender State
this.applyDamage(defender, wounds);
this.applyDamage(defender, wounds, gameEngine);
if (defender.isDead) {
log.defenderDied = true;
@@ -110,7 +110,7 @@ export class CombatMechanics {
return 6; // Fallback
}
static applyDamage(entity, amount) {
static applyDamage(entity, amount, gameEngine = null) {
if (!entity.stats) entity.stats = {};
// If entity doesn't have current wounds tracked, init it from max
@@ -135,6 +135,10 @@ export class CombatMechanics {
if (entity.currentWounds <= 0) {
entity.currentWounds = 0;
entity.isDead = true;
// Trigger death callback if available
if (gameEngine && gameEngine.onEntityDeath) {
gameEngine.onEntityDeath(entity.id);
}
}
}
}

View File

@@ -27,6 +27,7 @@ export class GameEngine {
this.onEntitySelect = null;
this.onEntityActive = null; // New: When entity starts/ends turn
this.onEntityHit = null; // New: When entity takes damage
this.onEntityDeath = null; // New: When entity dies
this.onPathChange = null;
}
@@ -149,29 +150,42 @@ export class GameEngine {
const clickedHero = this.heroes.find(h => h.x === x && h.y === y);
const clickedMonster = this.monsters ? this.monsters.find(m => m.x === x && m.y === y && !m.isDead) : null;
// COMBAT: Hero Attack Check
if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') {
const attackResult = this.performHeroAttack(clickedMonster.id);
if (attackResult && attackResult.success) {
// Attack performed, do not deselect hero
return;
}
// If attack failed (e.g. not adjacent), proceeds to select the monster
}
const clickedEntity = clickedHero || clickedMonster;
if (clickedEntity) {
if (this.selectedEntity === clickedEntity) {
// Toggle Deselect
this.deselectEntity();
} else {
// Select new entity
if (this.selectedEntity) this.deselectEntity();
this.selectedEntity = clickedEntity;
} else if (this.selectedMonster === clickedMonster && clickedMonster) {
// Clicking on already selected monster - deselect it
const monsterId = this.selectedMonster.id;
this.selectedMonster = null;
if (this.onEntitySelect) {
this.onEntitySelect(clickedEntity.id, true);
this.onEntitySelect(monsterId, false);
}
} else {
// Select new entity (don't deselect hero if clicking monster)
if (clickedMonster && this.selectedEntity && this.selectedEntity.type === 'hero') {
// Deselect previous monster if any
if (this.selectedMonster) {
const prevMonsterId = this.selectedMonster.id;
if (this.onEntitySelect) {
this.onEntitySelect(prevMonsterId, false);
}
}
// Keep hero selected, also select monster
this.selectedMonster = clickedMonster;
if (this.onEntitySelect) {
this.onEntitySelect(clickedMonster.id, true);
}
} else {
// Normal selection (deselect previous)
if (this.selectedEntity) this.deselectEntity();
this.selectedEntity = clickedEntity;
if (this.onEntitySelect) {
this.onEntitySelect(clickedEntity.id, true);
}
}
}
return;
@@ -201,7 +215,7 @@ export class GameEngine {
if (hero.hasAttacked) return { success: false, reason: 'cooldown' };
// Execute Attack
const result = CombatMechanics.resolveMeleeAttack(hero, monster);
const result = CombatMechanics.resolveMeleeAttack(hero, monster, this);
hero.hasAttacked = true;
if (this.onCombatResult) this.onCombatResult(result);
@@ -216,6 +230,13 @@ export class GameEngine {
this.plannedPath = [];
if (this.onEntitySelect) this.onEntitySelect(id, false);
if (this.onPathChange) this.onPathChange([]);
// Also deselect monster if selected
if (this.selectedMonster) {
const monsterId = this.selectedMonster.id;
this.selectedMonster = null;
if (this.onEntitySelect) this.onEntitySelect(monsterId, false);
}
}
// Alias for legacy calls if any

View File

@@ -220,7 +220,7 @@ export class MonsterAI {
// 4. Remove both rings
// 5. Show combat result
const result = CombatMechanics.resolveMeleeAttack(monster, hero);
const result = CombatMechanics.resolveMeleeAttack(monster, hero, this.game);
// Step 1: Green ring on attacker
if (this.game.onEntityActive) {