diff --git a/data/mods/gen1/moves.ts b/data/mods/gen1/moves.ts index 9bb9510f3f..36d4aa1543 100644 --- a/data/mods/gen1/moves.ts +++ b/data/mods/gen1/moves.ts @@ -821,9 +821,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } // Drain/recoil/secondary effect confusion do not happen if the substitute breaks if (target.volatiles['substitute']) { - if (move.recoil) { - this.damage(this.clampIntRange(Math.floor(uncappedDamage * move.recoil[0] / move.recoil[1]), 1), - source, target, 'recoil'); + if (uncappedDamage) { + this.actions.calcRecoilDamage(uncappedDamage, move, source); } if (move.drain) { const amount = this.clampIntRange(Math.floor(uncappedDamage * move.drain[0] / move.drain[1]), 1); diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index 83812bfa95..6a4d26aef8 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -588,6 +588,7 @@ export const Scripts: ModdedBattleScriptsData = { } if ((damage || damage === 0) && !target.fainted) { damage = this.battle.damage(damage, target, pokemon, move); + if (damage) this.calcRecoilDamage(damage, move, pokemon); if (!(damage || damage === 0)) return false; didSomething = true; } else if (damage === false && typeof hitResult === 'undefined') { diff --git a/data/mods/gen1jpn/moves.ts b/data/mods/gen1jpn/moves.ts index 75dbc55f6e..5b052b0ffb 100644 --- a/data/mods/gen1jpn/moves.ts +++ b/data/mods/gen1jpn/moves.ts @@ -47,8 +47,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } // Drain/recoil does not happen if the substitute breaks if (target.volatiles['substitute']) { - if (move.recoil) { - this.damage(Math.round(uncappedDamage * move.recoil[0] / move.recoil[1]), source, target, 'recoil'); + if (uncappedDamage) { + this.actions.calcRecoilDamage(uncappedDamage, move, source); } if (move.drain) { this.heal(Math.ceil(uncappedDamage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/mods/gen1stadium/scripts.ts b/data/mods/gen1stadium/scripts.ts index fb6380a046..06d987f47c 100644 --- a/data/mods/gen1stadium/scripts.ts +++ b/data/mods/gen1stadium/scripts.ts @@ -433,6 +433,7 @@ export const Scripts: ModdedBattleScriptsData = { } if ((damage || damage === 0) && !target.fainted) { damage = this.battle.damage(damage, target, pokemon, move); + if (damage && target.hp > 0) this.calcRecoilDamage(damage, move, pokemon); if (!(damage || damage === 0)) return false; didSomething = true; } else if (damage === false && typeof hitResult === 'undefined') { diff --git a/data/mods/gen2/scripts.ts b/data/mods/gen2/scripts.ts index 5bb79245ba..5ef71bd5c7 100644 --- a/data/mods/gen2/scripts.ts +++ b/data/mods/gen2/scripts.ts @@ -295,8 +295,8 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); - if (move.recoil && move.totalDamage) { - this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, target, 'recoil'); + if (move.totalDamage) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } return damage; }, diff --git a/data/mods/gen2stadium2/scripts.ts b/data/mods/gen2stadium2/scripts.ts index 3487c31df4..a21b471361 100644 --- a/data/mods/gen2stadium2/scripts.ts +++ b/data/mods/gen2stadium2/scripts.ts @@ -233,8 +233,8 @@ export const Scripts: ModdedBattleScriptsData = { // Implementing Recoil mechanics from Stadium 2. // If a pokemon caused the other to faint with a recoil move and only one pokemon remains on both sides, // recoil damage will not be taken. - if (move.recoil && move.totalDamage && (pokemon.side.pokemonLeft > 1 || target.side.pokemonLeft > 1 || target.hp)) { - this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, target, 'recoil'); + if (move.totalDamage && (pokemon.side.pokemonLeft > 1 || target.side.pokemonLeft > 1 || target.hp)) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } return damage; }, diff --git a/data/mods/gen3/scripts.ts b/data/mods/gen3/scripts.ts index 75d9dd50ee..bc316d91ae 100644 --- a/data/mods/gen3/scripts.ts +++ b/data/mods/gen3/scripts.ts @@ -188,7 +188,6 @@ export const Scripts: ModdedBattleScriptsData = { if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || !this.battle.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; return false; } @@ -457,8 +456,8 @@ export const Scripts: ModdedBattleScriptsData = { move.totalDamage = damage; } - if (move.recoil && move.totalDamage) { - this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, target, 'recoil'); + if (move.totalDamage) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } if (target && pokemon !== target) target.gotAttacked(move, damage, pokemon); @@ -476,9 +475,5 @@ export const Scripts: ModdedBattleScriptsData = { return damage; }, - - calcRecoilDamage(damageDealt, move) { - return this.battle.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); - }, }, }; diff --git a/data/mods/gen4/moves.ts b/data/mods/gen4/moves.ts index 795aa6e7c8..f87e20dfc7 100644 --- a/data/mods/gen4/moves.ts +++ b/data/mods/gen4/moves.ts @@ -1307,8 +1307,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-activate', target, 'Substitute', '[damage]'); } if (move.ohko) this.add('-ohko'); - if (move.recoil && damage) { - this.damage(this.actions.calcRecoilDamage(damage, move, source), source, target, 'recoil'); + if (damage) { + this.actions.calcRecoilDamage(damage, move, source); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/mods/gen4/scripts.ts b/data/mods/gen4/scripts.ts index c28929177d..0510d34a9f 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -199,8 +199,19 @@ export const Scripts: ModdedBattleScriptsData = { } return hitResults; }, - calcRecoilDamage(damageDealt, move) { - return this.battle.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); + calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number | null { + let recoilDamage = 0; + if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.floor(pokemon.baseMaxhp / 4), 1); + else if (move.recoil) { + recoilDamage = this.battle.clampIntRange(Math.floor(damageDealt * move.recoil[0] / move.recoil[1]), 1); + } else return null; + + if (move.struggleRecoil) { + this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); + } else { + this.battle.damage(recoilDamage, pokemon, pokemon, 'recoil'); + } + return recoilDamage; }, }, }; diff --git a/data/mods/gen5/moves.ts b/data/mods/gen5/moves.ts index a5c93cd67a..15bac9b8fe 100644 --- a/data/mods/gen5/moves.ts +++ b/data/mods/gen5/moves.ts @@ -872,8 +872,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { } else { this.add('-activate', target, 'Substitute', '[damage]'); } - if (move.recoil && damage) { - this.damage(this.actions.calcRecoilDamage(damage, move, source), source, target, 'recoil'); + if (damage) { + this.actions.calcRecoilDamage(damage, move, source); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts index e3b535939d..a5816fdb06 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -1314,7 +1314,6 @@ export const Scripts: ModdedBattleScriptsData = { if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || !this.battle.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; return false; } @@ -1476,14 +1475,6 @@ export const Scripts: ModdedBattleScriptsData = { // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). move.totalDamage += damage[i]; } - if (move.mindBlownRecoil) { - const hpBeforeRecoil = pokemon.hp; - this.battle.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get(move.id), true); - move.mindBlownRecoil = false; - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } this.battle.eachEvent('Update'); if (!pokemon.hp && targets.length === 1) { hit++; // report the correct number of hits for multihit moves @@ -1498,26 +1489,8 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.add('-hitcount', targets[0], hit - 1); } - if ((move.recoil || move.id === 'chloroblast') && move.totalDamage) { - const hpBeforeRecoil = pokemon.hp; - this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, pokemon, 'recoil'); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } - - if (move.struggleRecoil) { - const hpBeforeRecoil = pokemon.hp; - let recoilDamage; - if (this.dex.gen >= 5) { - recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - } else { - recoilDamage = this.battle.clampIntRange(this.battle.trunc(pokemon.maxhp / 4), 1); - } - this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } + if (move.totalDamage) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway diff --git a/data/mods/passiveaggressive/moves.ts b/data/mods/passiveaggressive/moves.ts index aaab74a98e..8953f6f9ec 100644 --- a/data/mods/passiveaggressive/moves.ts +++ b/data/mods/passiveaggressive/moves.ts @@ -140,15 +140,10 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, mindblown: { inherit: true, - onAfterMove(pokemon, target, move) { - if (move.mindBlownRecoil && !move.multihit) { - const hpBeforeRecoil = pokemon.hp; - const calc = calculate(this, pokemon, pokemon, 'mindblown'); - this.damage(Math.round(calc * pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get('Mind Blown'), true); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } + onMoveFail(target, source, move) { + if (move.multihit) return; + const calc = calculate(this, source, source, 'mindblown'); + this.damage(Math.round(calc * source.maxhp / 2), source, source, this.dex.conditions.get('Mind Blown')); }, }, nightmare: { @@ -244,15 +239,10 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, steelbeam: { inherit: true, - onAfterMove(pokemon, target, move) { - if (move.mindBlownRecoil && !move.multihit) { - const hpBeforeRecoil = pokemon.hp; - const calc = calculate(this, pokemon, pokemon, 'steelbeam'); - this.damage(Math.round(calc * pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get('Steel Beam'), true); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } + onMoveFail(target, source, move) { + if (move.multihit) return; + const calc = calculate(this, source, source, 'steelbeam'); + this.damage(Math.round(calc * source.maxhp / 2), source, source, this.dex.conditions.get('Mind Blown')); }, }, supercellslam: { diff --git a/data/mods/passiveaggressive/scripts.ts b/data/mods/passiveaggressive/scripts.ts index adcf373ae6..49093671c0 100644 --- a/data/mods/passiveaggressive/scripts.ts +++ b/data/mods/passiveaggressive/scripts.ts @@ -110,15 +110,6 @@ export const Scripts: ModdedBattleScriptsData = { // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). move.totalDamage += damage[i]; } - if (move.mindBlownRecoil) { - const hpBeforeRecoil = pokemon.hp; - const calc = calculate(this.battle, pokemon, pokemon, move.id); - this.battle.damage(Math.round(calc * pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get(move.id), true); - move.mindBlownRecoil = false; - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } this.battle.eachEvent('Update'); if (!pokemon.hp && targets.length === 1) { hit++; // report the correct number of hits for multihit moves @@ -133,27 +124,8 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.add('-hitcount', targets[0], hit - 1); } - if ((move.recoil || move.id === 'chloroblast') && move.totalDamage) { - const hpBeforeRecoil = pokemon.hp; - const recoilDamage = this.calcRecoilDamage(move.totalDamage, move, pokemon); - if (recoilDamage !== 1.1) this.battle.damage(recoilDamage, pokemon, pokemon, 'recoil'); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } - - if (move.struggleRecoil) { - const hpBeforeRecoil = pokemon.hp; - let recoilDamage; - if (this.dex.gen >= 5) { - recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - } else { - recoilDamage = this.battle.clampIntRange(this.battle.trunc(pokemon.maxhp / 4), 1); - } - this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } + if (move.totalDamage) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway @@ -194,12 +166,28 @@ export const Scripts: ModdedBattleScriptsData = { return damage; }, - calcRecoilDamage(damageDealt, move, pokemon): number { + calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number | null { const calc = calculate(this.battle, pokemon, pokemon, move.id); - if (calc === 0) return 1.1; - if (move.id === 'chloroblast') return Math.round(calc * pokemon.maxhp / 2); - const recoil = Math.round(damageDealt * calc * move.recoil![0] / move.recoil![1]); - return this.battle.clampIntRange(recoil, 1); + if (!calc) return 0; + + let recoilDamage = 0; + if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); + else if (move.mindBlownRecoil) recoilDamage = Math.round(calc * pokemon.maxhp / 2); + else if (move.recoil) { + recoilDamage = this.battle.clampIntRange(Math.round(damageDealt * calc * move.recoil[0] / move.recoil[1]), 1); + } else return null; + + const hpBeforeRecoil = pokemon.hp; + if (move.struggleRecoil) { + this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); + } else { + this.battle.damage(recoilDamage, pokemon, pokemon, 'recoil'); + } + if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } + + return recoilDamage; }, }, }; diff --git a/data/moves.ts b/data/moves.ts index 67fc257f2d..a796139efc 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -939,7 +939,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { flags: { contact: 1, protect: 1, mirror: 1, metronome: 1 }, hasCrashDamage: true, onMoveFail(target, source, move) { - this.damage(source.baseMaxhp / 2, source, source, this.dex.conditions.get('High Jump Kick')); + this.damage(source.baseMaxhp / 2, source, source, this.dex.conditions.get('Axe Kick')); }, secondary: { chance: 30, @@ -2443,7 +2443,8 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { pp: 5, priority: 0, flags: { protect: 1, mirror: 1, metronome: 1 }, - // Recoil implemented in battle-actions.ts + mindBlownRecoil: true, + // Contrary to Mind Blown, Chloroblast does not implement the MoveFail event target: "normal", type: "Grass", }, @@ -11885,14 +11886,9 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { priority: 0, flags: { protect: 1, mirror: 1 }, mindBlownRecoil: true, - onAfterMove(pokemon, target, move) { - if (move.mindBlownRecoil && !move.multihit) { - const hpBeforeRecoil = pokemon.hp; - this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get('Mind Blown'), true); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } + onMoveFail(target, source, move) { + if (move.multihit) return; + this.damage(Math.round(source.maxhp / 2), source, source, this.dex.conditions.get('Mind Blown')); }, target: "allAdjacent", type: "Fire", @@ -17888,14 +17884,9 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { priority: 0, flags: { protect: 1, mirror: 1 }, mindBlownRecoil: true, - onAfterMove(pokemon, target, move) { - if (move.mindBlownRecoil && !move.multihit) { - const hpBeforeRecoil = pokemon.hp; - this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get('Steel Beam'), true); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } + onMoveFail(target, source, move) { + if (move.multihit) return; + this.damage(Math.round(source.maxhp / 2), source, source, this.dex.conditions.get('Steel Beam')); }, target: "normal", type: "Steel", @@ -18369,8 +18360,8 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { } else { this.add('-activate', target, 'move: Substitute', '[damage]'); } - if (move.recoil || move.id === 'chloroblast') { - this.damage(this.actions.calcRecoilDamage(damage, move, source), source, target, 'recoil'); + if (damage) { + this.actions.calcRecoilDamage(damage, move, source); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 66fd617e6d..c40b93c135 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -488,7 +488,6 @@ export class BattleActions { tryMoveResult = this.battle.runEvent('TryMove', pokemon, target, move); } if (!tryMoveResult) { - move.mindBlownRecoil = false; return tryMoveResult; } @@ -524,7 +523,13 @@ export class BattleActions { } if (!moveResult) { + const originalHp = pokemon.hp; this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + if (pokemon && pokemon !== target && move.category !== 'Status') { + if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } + } return false; } @@ -959,14 +964,6 @@ export class BattleActions { // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). move.totalDamage += damage[i]; } - if (move.mindBlownRecoil) { - const hpBeforeRecoil = pokemon.hp; - this.battle.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.conditions.get(move.id), true); - move.mindBlownRecoil = false; - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } this.battle.eachEvent('Update'); if (!pokemon.hp && targets.length === 1) { hit++; // report the correct number of hits for multihit moves @@ -981,26 +978,8 @@ export class BattleActions { this.battle.add('-hitcount', targets[0], hit - 1); } - if ((move.recoil || move.id === 'chloroblast') && move.totalDamage) { - const hpBeforeRecoil = pokemon.hp; - this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, pokemon, 'recoil'); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } - } - - if (move.struggleRecoil) { - const hpBeforeRecoil = pokemon.hp; - let recoilDamage; - if (this.dex.gen >= 5) { - recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - } else { - recoilDamage = this.battle.clampIntRange(this.battle.trunc(pokemon.maxhp / 4), 1); - } - this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); - if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { - this.battle.runEvent('EmergencyExit', pokemon, pokemon); - } + if (move.totalDamage) { + this.calcRecoilDamage(move.totalDamage, move, pokemon); } // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway @@ -1397,9 +1376,25 @@ export class BattleActions { return retVal === true ? undefined : retVal; } - calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number { - if (move.id === 'chloroblast') return Math.round(pokemon.maxhp / 2); - return this.battle.clampIntRange(Math.round(damageDealt * move.recoil![0] / move.recoil![1]), 1); + calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number | null { + let recoilDamage = null; + if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); + else if (move.mindBlownRecoil) recoilDamage = Math.round(pokemon.maxhp / 2); + else if (move.recoil) { + recoilDamage = this.battle.clampIntRange(Math.round(damageDealt * move.recoil[0] / move.recoil[1]), 1); + } else return null; + + const hpBeforeRecoil = pokemon.hp; + if (move.struggleRecoil) { + this.battle.directDamage(recoilDamage, pokemon, pokemon, { id: 'strugglerecoil' } as Condition); + } else { + this.battle.damage(recoilDamage, pokemon, pokemon, 'recoil'); + } + if (pokemon.hp <= pokemon.maxhp / 2 && hpBeforeRecoil > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } + + return recoilDamage; } getZMove(move: Move, pokemon: Pokemon, skipChecks?: boolean): string | undefined { diff --git a/sim/battle.ts b/sim/battle.ts index 7a4d67aa65..354ec3446e 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -2109,7 +2109,7 @@ export class Battle { } if (targetDamage !== 0) targetDamage = this.clampIntRange(targetDamage, 1); - if (effect.id !== 'struggle-recoil') { // Struggle recoil is not affected by effects + if (effect.id !== 'strugglerecoil') { // Struggle recoil is not affected by effects if (effect.effectType === 'Weather' && !target.runStatusImmunity(effect.id)) { this.debug('weather immunity'); retVals[i] = 0; @@ -2158,12 +2158,6 @@ export class Battle { } if (targetDamage && effect.effectType === 'Move') { - if (this.gen <= 1 && effect.recoil && source) { - if (this.dex.currentMod !== 'gen1stadium' || target.hp > 0) { - const amount = this.clampIntRange(Math.floor(targetDamage * effect.recoil[0] / effect.recoil[1]), 1); - this.damage(amount, source, target, 'recoil'); - } - } if (this.gen <= 4 && effect.drain && source) { const amount = this.clampIntRange(Math.floor(targetDamage * effect.drain[0] / effect.drain[1]), 1); // Draining can be countered in gen 1 diff --git a/sim/global-types.ts b/sim/global-types.ts index 9fdbe08149..057d9553e7 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -154,7 +154,7 @@ interface BattleScriptsData { interface ModdedBattleActions { inherit?: true; afterMoveSecondaryEvent?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; - calcRecoilDamage?: (this: BattleActions, damageDealt: number, move: Move, pokemon: Pokemon) => number; + calcRecoilDamage?: (this: BattleActions, damageDealt: number, move: Move, pokemon: Pokemon) => number | null; canMegaEvo?: (this: BattleActions, pokemon: Pokemon) => string | undefined | null; canMegaEvoX?: (this: BattleActions, pokemon: Pokemon) => string | undefined | null; canMegaEvoY?: (this: BattleActions, pokemon: Pokemon) => string | undefined | null; diff --git a/test/sim/abilities/emergencyexit.js b/test/sim/abilities/emergencyexit.js index f5d035628a..fd6beff474 100644 --- a/test/sim/abilities/emergencyexit.js +++ b/test/sim/abilities/emergencyexit.js @@ -401,6 +401,32 @@ describe(`Emergency Exit`, () => { assert.equal(battle.requestState, 'switch'); }); + it(`should request a switchout after crash damage`, () => { + battle = common.createBattle([[ + { species: 'Golisopod', ability: 'Emergency Exit', moves: ['highjumpkick'], evs: { hp: 4 } }, + { species: 'Wynaut', moves: ['sleeptalk'] }, + ], [ + { species: 'Chansey', moves: ['protect'] }, + ]]); + const eePokemon = battle.p1.active[0]; + battle.makeChoices(); + assert.atMost(eePokemon.hp, eePokemon.maxhp / 2); + assert.equal(battle.requestState, 'switch'); + }); + + it(`should request a switchout after Mind Blown recoil damage`, () => { + battle = common.createBattle([[ + { species: 'Golisopod', ability: 'Emergency Exit', moves: ['mindblown'], evs: { hp: 4 } }, + { species: 'Wynaut', moves: ['sleeptalk'] }, + ], [ + { species: 'Chansey', moves: ['protect'] }, + ]]); + const eePokemon = battle.p1.active[0]; + battle.makeChoices(); + assert.atMost(eePokemon.hp, eePokemon.maxhp / 2); + assert.equal(battle.requestState, 'switch'); + }); + it(`should request a switchout after taking struggle recoil damage`, () => { battle = common.createBattle([[ { species: 'Golisopod', item: 'Assault Vest', ability: 'Emergency Exit', moves: ['protect'] }, diff --git a/test/sim/moves/mindblown.js b/test/sim/moves/mindblown.js index 16699ed6ac..83bcd5ca97 100644 --- a/test/sim/moves/mindblown.js +++ b/test/sim/moves/mindblown.js @@ -27,4 +27,15 @@ describe('Mind Blown', () => { ]]); assert.hurtsBy(battle.p1.active[0], Math.ceil(battle.p1.active[0].maxhp / 2), () => battle.makeChoices()); }); + + it('should not deal damage to the user if there is no target', () => { + battle = common.createBattle([[ + { species: 'Dugtrio', ability: 'sandveil', moves: ['memento'] }, + { species: 'Dugtrio', ability: 'sandveil', moves: ['memento'] }, + ], [ + { species: 'Blacephalon', ability: 'limber', moves: ['mindblown'] }, + ]]); + + assert.false.hurts(battle.p2.active[0], () => battle.makeChoices()); + }); });