"use strict"; exports.BattleScripts = { gen: 6, side: { pnames: { atk: 'Attack boost', minusatk: 'Attack debuff', def: 'Defense boost', minusdef: 'Defense debuff', spa: 'Special Attack boost', minusspa: 'Special Attack debuff', spd: 'Special Defense boost', minusspd: 'Special Defense debuff', spe: 'Speed boost', minusspe: 'Speed debuff', brn: 'Burn', par: 'Paralyze', psn: 'Poison', tox: 'Badly poisoned', slp: 'Sleep status', frz: 'Frozen', flinch: 'Flinch', confusion: 'Confusion', }, }, // Edit getDamage so there is no crits and no random damage. getDamage: function (pokemon, target, move, suppressMessages) { if (typeof move === 'string') { move = this.getMove(move); } if (typeof move === 'number') { move = { basePower: move, type: '???', category: 'Physical', }; } if (move.affectedByImmunities) { if (!target.runImmunity(move.type, true)) { return false; } } if (move.damageCallback) { return move.damageCallback.call(this, pokemon, target); } if (move.damage === 'level') { return pokemon.level; } if (move.damage) { return move.damage; } if (!move) { move = {}; } if (!move.type) move.type = '???'; let type = move.type; // '???' is typeless damage: used for Struggle and Confusion etc let category = this.getCategory(move); let defensiveCategory = move.defensiveCategory || category; let accuracy = move.accuracy; if (accuracy === true) { accuracy = 100; } let basePower = move.basePower * accuracy / 100; if (move.basePowerCallback) { basePower = move.basePowerCallback.call(this, pokemon, target, move); } if (!basePower) { if (basePower === 0) return; // returning undefined means not dealing damage return basePower; } if (move.critRatio === 2) basePower *= 1.125; if (move.critRatio === 3) basePower *= 1.25; basePower = this.clampIntRange(basePower, 1); basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); if (!basePower) return 0; basePower = this.clampIntRange(basePower, 1); let level = pokemon.level; let attacker = pokemon; let defender = target; let attackStat = category === 'Physical' ? 'atk' : 'spa'; let defenseStat = defensiveCategory === 'Physical' ? 'def' : 'spd'; let statTable = {atk:'Atk', def:'Def', spa:'SpA', spd:'SpD', spe:'Spe'}; let attack; let defense; let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; let defBoosts = move.useSourceDefensive ? attacker.boosts[defenseStat] : defender.boosts[defenseStat]; let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; let ignorePositiveDefensive = !!move.ignorePositiveDefensive; let ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); let ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); if (ignoreOffensive) { this.debug('Negating (sp)atk boost/penalty.'); atkBoosts = 0; } if (ignoreDefensive) { this.debug('Negating (sp)def boost/penalty.'); defBoosts = 0; } if (move.useTargetOffensive) { attack = defender.calculateStat(attackStat, atkBoosts); } else { attack = attacker.calculateStat(attackStat, atkBoosts); } if (move.useSourceDefensive) { defense = attacker.calculateStat(defenseStat, defBoosts); } else { defense = defender.calculateStat(defenseStat, defBoosts); } // Apply Stat Modifiers attack = this.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); defense = this.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); //int(int(int(2 * L / 5 + 2) * A * P / D) / 50); let baseDamage = Math.floor(Math.floor(Math.floor(2 * level / 5 + 2) * basePower * attack / defense) / 50) + 2; // multi-target modifier (doubles only) if (move.spreadHit) { let spreadModifier = move.spreadModifier || 0.75; this.debug('Spread modifier: ' + spreadModifier); baseDamage = this.modify(baseDamage, spreadModifier); } // weather modifier (TODO: relocate here) // STAB if (move.hasSTAB || type !== '???' && pokemon.hasType(type)) { // The "???" type never gets STAB // Not even if you Roost in Gen 4 and somehow manage to use // Struggle in the same turn. // (On second thought, it might be easier to get a Missingno.) baseDamage = this.modify(baseDamage, move.stab || 1.5); } // types let totalTypeMod = 0; if (target.negateImmunity && target.negateImmunity[move.type] !== 'IgnoreEffectiveness' || this.getImmunity(move.type, target)) { totalTypeMod = target.runEffectiveness(move); } totalTypeMod = this.clampIntRange(totalTypeMod, -6, 6); if (totalTypeMod > 0) { if (!suppressMessages) this.add('-supereffective', target); for (let i = 0; i < totalTypeMod; i++) { baseDamage *= 2; } } if (totalTypeMod < 0) { if (!suppressMessages) this.add('-resisted', target); for (let i = 0; i > totalTypeMod; i--) { baseDamage = Math.floor(baseDamage / 2); } } if (basePower && !Math.floor(baseDamage)) { return 1; } // Final modifier. Modifiers that modify damage after min damage check, such as Life Orb. baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage); return Math.floor(baseDamage); }, tryMoveHit: function (target, pokemon, move, spreadHit) { if (move.selfdestruct && spreadHit) pokemon.hp = 0; this.setActiveMove(move, pokemon, target); let hitResult = true; hitResult = this.singleEvent('PrepareHit', move, {}, target, pokemon, move); if (!hitResult) { if (hitResult === false) this.add('-fail', target); return false; } this.runEvent('PrepareHit', pokemon, target, move); if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { if (move.target === 'all') { hitResult = this.runEvent('TryHitField', target, pokemon, move); } else { hitResult = this.runEvent('TryHitSide', target, pokemon, move); } if (!hitResult) { if (hitResult === false) this.add('-fail', target); return true; } return this.moveHit(target, pokemon, move); } if ((move.affectedByImmunities && !target.runImmunity(move.type, true)) || (move.isSoundBased && (pokemon !== target || this.gen <= 4) && !target.runImmunity('sound', true))) { return false; } if (typeof move.affectedByImmunities === 'undefined') { move.affectedByImmunities = (move.category !== 'Status'); } hitResult = this.runEvent('TryHit', target, pokemon, move); if (!hitResult) { if (hitResult === false) this.add('-fail', target); return false; } let totalDamage = 0; let damage = 0; pokemon.lastDamage = 0; if (move.multihit) { let hits = move.multihit; if (hits.length) { hits = 3; } hits = Math.floor(hits); let nullDamage = true; let moveDamage; // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. let isSleepUsable = move.sleepUsable || this.getMove(move.sourceEffect).sleepUsable; let i; for (i = 0; i < hits && target.hp && pokemon.hp; i++) { if (pokemon.status === 'slp' && !isSleepUsable) break; moveDamage = this.moveHit(target, pokemon, move); if (moveDamage === false) break; if (nullDamage && (moveDamage || moveDamage === 0)) nullDamage = false; // Damage from each hit is individually counted for the // purposes of Counter, Metal Burst, and Mirror Coat. damage = (moveDamage || 0); // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). totalDamage += damage; this.eachEvent('Update'); } if (i === 0) return true; if (nullDamage) damage = false; this.add('-hitcount', target, i); } else { damage = this.moveHit(target, pokemon, move); totalDamage = damage; } if (move.recoil) { this.damage(this.clampIntRange(Math.round(totalDamage * move.recoil[0] / move.recoil[1]), 1), pokemon, target, 'recoil'); } if (target && move.category !== 'Status') target.gotAttacked(move, damage, pokemon); if (!damage && damage !== 0) return damage; if (target && !move.negateSecondary) { this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); this.runEvent('AfterMoveSecondary', target, pokemon, move); } return damage; }, moveHit: function (target, pokemon, move, moveData, isSecondary, isSelf) { let damage; move = this.getMoveCopy(move); if (!moveData) moveData = move; let hitResult = true; if (move.target === 'all' && !isSelf) { hitResult = this.singleEvent('TryHitField', moveData, {}, target, pokemon, move); } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { hitResult = this.singleEvent('TryHitSide', moveData, {}, target.side, pokemon, move); } else if (target) { hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move); } if (!hitResult) { if (hitResult === false) this.add('-fail', target); return false; } if (target && !isSecondary && !isSelf) { hitResult = this.runEvent('TryPrimaryHit', target, pokemon, moveData); if (hitResult === 0) { // special Substitute flag hitResult = true; target = null; } } if (target && isSecondary && !moveData.self) { hitResult = true; } if (!hitResult) { return false; } if (target) { let didSomething = false; damage = this.getDamage(pokemon, target, moveData); if ((damage || damage === 0) && !target.fainted) { if (move.noFaint && damage >= target.hp) { damage = target.hp - 1; } damage = this.damage(damage, target, pokemon, move); if (!(damage || damage === 0)) { this.debug('damage interrupted'); return false; } didSomething = true; } if (damage === false || damage === null) { if (damage === false) { this.add('-fail', target); } this.debug('damage calculation interrupted'); return false; } if (moveData.boosts && !target.fainted) { hitResult = this.boost(moveData.boosts, target, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.heal && !target.fainted) { let d = target.heal(Math.round(target.maxhp * moveData.heal[0] / moveData.heal[1])); if (!d && d !== 0) { this.add('-fail', target); this.debug('heal interrupted'); return false; } this.add('-heal', target, target.getHealth); didSomething = true; } if (moveData.status) { if (!target.status) { hitResult = target.setStatus(moveData.status, pokemon, move); didSomething = didSomething || hitResult; } else if (!isSecondary) { if (target.status === moveData.status) { this.add('-fail', target, target.status); } else { this.add('-fail', target); } return false; } } if (moveData.forceStatus) { hitResult = target.setStatus(moveData.forceStatus, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.volatileStatus) { hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.sideCondition) { hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.weather) { hitResult = this.setWeather(moveData.weather, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.terrain) { hitResult = this.setTerrain(moveData.terrain, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.pseudoWeather) { hitResult = this.addPseudoWeather(moveData.pseudoWeather, pokemon, move); didSomething = didSomething || hitResult; } if (moveData.forceSwitch) { if (this.canSwitch(target.side)) didSomething = true; // at least defer the fail message to later } if (moveData.selfSwitch) { if (this.canSwitch(pokemon.side)) didSomething = true; // at least defer the fail message to later } // Hit events // These are like the TryHit events, except we don't need a FieldHit event. // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) hitResult = null; if (move.target === 'all' && !isSelf) { if (moveData.onHitField) hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move); } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { if (moveData.onHitSide) hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); } else { if (moveData.onHit) hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); if (!isSelf && !isSecondary) { this.runEvent('Hit', target, pokemon, move); } if (moveData.onAfterHit) hitResult = this.singleEvent('AfterHit', moveData, {}, target, pokemon, move); } if (!hitResult && !didSomething && !moveData.self && !moveData.selfdestruct) { if (!isSelf && !isSecondary) { if (hitResult === false || didSomething === false) this.add('-fail', target); } this.debug('move failed because it did nothing'); return false; } } if (moveData.self) { let selfRoll; if (!isSecondary && moveData.self.boosts) selfRoll = this.random(100); // This is done solely to mimic in-game RNG behaviour. All self drops have a 100% chance of happening but still grab a random number. if (typeof moveData.self.chance === 'undefined' || selfRoll < moveData.self.chance) { this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); } } if (moveData.secondaries && this.runEvent('TrySecondaryHit', target, pokemon, moveData)) { let messages = []; // We gather the effects to apply them. for (let i = 0; i < moveData.secondaries.length; i++) { let buffDebuff = 'none'; let accuracy = moveData.accuracy; if (accuracy === true) { accuracy = 100; } let points = Math.floor(moveData.secondaries[i].chance * accuracy / 100); let boosts = false; let status = false; let volatileStatus = false; let secTarget = false; let isSecSelf = false; if (moveData.secondaries[i].self) { boosts = moveData.secondaries[i].self.boosts; status = moveData.secondaries[i].self.status; volatileStatus = moveData.secondaries[i].self.volatileStatus; secTarget = pokemon; isSecSelf = true; } else if (target) { boosts = moveData.secondaries[i].boosts; status = moveData.secondaries[i].status; volatileStatus = moveData.secondaries[i].volatileStatus; secTarget = target; } // If boosts, go through all of them. if (boosts) { for (let b in boosts) { buffDebuff = (boosts[b] > 0) ? b : 'minus' + b; messages.push([points, buffDebuff, secTarget, isSecSelf, 'boosts', b, boosts[b]]); } } else if (status) { messages.push([points, status, secTarget, isSecSelf, 'status']); } else if (volatileStatus) { messages.push([points, volatileStatus, secTarget, isSecSelf, 'volatileStatus']); } } // After having gathered the effects, add points and trigger them. for (let i = 0; i < messages.length; i++) { let pointsBuff = messages[i][0]; let buffing = messages[i][1]; let secTarget = messages[i][2]; //let isSecSelf = messages[i][3]; let actualWhat = messages[i][4]; if (!!buffing && !!pointsBuff && !!secTarget) { if (!secTarget.side.points) secTarget.side.points = {}; if (!secTarget.side.points[buffing] && secTarget.side.points[buffing] !== 0) secTarget.side.points[buffing] = 50; if (!secTarget.fainted && secTarget.hp > 0) { if (actualWhat !== 'status' || !secTarget.status) { secTarget.side.points[buffing] += pointsBuff; this.add('-message', secTarget.side.name + ' acquired ' + pointsBuff + ' points in ' + secTarget.side.pnames[buffing] + ' [Total: ' + secTarget.side.points[buffing] + ']!'); } if (secTarget.side.points[buffing] >= 100 && secTarget.hp > 0) { secTarget.side.points[buffing] -= 100; this.add('-message', 'A secondary effect on ' + secTarget.side.pnames[buffing] + ' triggered! [-100 points]'); // Actually trigger here the secondaries to avoid recursion. if (actualWhat === 'boosts') { let boosting = {}; boosting[messages[i][5]] = messages[i][6]; this.boost(boosting, secTarget, pokemon, move); } if (actualWhat === 'status') { if (!!!secTarget.status) { secTarget.trySetStatus(buffing, pokemon, move); } secTarget.removeVolatile(move.id); } if (actualWhat === 'volatileStatus') { secTarget.addVolatile(buffing, pokemon, move); } } } } } } if (target && target.hp > 0 && pokemon.hp > 0 && moveData.forceSwitch && this.canSwitch(target.side)) { hitResult = this.runEvent('DragOut', target, pokemon, move); if (hitResult) { target.forceSwitchFlag = true; } else if (hitResult === false) { this.add('-fail', target); } } if (move.selfSwitch && pokemon.hp) { pokemon.switchFlag = move.selfSwitch; } return damage; }, comparePriority: function (a, b) { a.priority = a.priority || 0; a.subPriority = a.subPriority || 0; a.speed = a.speed || 0; a.hp = a.hp || 0; a.weight = a.weight || 0; a.height = a.height || 0; a.pokemonLeft = a.pokemonLeft || 0; a.totalSpeed = a.totalSpeed || 0; b.priority = b.priority || 0; b.subPriority = b.subPriority || 0; b.speed = b.speed || 0; b.hp = b.hp || 0; b.weight = b.weight || 0; b.height = b.height || 0; b.pokemonLeft = b.pokemonLeft || 0; b.totalSpeed = b.totalSpeed || 0; if ((typeof a.order === 'number' || typeof b.order === 'number') && a.order !== b.order) { if (typeof a.order !== 'number') { return -1; } if (typeof b.order !== 'number') { return 1; } if (b.order - a.order) { return -(b.order - a.order); } } if (b.priority - a.priority) { return b.priority - a.priority; } if (b.speed - a.speed) { return b.speed - a.speed; } if (b.subOrder - a.subOrder) { return -(b.subOrder - a.subOrder); } if (b.hp - a.hp) { return b.hp - a.hp; } if (b.speed - a.speed) { return b.speed - a.speed; } if (b.weight - a.weight) { return -(b.weight - a.weight); } if (b.height - a.height) { return -(b.height - a.height); } if (b.totalSpeed - a.totalSpeed) { return b.totalSpeed - a.totalSpeed; } return Math.random() - 0.5; }, addQueue: function (decision, noSort, side) { if (decision) { if (Array.isArray(decision)) { for (let i = 0; i < decision.length; i++) { this.addQueue(decision[i], noSort); } return; } if (!decision.side && side) decision.side = side; if (!decision.side && decision.pokemon) decision.side = decision.pokemon.side; if (!decision.choice && decision.move) decision.choice = 'move'; if (!decision.priority) { let priorities = { 'beforeTurn': 100, 'beforeTurnMove': 99, 'switch': 6, 'runSwitch': 6.1, 'megaEvo': 5.9, 'residual': -100, 'team': 102, 'start': 101, }; if (priorities[decision.choice]) { decision.priority = priorities[decision.choice]; } } if (decision.choice === 'move') { if (this.getMove(decision.move).beforeTurnCallback) { this.addQueue({choice: 'beforeTurnMove', pokemon: decision.pokemon, move: decision.move, targetLoc: decision.targetLoc}, true); } } else if (decision.choice === 'switch') { if (decision.pokemon.switchFlag && decision.pokemon.switchFlag !== true) { decision.pokemon.switchCopyFlag = decision.pokemon.switchFlag; } decision.pokemon.switchFlag = false; if (!decision.speed && decision.pokemon && decision.pokemon.isActive) decision.speed = decision.pokemon.speed; } if (decision.move) { let target; if (!decision.targetPosition) { target = this.resolveTarget(decision.pokemon, decision.move); decision.targetSide = target.side; decision.targetPosition = target.position; } decision.move = this.getMoveCopy(decision.move); if (!decision.priority) { let priority = decision.move.priority; priority = this.runEvent('ModifyPriority', decision.pokemon, target, decision.move, priority); decision.priority = priority; // In Gen 6, Quick Guard blocks moves with artificially enhanced priority. if (this.gen > 5) decision.move.priority = priority; } } if (!decision.pokemon && !decision.speed) decision.speed = 1; if (!decision.speed && decision.choice === 'switch' && decision.target) decision.speed = decision.target.speed; if (!decision.speed) decision.speed = decision.pokemon.speed; if (!decision.hp && decision.pokemon) decision.hp = decision.pokemon.hp; if (!decision.weight && decision.pokemon) decision.weight = decision.pokemon.weightkg; if (!decision.height && decision.pokemon) decision.height = decision.pokemon.height; if (!decision.pokemonLeft && decision.side) decision.pokemonLeft = decision.side.pokemonLeft; if (!decision.totalSpeed && decision.side) { decision.totalSpeed = 0; for (let i = 0; i < decision.side.pokemon.length; i++) { decision.totalSpeed += decision.side.pokemon[i].speed; } } if (decision.choice === 'switch' && !decision.side.pokemon[0].isActive) { // if there's no actives, switches happen before activations decision.priority = 6.2; } this.queue.push(decision); } if (!noSort) { this.queue.sort(this.comparePriority); } }, };