export const TO_HIT_CHART = [ // Defender WS 1 2 3 4 5 6 7 8 9 10 /* Attacker 1 */[4, 4, 5, 6, 6, 6, 6, 6, 6, 6], /* Attacker 2 */[3, 4, 4, 4, 5, 5, 6, 6, 6, 6], /* Attacker 3 */[2, 3, 4, 4, 4, 4, 5, 5, 5, 6], /* Attacker 4 */[2, 3, 3, 4, 4, 4, 4, 4, 5, 5], /* Attacker 5 */[2, 2, 3, 3, 4, 4, 4, 4, 4, 4], /* Attacker 6 */[2, 2, 3, 3, 3, 4, 4, 4, 4, 4], /* Attacker 7 */[2, 2, 2, 3, 3, 3, 4, 4, 4, 4], /* Attacker 8 */[2, 2, 2, 3, 3, 3, 3, 4, 4, 4], /* Attacker 9 */[2, 2, 2, 2, 3, 3, 3, 3, 4, 4], /* Attacker 10*/[2, 2, 2, 2, 3, 3, 3, 3, 3, 4] ]; export class CombatMechanics { /** * Resolves a melee attack sequence between two entities. * @param {Object} attacker * @param {Object} defender * @returns {Object} Result log */ static resolveMeleeAttack(attacker, defender, gameEngine = null) { const log = { attackerId: attacker.id, defenderId: defender.id, hitRoll: 0, targetToHit: 0, hitSuccess: false, damageRoll: 0, damageTotal: 0, woundsCaused: 0, defenderDied: false, message: '' }; // 1. Determine Stats // Use stats object if available, otherwise direct property (fallback) const attStats = attacker.stats || attacker; const defStats = defender.stats || defender; const attWS = Math.min(Math.max(attStats.ws || 1, 1), 10); const defWS = Math.min(Math.max(defStats.ws || 1, 1), 10); // 2. Roll To Hit log.targetToHit = this.getToHitTarget(attWS, defWS); log.hitRoll = this.rollD6(); // Debug // console.log(`Combat: ${attacker.name} (WS${attWS}) vs ${defender.name} (WS${defWS}) -> Need ${log.targetToHit}+. Rolled ${log.hitRoll}`); if (log.hitRoll < log.targetToHit) { log.hitSuccess = false; log.message = `${attacker.name} falla el ataque (Sacó ${log.hitRoll}, necesita ${log.targetToHit}+).`; return log; } log.hitSuccess = true; // 3. Roll To Damage const attStr = attStats.str || 3; const defTough = defStats.toughness || 3; const damageDice = attStats.damageDice || 1; // Default 1D6 let damageSum = 0; let rolls = []; for (let i = 0; i < damageDice; i++) { const r = this.rollD6(); rolls.push(r); damageSum += r; } log.damageRoll = damageSum; // Just sum for simple log, or we could array it log.damageTotal = damageSum + attStr; // 4. Calculate Wounds // Wounds = (Dice + Str) - Toughness let wounds = log.damageTotal - defTough; if (wounds < 0) wounds = 0; log.woundsCaused = wounds; // 5. Build Message if (wounds > 0) { log.message = `${attacker.name} impacta y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`; } else { log.message = `${attacker.name} impacta pero no logra herir. (Daño ${log.damageTotal} vs Res ${defTough})`; } // 6. Apply Damage to Defender State this.applyDamage(defender, wounds, gameEngine); if (defender.isDead) { log.defenderDied = true; log.message += ` ¡${defender.name} ha muerto!`; } else if (defender.isUnconscious) { log.message += ` ¡${defender.name} cae inconsciente!`; } return log; } static resolveRangedAttack(attacker, defender, gameEngine = null) { const log = { attackerId: attacker.id, defenderId: defender.id, hitSuccess: false, damageTotal: 0, woundsCaused: 0, defenderDied: false, message: '' }; // 1. Roll To Hit (BS vs WS) // Use attacker BS or default to WS if missing (fallback). const attackerBS = attacker.stats.bs || attacker.stats.ws; const defenderWS = defender.stats.ws; const toHitTarget = this.getToHitTarget(attackerBS, defenderWS); const hitRoll = this.rollD6(); log.hitRoll = hitRoll; log.toHitTarget = toHitTarget; if (hitRoll === 1) { log.hitSuccess = false; log.message = `${attacker.name} dispara y falla (1 es fallo automático)`; return log; } if (hitRoll < toHitTarget) { log.hitSuccess = false; log.message = `${attacker.name} dispara y falla (${hitRoll} < ${toHitTarget})`; return log; } log.hitSuccess = true; // 2. Roll Damage // Elf Bow Strength = 3 const weaponStrength = 3; const damageRoll = this.rollD6(); const damageTotal = weaponStrength + damageRoll; log.damageRoll = damageRoll; log.damageTotal = damageTotal; // 3. Compare vs Toughness const defTough = defender.stats.toughness || 1; const wounds = Math.max(0, damageTotal - defTough); log.woundsCaused = wounds; // 4. Build Message if (wounds > 0) { log.message = `${attacker.name} acierta con el arco y causa ${wounds} heridas! (Daño ${log.damageTotal} - Res ${defTough})`; } else { log.message = `${attacker.name} acierta pero la flecha rebota. (Daño ${log.damageTotal} vs Res ${defTough})`; } // 5. Apply Damage this.applyDamage(defender, wounds, gameEngine); if (defender.isDead) { log.defenderDied = true; log.message += ` ¡${defender.name} ha muerto!`; } return log; } static getToHitTarget(attackerWS, defenderWS) { // Adjust for 0-index array const row = attackerWS - 1; const col = defenderWS - 1; if (TO_HIT_CHART[row] && TO_HIT_CHART[row][col]) { return TO_HIT_CHART[row][col]; } return 6; // Fallback } static applyDamage(entity, amount, gameEngine = null) { if (!entity.stats) entity.stats = {}; // If entity doesn't have current wounds tracked, init it from max if (entity.currentWounds === undefined) { // For Heros it is 'wounds', for Monsters typical just 'wounds' in def // We assume entity has been initialized properly before, // but if not, we grab max from definition entity.currentWounds = entity.stats.wounds || 1; } entity.currentWounds -= amount; // Check Status if (entity.type === 'hero') { if (entity.currentWounds <= 0) { entity.currentWounds = 0; entity.isConscious = false; // entity.isDead is not immediate for heroes usually, but let's handle via isConscious } } else { // Monsters die at 0 if (entity.currentWounds <= 0) { entity.currentWounds = 0; entity.isDead = true; // Trigger death callback if available if (gameEngine && gameEngine.onEntityDeath) { gameEngine.onEntityDeath(entity.id); } } } } static rollD6() { return Math.floor(Math.random() * 6) + 1; } }