feat(game-loop): implement strict phase rules, exploration stops, and hero attacks
This commit is contained in:
145
src/engine/game/CombatMechanics.js
Normal file
145
src/engine/game/CombatMechanics.js
Normal file
@@ -0,0 +1,145 @@
|
||||
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) {
|
||||
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);
|
||||
|
||||
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 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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static rollD6() {
|
||||
return Math.floor(Math.random() * 6) + 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user