From 0ff6744496116a06f1ac6cb23842e5abc428a8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= Date: Tue, 13 Jan 2026 10:03:29 +0000 Subject: [PATCH 1/6] Approximate the implementations of High Jump Kick and Mind Blown --- data/moves.ts | 22 ++++++---------------- sim/battle-actions.ts | 8 ++++++-- test/sim/abilities/emergencyexit.js | 26 ++++++++++++++++++++++++++ test/sim/moves/mindblown.js | 11 +++++++++++ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/data/moves.ts b/data/moves.ts index 75b8e29dce..be77594f72 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -12314,14 +12314,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')); }, secondary: null, target: "allAdjacent", @@ -18570,14 +18565,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')); }, secondary: null, target: "normal", diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 7879fdd6c0..17f123abb7 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -488,7 +488,6 @@ export class BattleActions { if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || !this.battle.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; return false; } @@ -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; } @@ -961,7 +966,6 @@ export class BattleActions { 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); } diff --git a/test/sim/abilities/emergencyexit.js b/test/sim/abilities/emergencyexit.js index e7527dc776..caa5e2f824 100644 --- a/test/sim/abilities/emergencyexit.js +++ b/test/sim/abilities/emergencyexit.js @@ -381,6 +381,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..8989764bff 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()); + }); }); From 91230fcae77fe9f299bd4b3ef9d8ec5eb18dd510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= <80102738+andrebastosdias@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:12:36 +0000 Subject: [PATCH 2/6] Lint --- test/sim/moves/mindblown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sim/moves/mindblown.js b/test/sim/moves/mindblown.js index 8989764bff..83bcd5ca97 100644 --- a/test/sim/moves/mindblown.js +++ b/test/sim/moves/mindblown.js @@ -27,7 +27,7 @@ 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'] }, From 1cee245c60a9c36cad4237e103c8c1e7edbbfd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= Date: Tue, 13 Jan 2026 14:30:12 +0000 Subject: [PATCH 3/6] Normalize implementations --- data/mods/gen2/scripts.ts | 4 +- data/mods/gen2stadium2/scripts.ts | 4 +- data/mods/gen3/scripts.ts | 9 +--- data/mods/gen4/moves.ts | 4 +- data/mods/gen4/scripts.ts | 14 +++++- data/mods/gen5/moves.ts | 4 +- data/mods/gen9ssb/scripts.ts | 31 +------------ data/mods/passiveaggressive/moves.ts | 26 ++++------- data/mods/passiveaggressive/scripts.ts | 60 +++++++++++--------------- data/moves.ts | 7 +-- sim/battle-actions.ts | 49 +++++++++------------ 11 files changed, 81 insertions(+), 131 deletions(-) diff --git a/data/mods/gen2/scripts.ts b/data/mods/gen2/scripts.ts index d564f84d97..467243cc12 100644 --- a/data/mods/gen2/scripts.ts +++ b/data/mods/gen2/scripts.ts @@ -296,8 +296,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 9a9ddce00a..75663687a2 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 fd0d8b31a8..7ff4938427 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; } @@ -456,8 +455,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); @@ -475,9 +474,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 d6ad9cf3c5..88649d770a 100644 --- a/data/mods/gen4/moves.ts +++ b/data/mods/gen4/moves.ts @@ -1667,8 +1667,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 67f54d423d..9206d07610 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -182,8 +182,18 @@ 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 { + if (damageDealt === 0) return 0; + let recoilDamage = 0; + if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.floor(pokemon.baseMaxhp / 4), 1); + if (move.recoil) recoilDamage = this.battle.clampIntRange(Math.floor(damageDealt * move.recoil[0] / move.recoil[1]), 1); + + 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 ab4cc6060f..137a5294ce 100644 --- a/data/mods/gen5/moves.ts +++ b/data/mods/gen5/moves.ts @@ -921,8 +921,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 6be6a31470..80d9afb29a 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -1344,7 +1344,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; } @@ -1506,14 +1505,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 @@ -1528,26 +1519,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 78a2f7408a..2588e07296 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: { @@ -248,15 +243,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..f1a74b2878 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,30 @@ export const Scripts: ModdedBattleScriptsData = { return damage; }, - calcRecoilDamage(damageDealt, move, pokemon): number { + calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number { + if (damageDealt === 0) return 0; + const hpBeforeRecoil = pokemon.hp; + 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); + if (move.mindBlownRecoil) recoilDamage = Math.round(calc * pokemon.maxhp / 2); + if (move.recoil) { + recoilDamage = this.battle.clampIntRange(Math.round(damageDealt * calc * move.recoil[0] / move.recoil[1]), 1); + } + + 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 be77594f72..8a06cbc93b 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -2527,7 +2527,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 secondary: null, target: "normal", type: "Grass", @@ -19056,8 +19057,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 17f123abb7..5bbaf4e274 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -963,13 +963,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); - 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 @@ -984,26 +977,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 @@ -1396,8 +1371,24 @@ export class BattleActions { } 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); + if (damageDealt === 0) return 0; + const hpBeforeRecoil = pokemon.hp; + + let recoilDamage = 0; + if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); + if (move.mindBlownRecoil) recoilDamage = Math.round(pokemon.maxhp / 2); + if (move.recoil) recoilDamage = this.battle.clampIntRange(Math.round(damageDealt * move.recoil[0] / move.recoil[1]), 1); + + 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 { From fb2c1a126ffb8345b670065becbfddc5d7991efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= Date: Tue, 13 Jan 2026 16:04:29 +0000 Subject: [PATCH 4/6] Refactor Gen 1 --- data/mods/gen1/moves.ts | 5 ++--- data/mods/gen1jpn/moves.ts | 4 ++-- data/mods/gen4/scripts.ts | 6 +++--- data/mods/passiveaggressive/scripts.ts | 12 +++++------- sim/battle-actions.ts | 13 ++++++------- sim/global-types.ts | 2 +- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/data/mods/gen1/moves.ts b/data/mods/gen1/moves.ts index ba21a2506c..8b12ba04a1 100644 --- a/data/mods/gen1/moves.ts +++ b/data/mods/gen1/moves.ts @@ -831,9 +831,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/gen1jpn/moves.ts b/data/mods/gen1jpn/moves.ts index 3e26d6af8e..3f0202a885 100644 --- a/data/mods/gen1jpn/moves.ts +++ b/data/mods/gen1jpn/moves.ts @@ -52,8 +52,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/gen4/scripts.ts b/data/mods/gen4/scripts.ts index 9206d07610..116d2c39fd 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -182,11 +182,11 @@ export const Scripts: ModdedBattleScriptsData = { } return hitResults; }, - calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number { - if (damageDealt === 0) return 0; + 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); - if (move.recoil) recoilDamage = this.battle.clampIntRange(Math.floor(damageDealt * move.recoil[0] / move.recoil[1]), 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); diff --git a/data/mods/passiveaggressive/scripts.ts b/data/mods/passiveaggressive/scripts.ts index f1a74b2878..49093671c0 100644 --- a/data/mods/passiveaggressive/scripts.ts +++ b/data/mods/passiveaggressive/scripts.ts @@ -166,20 +166,18 @@ export const Scripts: ModdedBattleScriptsData = { return damage; }, - calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number { - if (damageDealt === 0) return 0; - const hpBeforeRecoil = pokemon.hp; - + calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number | null { const calc = calculate(this.battle, pokemon, pokemon, move.id); if (!calc) return 0; let recoilDamage = 0; if (move.struggleRecoil) recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - if (move.mindBlownRecoil) recoilDamage = Math.round(calc * pokemon.maxhp / 2); - if (move.recoil) { + 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 { diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 5bbaf4e274..869b1bdb5b 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -1370,15 +1370,14 @@ export class BattleActions { return retVal === true ? undefined : retVal; } - calcRecoilDamage(damageDealt: number, move: Move, pokemon: Pokemon): number { - if (damageDealt === 0) return 0; - const hpBeforeRecoil = pokemon.hp; - - let recoilDamage = 0; + 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); - if (move.mindBlownRecoil) recoilDamage = Math.round(pokemon.maxhp / 2); - if (move.recoil) recoilDamage = this.battle.clampIntRange(Math.round(damageDealt * move.recoil[0] / move.recoil[1]), 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 { diff --git a/sim/global-types.ts b/sim/global-types.ts index dab8ed3233..bb41126f78 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; From 14df235093b2d716dea72e5582830913ff1c6cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= Date: Wed, 14 Jan 2026 00:24:15 +0000 Subject: [PATCH 5/6] Move Gen 1 logic in its script --- data/mods/gen1/scripts.ts | 1 + data/mods/gen1stadium/scripts.ts | 1 + data/mods/gen4/scripts.ts | 5 +++-- sim/battle-actions.ts | 5 +++-- sim/battle.ts | 8 +------- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index 118b5ac213..950b0ac836 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -537,6 +537,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/gen1stadium/scripts.ts b/data/mods/gen1stadium/scripts.ts index 04071bb678..44c07afa86 100644 --- a/data/mods/gen1stadium/scripts.ts +++ b/data/mods/gen1stadium/scripts.ts @@ -434,6 +434,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/gen4/scripts.ts b/data/mods/gen4/scripts.ts index 116d2c39fd..524e7df16d 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -185,8 +185,9 @@ export const Scripts: ModdedBattleScriptsData = { 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; + 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); diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 869b1bdb5b..c8dd636dcc 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -1374,8 +1374,9 @@ export class BattleActions { 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; + 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) { diff --git a/sim/battle.ts b/sim/battle.ts index e4c6e2ee87..5d8e330b34 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -2066,7 +2066,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; @@ -2115,12 +2115,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 From 44c635220a17d4596c59c2755bcb43052d767724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?= <80102738+andrebastosdias@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:43:58 +0000 Subject: [PATCH 6/6] Fix Axe Kick message --- data/moves.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/moves.ts b/data/moves.ts index 8a06cbc93b..0b90bf0ba7 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -970,7 +970,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,