// 3d241eecc8e75f3e610ca12957e46fcfc4ab3d3e615d6c02cd3b7c9ec383513a_o1 class BattleUtils extends FyxClass { static isUnderStatusEffect(fighterState, statusEffect, round) { return ( fighterState.statusEffectsRound && fighterState.statusEffectsRound !== null && fighterState.statusEffectsRound[statusEffect] > round ); } static activeCooldownList(fighterState, round) { return fighterState.actionCooldownsRound.map((i) => i > round); } static isSkillInCooldown(battlePlayer, fighterState, round, skillType) { return BattleUtils.activeCooldownList(fighterState, round)[ battlePlayer.skills.findIndex((x) => x == skillType) ]; } static validateRandom(random, prevRandom) { let hashBytes = new Uint8Array(32); for (let i = 0; i < 64; i += 2) { hashBytes[i / 2] = parseInt(random.slice(i, i + 2), 16); } return Sha256.hashToHex(hashBytes) === prevRandom; } static rollDmgDice(ability, diceCount, diceFaces, dice, rolls, type) { let damage = 0; for (var i = 0; i < diceCount; ++i) { let toDamageNativeRoll = diceFaces === 0 ? 0 : dice.roll(1, diceFaces); rolls.push({ type: type, ability: ability, size: diceFaces, value: toDamageNativeRoll, }); damage += toDamageNativeRoll; } return damage; } static applySkillCooldown(battle, state, skillType, increment = -1) { let skillIndex = battle.battlePlayers[state.playerToAct].skills.findIndex( (x) => x.skillType == skillType ); state.fighterStates[state.playerToAct].actionCooldownsRound[skillIndex] = state.fighterStates[state.playerToAct].turnCounter + increment + 1; } static applyStatusEffect( actionLog, battle, state, statusEffect, increment, applyToAttacker = true ) { const statusEffectMap = Object.keys(Constants.StatusEffect).map(() => 0); if (applyToAttacker) { state.fighterStates[state.playerToAct].statusEffectsRound[statusEffect] = state.fighterStates[state.playerToAct].turnCounter + increment + 1; const lastResult = actionLog.results.slice(-1).pop(); if (!lastResult) { console.error( "BattleUtils.applyStatusEffect call with no results:", JSON.stringify(actionLog) ); return; } /*if (lastResult.attackerStatusEffectsIncrement === null || !lastResult.attackerStatusEffectsIncrement) { lastResult.attackerStatusEffectsIncrement = statusEffectMap; } lastResult.attackerStatusEffectsIncrement[statusEffect] = increment;*/ actionLog.actionLogMessage += BattleUtils.getPrettifiedPlayerName( battle, state, !applyToAttacker ); } else { let turnCount = statusEffect == Constants.StatusEffect.Stunned ? battle.turnCount : state.fighterStates[state.playerToAct ? 0 : 1].turnCounter; state.fighterStates[state.playerToAct ? 0 : 1].statusEffectsRound[ statusEffect ] = turnCount + increment; /*if (actionLog.results.slice(-1).pop().defenderStatusEffectsIncrement === null || !actionLog.results.slice(-1).pop().defenderStatusEffectsIncrement) { actionLog.results.slice(-1).pop().defenderStatusEffectsIncrement = statusEffectMap; } actionLog.results.slice(-1).pop().defenderStatusEffectsIncrement[statusEffect] = increment;*/ actionLog.actionLogMessage += BattleUtils.getOpponentName( battle, state, applyToAttacker ); } if (increment > 90) { actionLog.actionLogMessage += ` is ${BattleUtils.parseStatusEffect( statusEffect )}\n`; } else { actionLog.actionLogMessage += ` is ${BattleUtils.parseStatusEffect( statusEffect )} for ${increment <= 0 ? 1 : increment} turns\n`; } } static makeStatusEffectActionLog( battle, state, skill, statusEffect, increment, log, applyToAttacker = true ) { let attackerState = state.fighterStates[state.playerToAct]; let defenderState = state.fighterStates[state.playerToAct ? 0 : 1]; let damageMap = Object.keys(Constants.DamageType).map(() => 0); let actionLog = { playerIndex: state.playerToAct, skillType: skill, results: [ { rolls: [], outcome: Constants.Outcome.Success, damageOutput: [ { damage: damageMap, defenderHp: defenderState.hp, defenderReduction: damageMap, }, ], hpIncrement: 0, attackerHp: attackerState.hp, }, ], actionLogMessage: log, }; this.applyStatusEffect( actionLog, battle, state, statusEffect, increment, applyToAttacker ); return actionLog; } static abilityCheckAndApplyStatusEffect( battle, state, dice, skill, attackerAbility, defenderAbility, statusEffect, duration, applyToAttacker, log ) { const attackPlayer = battle.battlePlayers[state.playerToAct]; const attackerState = state.fighterStates[state.playerToAct]; const defenderState = state.fighterStates[state.playerToAct ? 0 : 1]; var playerName = BattleUtils.getPrettifiedPlayerName(battle, state); // Rolls let nativeRoll = dice.roll(1, 20); let rolls = []; rolls.push({ type: defenderAbility, ability: attackerAbility, size: 20, value: nativeRoll, }); // Check const defenderDC = 10 + defenderState.modifiers[defenderAbility]; let attackerMod = attackerState.modifiers[attackerAbility]; if (skill == Constants.SkillType.Hide) { let mainHand = false; if (attackPlayer.mainhand && attackPlayer.mainhand !== null) { if ( attackPlayer.mainhand.properties && attackPlayer.mainhand.properties !== null ) { mainHand = true; } } let offHand = false; if (attackPlayer.offhand && attackPlayer.offhand !== null) { if ( attackPlayer.offhand.properties && attackPlayer.offhand.properties !== null ) { offHand = true; } } if ( (!mainHand && !offHand) || (mainHand && attackPlayer.mainhand.properties[Constants.ItemProperty.Light] && offHand && attackPlayer.offhand.properties[Constants.ItemProperty.Light]) ) { attackerMod += 2; } } const outcome = nativeRoll + attackerMod >= defenderDC ? Constants.Outcome.Success : Constants.Outcome.Fail; log += `${playerName} attempts to ${BattleUtils.parseSkillName( skill )} : *${BattleUtils.parseSkillOutcome( outcome )}* : (${nativeRoll} + ${attackerMod} vs ${defenderDC} DC)\n`; // Success if (outcome === Constants.Outcome.Success) { var al = BattleUtils.makeStatusEffectActionLog( battle, state, skill, statusEffect, duration, log, applyToAttacker ); al.results[0].rolls = rolls; al.results[0].outcome = outcome; return al; } const damageMap = Object.keys(Constants.DamageType).map(() => 0); // Failure return { playerIndex: state.playerToAct, skillType: skill, results: [ { rolls: rolls, outcome: outcome, damageOutput: [ { damage: damageMap, defenderHp: defenderState.hp, defenderReduction: damageMap, }, ], attackerStatusEffectsIncrement: Object.keys( Constants.StatusEffect ).map(() => 0), hpIncrement: 0, attackerHp: attackerState.hp, }, ], actionLogMessage: log, }; } static parseAbility(ability) { return ability === 0 ? "Strength" : ability === 1 ? "Dexterity" : "Intelligence"; } static parseAttackOutcome(outcome) { return outcome === 0 ? "Miss" : outcome === 1 ? "Hit" : "Critical"; } static parseSkillOutcome(outcome) { return outcome === 0 ? "Fail" : outcome === 1 ? "Success" : "Critical"; } static getPrettifiedName(battle, playerIndex) { return `${battle.battlePlayers[ playerIndex ].fighter.metadata.name.toUpperCase()}`; } static getPrettifiedPlayerName(battle, state, switchAttacker = false) { let attackerIndex = switchAttacker ? state.playerToAct ? 0 : 1 : state.playerToAct; return this.getPrettifiedName(battle, attackerIndex); } static getOpponentName(battle, state, switchAttacker = false) { let attackerIndex = switchAttacker ? state.playerToAct : !state.playerToAct ? 1 : 0; return this.getPrettifiedName(battle, attackerIndex); } static parseSkillName(skill) { switch (skill) { case Constants.SkillType.Attack: return "Attack"; case Constants.SkillType.BattleCry: return "Battle Cry"; case Constants.SkillType.Burn: return "Burn"; case Constants.SkillType.Chill: return "Chill"; case Constants.SkillType.Cleanse: return "Cleanse"; case Constants.SkillType.CoatWeapon: return "Coat Weapon"; case Constants.SkillType.Curse: return "Curse"; case Constants.SkillType.DirtyFighting: return "Dirty Fighting"; case Constants.SkillType.Fireball: return "Fireball"; case Constants.SkillType.Focus: return "Focus"; case Constants.SkillType.Freeze: return "Freeze"; case Constants.SkillType.Gore: return "Gore"; case Constants.SkillType.Heal: return "Heal"; case Constants.SkillType.Hex: return "Hex"; case Constants.SkillType.Hide: return "Hide"; case Constants.SkillType.LightningBolt: return "Lightning Bolt"; case Constants.SkillType.LockAndLoad: return "Lock And Load"; case Constants.SkillType.Mark: return "Mark"; case Constants.SkillType.Meditate: return "Meditate"; case Constants.SkillType.PhaseShift: return "Phase Shift"; case Constants.SkillType.PinningStrike: return "Pinning Strike"; case Constants.SkillType.PowerAttack: return "Power Attack"; case Constants.SkillType.SecondWind: return "Second Wind"; case Constants.SkillType.Shock: return "Shock"; case Constants.SkillType.Smite: return "Smite"; case Constants.SkillType.SneakAttack: return "Sneak Attack"; case Constants.SkillType.Stun: return "Stun"; case Constants.SkillType.Taunt: return "Taunt"; case Constants.SkillType.Stab: return "Stab"; case Constants.SkillType.Strike: return "Strike"; case Constants.SkillType.Charge: return "Charge"; case Constants.SkillType.MundaneProtection: return "Mundane Protection"; } } static parseStatusEffect(statusEffect) { switch (statusEffect) { case Constants.StatusEffect.Cursed: return "cursed"; case Constants.StatusEffect.Ethereal: return "ethereal"; case Constants.StatusEffect.Focused: return "focused"; case Constants.StatusEffect.Hexed: return "hexed"; case Constants.StatusEffect.Hidden: return "hidden"; case Constants.StatusEffect.Inspired: return "inspired"; case Constants.StatusEffect.Marked: return "marked"; case Constants.StatusEffect.Meditative: return "meditative"; case Constants.StatusEffect.Poisoned: return "poisoned"; case Constants.StatusEffect.Stunned: return "stunned"; case Constants.StatusEffect.Taunted: return "taunted"; case Constants.StatusEffect.Poisonous: return "poisonous"; case Constants.StatusEffect.Demoralized: return "demoralized"; case Constants.StatusEffect.Swifted: return "swifted"; case Constants.StatusEffect.ProtectedAgainstMundaneDamage: return "protected against mundane damage"; } } static getItemResistance(item, defenderResistance) { if (item) { for (var j = 0; j < 10; j++) { if (item.damageReduction[j]) { defenderResistance[j] += item.damageReduction[j]; } } } } // Compute resistance according to items static getDefenderResistance(player) { var defenderResistance = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; this.getItemResistance(player.mainhand, defenderResistance); this.getItemResistance(player.offhand, defenderResistance); this.getItemResistance(player.armor, defenderResistance); this.getItemResistance(player.hat, defenderResistance); if (player.fighter.skills.includes(Constants.SkillType.Armored)) { if (player.fighter.skills.includes(Constants.SkillType.ImprovedArmored)) { for (var i = 0; i < 10; i++) { defenderResistance[i] += 1; } } else { defenderResistance[Constants.DamageType.Slashing] += 1; defenderResistance[Constants.DamageType.Piercing] += 1; defenderResistance[Constants.DamageType.Bludgeoning] += 1; } } if (player.fighter.skills.includes(Constants.SkillType.Dwarven)) { defenderResistance[Constants.DamageType.Slashing] += 1; defenderResistance[Constants.DamageType.Piercing] += 1; defenderResistance[Constants.DamageType.Bludgeoning] += 1; } return defenderResistance; } static getItemBonus(item, defenderBonus) { if (item) { for (var j = 0; j < 18; j++) { if (item.bonuses[j]) { defenderBonus[j] += item.bonuses[j]; } } } } // Compute bonuses according to items static getItemBonuses(player) { var defenderBonus = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; this.getItemBonus(player.mainhand, defenderBonus); this.getItemBonus(player.offhand, defenderBonus); this.getItemBonus(player.armor, defenderBonus); this.getItemBonus(player.hat, defenderBonus); return defenderBonus; } static buildBattleState(players, dice) { const diceRolls = players.map(() => dice.roll(1, 20)); const fighterStates = players.map((player, i) => { const fighterState = this.buildFighterState(player); fighterState.initiative += fighterState.modifiers[Constants.Ability.Dexterity] + diceRolls[i]; fighterState.turnCounter = 0; return fighterState; }); let logMessage = ""; let playerToAct; if (fighterStates[0].initiative === fighterStates[1].initiative) { if ( fighterStates[0].modifiers[Constants.Ability.Dexterity] === fighterStates[1].modifiers[Constants.Ability.Dexterity] ) { logMessage += "\nIt's a tie, tossing a coin"; playerToAct = dice.roll(1, 2) - 1; } else { playerToAct = fighterStates[0].modifiers[Constants.Ability.Dexterity] > fighterStates[1].modifiers[Constants.Ability.Dexterity] ? 0 : 1; logMessage += `\n${players[ playerToAct ].fighter.metadata.name.toUpperCase()} has higher dexterity`; } } else { playerToAct = fighterStates[0].initiative > fighterStates[1].initiative ? 0 : 1; } const damageMap = Object.keys(Constants.DamageType).map(() => 0); const statusEffectMap = Object.keys(Constants.StatusEffect).map(() => 0); const actionLogs = fighterStates.map((fighterState, i) => { return { playerIndex: i, skillType: Constants.SkillType.Focus, results: [ { rolls: [ { type: Constants.RollType.Initiative, ability: Constants.Ability.Dexterity, size: 20, value: diceRolls[i], discarded: false, bonusFromAbility: fighterState.modifiers[Constants.Ability.Dexterity], bonusFromSkill: 0, totalBonus: fighterState.modifiers[Constants.Ability.Dexterity], }, ], outcome: playerToAct === i ? Constants.Outcome.Success : Constants.Outcome.Fail, damageOutput: [ { damage: damageMap, defenderHp: fighterStates[i ? 0 : 1].maxHP, defenderReduction: damageMap, }, ], attackerStatusEffectsIncrement: statusEffectMap, defenderStatusEffectsIncrement: statusEffectMap, hpIncrement: 0, attackerHp: fighterState.maxHP, }, ], actionLogMessage: `${players[ i ].fighter.metadata.name.toUpperCase()} attempts Initiative: ${ diceRolls[i] + fighterState.modifiers[Constants.Ability.Dexterity] }` + (players[i].fighter.skills.includes(Constants.SkillType.Elven) ? " + 10" : ""), }; }); logMessage += `\n${players[ playerToAct ].fighter.metadata.name.toUpperCase()} won Initiative`; actionLogs[playerToAct ? 0 : 1].actionLogMessage += logMessage; return { status: Constants.Status.Open, fighterStates, playerToAct, actionLogs, }; } static simulateFighterState(battlePlayer) { const fighter = battlePlayer.fighter; // Construct fighter states const items = [ battlePlayer.mainhand, battlePlayer.offhand, battlePlayer.armor, battlePlayer.hat, ]; const derivedStats = BattleUtils.getDerivedStats(fighter, items); const fighterModifiers = derivedStats.modifiers; const hpIncrement = derivedStats.hpIncrement + fighter.hpBonus; // Create fighter state const fighterState = { abilityScoreBonuses: derivedStats.abilityScoreBonuses, abilityScores: derivedStats.abilityScores, modifiers: fighterModifiers, evasion: derivedStats.evasion, hp: fighterModifiers[Constants.Ability.Strength] + 10 + hpIncrement, maxHP: fighterModifiers[Constants.Ability.Strength] + 10 + hpIncrement, critChance: derivedStats.critChance, statusEffectsRound: Object.keys(Constants.StatusEffect).map(() => 0), areLoaded: [false, false], actionCooldownsRound: [0, 0, 0, 0], validActions: BattleUtils.getValidActions( battlePlayer.skills, battlePlayer.fighter.skills, [false, false, false, false], false, true, true, true, battlePlayer.mainhand, battlePlayer.offhand ), resistances: BattleUtils.getDefenderResistance(battlePlayer), initiative: derivedStats.initiative, }; return fighterState; } static buildFighterState(battlePlayer) { const fighter = battlePlayer.fighter; // Construct fighter states const items = [ battlePlayer.mainhand, battlePlayer.offhand, battlePlayer.armor, battlePlayer.hat, ]; const derivedStats = BattleUtils.getDerivedStats(fighter, items); const fighterModifiers = derivedStats.modifiers; const hpIncrement = derivedStats.hpIncrement + fighter.hpBonus; // Create fighter state const fighterState = { abilityScoreBonuses: derivedStats.abilityScoreBonuses, abilityScores: derivedStats.abilityScores, modifiers: fighterModifiers, evasion: derivedStats.evasion, hp: fighterModifiers[Constants.Ability.Strength] + 10 + hpIncrement, maxHP: fighterModifiers[Constants.Ability.Strength] + 10 + hpIncrement, critChance: derivedStats.critChance, statusEffectsRound: Object.keys(Constants.StatusEffect).map(() => 0), areLoaded: [false, false], actionCooldownsRound: [0, 0, 0, 0], validActions: BattleUtils.getValidActions( battlePlayer.skills, fighter.skills, [false, false, false, false], false, false, false, false, battlePlayer.mainhand, battlePlayer.offhand ), resistances: BattleUtils.getDefenderResistance(battlePlayer), initiative: derivedStats.initiative, }; return fighterState; } // Derived unmutable stats (HP and MaxHP are mutable) static getDerivedStats(fighter, items) { let abilityScores = FyxClass.deepClone(fighter.abilityScores); let abilityScoreBonuses = [0, 0, 0]; let evasion = 0; let critChance = 0; let hpIncrement = fighter.skills.includes(Constants.SkillType.Durable) ? fighter.level * 2 : 0; let initiative = 0; let mainHand = false; if (items[0] && items[0] !== null) { if (items[0].properties && items[0].properties !== null) { mainHand = true; } } for (const item of items || []) { if (item) { abilityScores[Constants.Ability.Strength] += item.bonuses[Constants.Bonus.Strength]; abilityScoreBonuses[Constants.Ability.Strength] += item.bonuses[Constants.Bonus.Strength]; abilityScores[Constants.Ability.Dexterity] += item.bonuses[Constants.Bonus.Dexterity]; abilityScoreBonuses[Constants.Ability.Dexterity] += item.bonuses[Constants.Bonus.Dexterity]; abilityScores[Constants.Ability.Intelligence] += item.bonuses[Constants.Bonus.Intelligence]; abilityScoreBonuses[Constants.Ability.Intelligence] += item.bonuses[Constants.Bonus.Intelligence]; evasion += item.bonuses[Constants.Bonus.Evasion]; critChance += item.bonuses[Constants.Bonus.CritChance]; initiative += item.bonuses[Constants.Bonus.Initiative]; } } let modifiers = BattleUtils.getModifiers(abilityScores); evasion = modifiers[Constants.Ability.Dexterity] + 10; if (fighter.skills.includes(Constants.SkillType.Elven)) { initiative += 10; evasion += 2; } if (fighter.skills.includes(Constants.SkillType.Slippery)) { evasion += 2; } if (fighter.skills.includes(Constants.SkillType.Cirurgical)) { critChance += 1; } //we might need display two crits if we add offhand precision weapons if ( fighter.skills.includes(Constants.SkillType.Precise) && mainHand && items[0].properties[Constants.ItemProperty.Precision] ) { critChance += 1; } if (fighter.skills.includes(Constants.SkillType.Strong)) { modifiers[Constants.Ability.Strength] += 1; } return { abilityScoreBonuses: abilityScoreBonuses, abilityScores: abilityScores, modifiers: modifiers, evasion: evasion, critChance: critChance, initiative: initiative, hpIncrement: hpIncrement, }; } static getModifiers(abilityScores) { return [ parseInt( this.floor((abilityScores[Constants.Ability.Strength] - 10) / 2) ), parseInt( this.floor((abilityScores[Constants.Ability.Dexterity] - 10) / 2) ), parseInt( this.floor((abilityScores[Constants.Ability.Intelligence] - 10) / 2) ), ]; } static floor(value) { return value > 0 ? value >> 0 : (value - 0.5) >> 0; } static getValidActions( skills = [], skillTypes = [], skillsCooldown = [], isTaunted, isHiden, isInspired, enemyIsMarked, mainhand, offhand ) { const { SkillType, ItemProperty, DamageType } = Constants; let weapons = []; let mainHand = false; if (mainhand && mainhand !== null) { if (mainhand.properties && mainhand.properties !== null) { mainHand = true; weapons.push(mainhand); } } let offHand = false; if (offhand && offhand !== null) { if (offhand.properties && offhand.properties !== null) { offHand = true; weapons.push(offhand); } } let skillIndex = -1; return skills.map((skill) => { let validSkill = false; skillIndex++; if (skill) { if (isTaunted && !skill.isAttackAction) { return false; } else if (skill.skillType === SkillType.Charge) { validSkill = (weapons.length == 0 && skillTypes.includes(SkillType.Unarmed)) || (weapons.length == 1 && weapons[0].properties[ItemProperty.Heavy] && weapons[0].properties[ItemProperty.Brutal]); } else if (skill.skillType == SkillType.Smite) { validSkill = (mainHand && mainhand.damageOutputs && mainhand.damageOutputs !== null && mainhand.damageOutputs.length > 0) || weapons.length == 0; } else if ( (mainHand && mainhand.properties[ItemProperty.Innocuous]) || (offHand && offhand.properties[ItemProperty.Innocuous]) ) { validSkill = !skill.isAttackAction || (skill.isAttackAction && skill.isSpellAction); } else if (skill.isAttackAction) { if (skill.skillType === SkillType.Strike) { validSkill = (mainHand && mainhand.properties[ItemProperty.Light]) || (offHand && offhand.properties[ItemProperty.Light]); } else if (skill.skillType === SkillType.Stab) { validSkill = !skillTypes.includes(SkillType.Warmongering) && ((mainHand && mainhand.properties[ItemProperty.Assault] && mainhand.damageOutputs && mainhand.damageOutputs !== null && mainhand.damageOutputs.length > 0 && (mainhand.damageOutputs[0].type == DamageType.Piercing || mainhand.damageOutputs[0].type == DamageType.Slashing)) || (offHand && offhand.properties[ItemProperty.Assault] && offhand.damageOutputs && offhand.damageOutputs !== null && offhand.damageOutputs.length > 0 && (offhand.damageOutputs[0].type == DamageType.Piercing || offhand.damageOutputs[0].type == DamageType.Slashing))); } else if (skill.skillType === SkillType.SneakAttack) { validSkill = (isHiden && (skillTypes.includes(SkillType.Hide) || skillTypes.includes(SkillType.Shadowy))) || (skillTypes.includes(SkillType.ImprovedSneakAttack) && (isInspired || enemyIsMarked)); } else if (skill.skillType === SkillType.LockAndLoad) { validSkill = (mainHand && mainhand.properties[ItemProperty.Loading]) || (offHand && offhand.properties[ItemProperty.Loading]); } else if (skill.skillType === SkillType.Stun) { validSkill = (weapons.length == 0 && skillTypes.includes(SkillType.Unarmed)) || (mainHand && mainhand.properties[ItemProperty.Brutal] && mainhand.properties[ItemProperty.Heavy]); } else { validSkill = true; } } else { validSkill = true; } } return validSkill == true && skillsCooldown[skillIndex] == false; }); } static computeFighterStates(battle, state) { var fighterStates = state.fighterStates; for (var i = 0; i < 2; i++) { // initialize mutable derived stats const items = [ battle.battlePlayers[i].mainhand, battle.battlePlayers[i].offhand, battle.battlePlayers[i].armor, battle.battlePlayers[i].hat, ]; var derivedStats = BattleUtils.getDerivedStats( battle.battlePlayers[i].fighter, items ); fighterStates[i].abilityScoreBonuses = derivedStats.abilityScoreBonuses; fighterStates[i].abilityScores = derivedStats.abilityScores; fighterStates[i].modifiers = derivedStats.modifiers; fighterStates[i].evasion = derivedStats.evasion; fighterStates[i].initiative = derivedStats.initiative; fighterStates[i].critChance = derivedStats.critChance; // offset modifiers and derived stats according to status effects if ( BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.Hexed, fighterStates[i].turnCounter ) ) { fighterStates[i].modifiers[Constants.Ability.Dexterity] -= fighterStates[i].modifiers[Constants.Ability.Intelligence]; } if ( BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.Focused, fighterStates[i].turnCounter ) ) { fighterStates[i].modifiers[Constants.Ability.Intelligence] += fighterStates[i].modifiers[Constants.Ability.Intelligence]; } let enemyIndex = i ? 0 : 1; fighterStates[i].validActions = BattleUtils.getValidActions( battle.battlePlayers[i].skills, battle.battlePlayers[i].fighter.skills, BattleUtils.activeCooldownList( fighterStates[i], fighterStates[i].turnCounter ), BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.Taunted, fighterStates[i].turnCounter ), BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.Hidden, fighterStates[i].turnCounter ), BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.isInspired, fighterStates[i].turnCounter ), BattleUtils.isUnderStatusEffect( fighterStates[enemyIndex], Constants.StatusEffect.Marked, fighterStates[enemyIndex].turnCounter ), battle.battlePlayers[i].mainhand, battle.battlePlayers[i].offhand ); fighterStates[i].evasion = BattleUtils.isUnderStatusEffect( fighterStates[i], Constants.StatusEffect.Ethereal, fighterStates[i].turnCounter ) ? fighterStates[i].evasion + fighterStates[i].modifiers[Constants.Ability.Intelligence] : fighterStates[i].evasion; } return fighterStates; } static skipTurn(battle, state, dice) { const defenderState = state.fighterStates[state.playerToAct ? 0 : 1]; const attackerState = state.fighterStates[state.playerToAct]; const damageMap = Object.keys(Constants.DamageType).map(() => 0); const statusEffectMap = Object.keys(Constants.StatusEffect).map(() => 0); var actionLog = { playerIndex: state.playerToAct, actionLogMessage: `${this.getPrettifiedPlayerName( battle, state, false )} skipped turn`, results: [ { rolls: [], outcome: Constants.Outcome.Success, damageOutput: [ { damage: damageMap, defenderHp: defenderState.hp, defenderReduction: damageMap, }, ], attackerStatusEffectsIncrement: statusEffectMap, hpIncrement: 0, attackerHp: attackerState.hp, }, ], }; state.actionLogs.push(actionLog); return this.endTurn(battle, state, dice); } static endTurn(battle, state, dice) { var attackerIndex = state.playerToAct; var defenderIndex = state.playerToAct ? 0 : 1; if (state.fighterStates[0].hp > 0 && state.fighterStates[1].hp > 0) { if ( BattleUtils.isUnderStatusEffect( state.fighterStates[attackerIndex], Constants.StatusEffect.Cursed, state.fighterStates[attackerIndex].turnCounter ) ) { if ( state.actionLogs && state.actionLogs.slice(-1).pop().skillType === Constants.SkillType.Vengeful ) { let vengefulLog = `${this.getPrettifiedPlayerName( battle, state, true )} is vengeful\n`; let damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[Constants.DamageType.Necrotic] = 1; state.actionLogs.push( this.makeDamageActionLog( battle, state, state.fighterStates[attackerIndex], battle.battlePlayers[attackerIndex], damageMap, [], Constants.Outcome.Success, Constants.SkillType.Vengeful, vengefulLog, "", true ) ); } else { var damage = dice.roll(1, 4); var roll = [ { type: Constants.RollType.ToDamage, ability: Constants.Ability.Intelligence, size: 4, value: damage, }, ]; let damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[Constants.DamageType.Necrotic] = damage; state.actionLogs.push( this.makeDamageActionLog( battle, state, state.fighterStates[attackerIndex], battle.battlePlayers[attackerIndex], damageMap, [], Constants.Outcome.Success, Constants.SkillType.Curse, "", "", true ) ); } } if ( BattleUtils.isUnderStatusEffect( state.fighterStates[attackerIndex], Constants.StatusEffect.Poisoned, state.fighterStates[attackerIndex].turnCounter ) ) { var damage = dice.roll(1, 4); var roll = [ { type: Constants.RollType.ToDamage, ability: Constants.Ability.Dexterity, size: 4, value: damage, }, ]; let damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[Constants.DamageType.Poison] = damage; state.actionLogs.push( this.makeDamageActionLog( battle, state, state.fighterStates[attackerIndex], battle.battlePlayers[attackerIndex], damageMap, roll, Constants.Outcome.Success, Constants.SkillType.CoatWeapon, "", "", true ) ); } if ( BattleUtils.isUnderStatusEffect( state.fighterStates[attackerIndex], Constants.StatusEffect.Burned, state.fighterStates[attackerIndex].turnCounter ) ) { var damage = dice.roll(1, 4); var roll = [ { type: Constants.RollType.ToDamage, ability: Constants.Ability.Intelligence, size: 4, value: damage, }, ]; let damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[Constants.DamageType.Fire] = damage; state.actionLogs.push( this.makeDamageActionLog( battle, state, state.fighterStates[attackerIndex], battle.battlePlayers[attackerIndex], damageMap, roll, Constants.Outcome.Success, Constants.SkillType.Fireball, "", "", true ) ); } let fatigueThreshold = 7; // TODO: I'd like this to be a global const, but not quite sure how to do that right now if (state.fighterStates[attackerIndex].turnCounter > fatigueThreshold) { let fatigueLog = `${this.getPrettifiedPlayerName( battle, state )} is fatigued\n`; let damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[Constants.DamageType.Necrotic] = state.fighterStates[attackerIndex].turnCounter - fatigueThreshold; state.actionLogs.push( this.makeDamageActionLog( battle, state, state.fighterStates[attackerIndex], battle.battlePlayers[attackerIndex], damageMap, [], Constants.Outcome.Success, Constants.SkillType.Curse, fatigueLog, "", true ) ); } if ( BattleUtils.isUnderStatusEffect( state.fighterStates[defenderIndex], Constants.StatusEffect.Stunned, battle.turnCount ) ) { state.fighterStates[defenderIndex].turnCounter++; state.fighterStates[attackerIndex].turnCounter++; } else { state.playerToAct = (state.playerToAct + 1) % 2; } } if (state.fighterStates[0].hp <= 0 || state.fighterStates[1].hp <= 0) { const victorIndex = state.fighterStates[attackerIndex].hp > state.fighterStates[defenderIndex].hp ? attackerIndex : defenderIndex; state.status = Constants.Status.Complete; var results = state.actionLogs.slice(-1).pop().results; results.slice(-1).pop().resultMessage += `${this.getPrettifiedName( battle, victorIndex )} won`; state.playerToAct = victorIndex; state.victor = { pubkey: battle.battlePlayers[victorIndex].pubkey, userId: battle.battlePlayers[victorIndex].userId, owner: battle.battlePlayers[victorIndex].owner, fighter: battle.battlePlayers[victorIndex].fighter, }; } state.fighterStates = BattleUtils.computeFighterStates(battle, state); if ( !BattleUtils.isUnderStatusEffect( state.fighterStates[defenderIndex], Constants.StatusEffect.Stunned, battle.turnCount ) ) { state.fighterStates[attackerIndex].turnCounter++; } return state; } static makeDamageActionLog( battle, state, defenderState, defenderPlayer, damageMap, rolls, outcome, skillType, log, preResultDescription, damageSelf = false ) { return { playerIndex: state.playerToAct, skillType: skillType, results: [ this.makeDamageResult( battle, state, defenderState, defenderPlayer, damageMap, null, rolls, outcome, preResultDescription, damageSelf ), ], actionLogMessage: log, }; } static makeDamageResult( battle, state, damageState, damagePlayer, damageMap, weaponFlatDamageMap, rolls, outcome, preResultDescription, damageSelf = false ) { var playerName = BattleUtils.getPrettifiedPlayerName(battle, state); var opponentName = BattleUtils.getOpponentName(battle, state); if ( weaponFlatDamageMap === null || !weaponFlatDamageMap || weaponFlatDamageMap.length == 0 ) { weaponFlatDamageMap = Object.keys(Constants.DamageType).map(() => 0); } let defenderState = state.fighterStates[state.playerToAct ? 0 : 1]; let defenderMundaneStatusEffect = this.isUnderStatusEffect( defenderState, Constants.StatusEffect.ProtectedAgainstMundaneDamage, defenderState.turnCounter ); let resistances = this.getDefenderResistance(damagePlayer); let defenderReduction = Object.keys(Constants.DamageType).map(() => 0); let resultMessage = ""; for (let i = 0; i < (damageMap || []).length; i++) { let resistance = resistances[i]; let mundaneDamage = i <= 2 && defenderMundaneStatusEffect; expect(resistance).toBeInteger(`${playerName} unknown resistance ${i}`); expect(damageMap[i]).toBeInteger(`${playerName} unknown damageMap ${i}`); let inflictedDamage = damageMap[i] + weaponFlatDamageMap[i] - resistance; inflictedDamage = mundaneDamage ? Math.floor(inflictedDamage / 2) : inflictedDamage; inflictedDamage = inflictedDamage < 0 ? 0 : inflictedDamage; defenderReduction[i] = resistance; damageState.hp -= inflictedDamage; expect(inflictedDamage).toBeInteger( `${playerName} inflicted unknown damage ${inflictedDamage}` ); if (damageMap[i] != 0 || weaponFlatDamageMap[i] != 0) { resultMessage += damageSelf ? `${playerName} takes ${mundaneDamage ? `half ` : ``}damage : ` : `${playerName} damages ${ mundaneDamage ? `half ` : `` }${opponentName} : `; resultMessage += `${inflictedDamage} (${ damageMap[i] != 0 ? damageMap[i] : "" }`; if (weaponFlatDamageMap[i] != 0 && damageMap[i] != 0) { resultMessage += `${` + ${weaponFlatDamageMap[i]}`}`; } else if (weaponFlatDamageMap[i] != 0) { resultMessage += weaponFlatDamageMap[i]; } resultMessage += ` ${Constants.DamageTypeNames[i]}${ resistance > 0 ? ` - ${resistance} Resist` : "" })\n`; } damageMap[i] = inflictedDamage; } return { rolls: rolls, outcome: outcome, damageOutput: [ { damage: damageMap, defenderHp: damageSelf ? state.fighterStates[state.playerToAct ? 0 : 1].hp : Math.max(damageState.hp, 0), defenderReduction: defenderReduction, }, ], attackerStatusEffectsIncrement: Object.keys(Constants.StatusEffect).map( () => 0 ), hpIncrement: 0, attackerHp: damageSelf ? damageState.hp : state.fighterStates[state.playerToAct].hp, preResultDescription: preResultDescription, resultMessage: resultMessage, }; } static directDamage( battle, state, dice, diceCount, diceFaces, defenderState, defenderPlayer, damageType, skill, log ) { let rolls = []; let nativeRoll = diceFaces > 0 ? dice.roll(diceCount, diceFaces) : 0; rolls.push({ type: 5, ability: Constants.Ability.Strength, size: diceCount * diceFaces, value: nativeRoll, }); const damageMap = Object.keys(Constants.DamageType).map(() => 0); damageMap[damageType] = nativeRoll; return this.makeDamageActionLog( battle, state, defenderState, defenderPlayer, damageMap, rolls, Constants.Outcome.Success, skill, log, "" ); } static getDefenderMaxAbility(battle, state) { var abilities = battle.battlePlayers[state.playerToAct ? 0 : 1].fighter.abilityScores; var max = Math.max(...abilities); for (let i = 0; i < 3; ++i) { if (abilities[i] === max) { return i; } } return 2; } }