From 6cc75a6f88bae7939ba58249561d967d514b6882 Mon Sep 17 00:00:00 2001 From: livid washed <115855253+livid-washed@users.noreply.github.com> Date: Wed, 30 Nov 2022 22:14:03 +0100 Subject: [PATCH] Gen 1: Fix some more Counter inaccuracies (#9091) * Move Gen 1 Counter tests into its own section * Confusion damage, (h)jk recoil, and draining can be countered in gen 1 * Recoil and Drain round down in gen 1 (against substitute) * Add tests for Counter + fix linting issue * lastSelectedMove is updated when Mirror Move or Metronome calls another move * Move pokemon.side.lastMove setting into a (more) correct location * move pokemon.lastMove as well * Remove unnecessary seed in test --- data/mods/gen1/moves.ts | 8 +- data/mods/gen1/scripts.ts | 5 +- data/mods/gen1stadium/moves.ts | 2 +- data/mods/gen1stadium/scripts.ts | 4 +- data/moves.ts | 1 + sim/battle.ts | 32 +++-- test/sim/moves/counter.js | 225 +++++++++++++++++++------------ 7 files changed, 172 insertions(+), 105 deletions(-) diff --git a/data/mods/gen1/moves.ts b/data/mods/gen1/moves.ts index 3647fa42c2..058267e709 100644 --- a/data/mods/gen1/moves.ts +++ b/data/mods/gen1/moves.ts @@ -560,6 +560,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!foe?.lastMove || foe.lastMove.id === 'mirrormove') { return false; } + pokemon.side.lastSelectedMove = foe.lastMove.id; this.actions.useMove(foe.lastMove.id, pokemon); }, }, @@ -850,10 +851,13 @@ export const Moves: {[k: string]: ModdedMoveData} = { // Drain/recoil/secondary effect confusion do 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'); + this.damage(this.clampIntRange(Math.floor(uncappedDamage * move.recoil[0] / move.recoil[1]), 1) + , source, target, 'recoil'); } if (move.drain) { - this.heal(Math.ceil(uncappedDamage * move.drain[0] / move.drain[1]), source, target, 'drain'); + const amount = this.clampIntRange(Math.floor(uncappedDamage * move.drain[0] / move.drain[1]), 1); + this.lastDamage = amount; + this.heal(amount, source, target, 'drain'); } if (move.secondary?.volatileStatus === 'confusion') { const secondary = move.secondary; diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index c6ea3aba4e..704759f410 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -143,9 +143,6 @@ export const Scripts: ModdedBattleScriptsData = { (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) ) { pokemon.deductPP(move, null, target); - // On gen 1 moves are stored when they are chosen and a PP is deducted. - pokemon.side.lastMove = move; - pokemon.lastMove = move; } else { sourceEffect = move; } @@ -187,6 +184,8 @@ export const Scripts: ModdedBattleScriptsData = { if (move.id !== 'mirrormove' || (!pokemon.side.foe.active[0]?.lastMove || pokemon.side.foe.active[0].lastMove?.id === 'mirrormove')) { // The move is our 'final' move (a failed Mirror Move, or any move that isn't Metronome or Mirror Move). + pokemon.side.lastMove = move; + pokemon.lastMove = move; this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); // If target fainted diff --git a/data/mods/gen1stadium/moves.ts b/data/mods/gen1stadium/moves.ts index e7bf97f325..0b8952d38f 100644 --- a/data/mods/gen1stadium/moves.ts +++ b/data/mods/gen1stadium/moves.ts @@ -264,7 +264,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { // Drain/recoil does not happen if the substitute breaks if (target.volatiles['substitute']) { if (move.recoil) { - this.damage(Math.round(damage * move.recoil[0] / move.recoil[1]), source, target, 'recoil'); + this.damage(this.clampIntRange(Math.floor(damage * move.recoil[0] / move.recoil[1]), 1), source, target, 'recoil'); } } this.runEvent('AfterSubDamage', target, source, move, damage); diff --git a/data/mods/gen1stadium/scripts.ts b/data/mods/gen1stadium/scripts.ts index 518d8bcd97..c1daad4a6b 100644 --- a/data/mods/gen1stadium/scripts.ts +++ b/data/mods/gen1stadium/scripts.ts @@ -90,8 +90,6 @@ export const Scripts: ModdedBattleScriptsData = { (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) ) { pokemon.deductPP(move, null, target); - pokemon.side.lastMove = move; - pokemon.lastMove = move; } else { sourceEffect = move; } @@ -126,6 +124,8 @@ export const Scripts: ModdedBattleScriptsData = { if (move.id !== 'mirrormove' || (!pokemon.side.foe.active[0]?.lastMove || pokemon.side.foe.active[0].lastMove?.id === 'mirrormove')) { // The move is our 'final' move (a failed Mirror Move, or any move that isn't Metronome or Mirror Move). + pokemon.side.lastMove = move; + pokemon.lastMove = move; this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); // If target fainted diff --git a/data/moves.ts b/data/moves.ts index defae64891..9bb8c9ad6d 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -11903,6 +11903,7 @@ export const Moves: {[moveid: string]: MoveData} = { randomMove = this.sample(moves).id; } if (!randomMove) return false; + source.side.lastSelectedMove = this.toID(randomMove); this.actions.useMove(randomMove, target); }, secondary: null, diff --git a/sim/battle.ts b/sim/battle.ts index a76b708033..a149418744 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -1925,6 +1925,8 @@ export class Battle { } 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 + if (this.gen <= 1) this.lastDamage = amount; this.heal(amount, source, target, 'drain'); } if (this.gen > 4 && effect.drain && source) { @@ -1978,21 +1980,25 @@ export class Battle { // In Gen 1 BUT NOT STADIUM, Substitute also takes confusion and HJK recoil damage if (this.gen <= 1 && this.dex.currentMod !== 'gen1stadium' && - ['confusion', 'jumpkick', 'highjumpkick'].includes(effect.id) && target.volatiles['substitute']) { - const hint = "In Gen 1, if a Pokemon with a Substitute hurts itself due to confusion or Jump Kick/Hi Jump Kick recoil and the target"; - if (source?.volatiles['substitute']) { - source.volatiles['substitute'].hp -= damage; - if (source.volatiles['substitute'].hp <= 0) { - source.removeVolatile('substitute'); - source.subFainted = true; + ['confusion', 'jumpkick', 'highjumpkick'].includes(effect.id)) { + // Confusion and recoil damage can be countered + this.lastDamage = damage; + if (target.volatiles['substitute']) { + const hint = "In Gen 1, if a Pokemon with a Substitute hurts itself due to confusion or Jump Kick/Hi Jump Kick recoil and the target"; + if (source?.volatiles['substitute']) { + source.volatiles['substitute'].hp -= damage; + if (source.volatiles['substitute'].hp <= 0) { + source.removeVolatile('substitute'); + source.subFainted = true; + } else { + this.add('-activate', source, 'Substitute', '[damage]'); + } + this.hint(hint + " has a Substitute, the target's Substitute takes the damage."); + return damage; } else { - this.add('-activate', source, 'Substitute', '[damage]'); + this.hint(hint + " does not have a Substitute there is no damage dealt."); + return 0; } - this.hint(hint + " has a Substitute, the target's Substitute takes the damage."); - return damage; - } else { - this.hint(hint + " does not have a Substitute there is no damage dealt."); - return 0; } } diff --git a/test/sim/moves/counter.js b/test/sim/moves/counter.js index f4930099c7..af6f1dfc2b 100644 --- a/test/sim/moves/counter.js +++ b/test/sim/moves/counter.js @@ -72,6 +72,106 @@ describe('Counter', function () { assert.fullHP(battle.p2.active[0]); }); + it(`should not have its target changed by Stalwart`, function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: "Duraludon", ability: 'stalwart', moves: ['counter']}, + {species: "Diglett", moves: ['sleeptalk']}, + ], [ + {species: "Wynaut", moves: ['sleeptalk']}, + {species: "Noivern", moves: ['dragonclaw']}, + ]]); + + const wynaut = battle.p2.active[0]; + battle.makeChoices('auto', 'move sleeptalk, move dragonclaw 1'); + assert.equal(wynaut.maxhp, wynaut.hp); + }); +}); + +describe('Mirror Coat', function () { + afterEach(function () { + battle.destroy(); + }); + + it('should deal damage equal to twice the damage taken from the last Special attack', function () { + battle = common.createBattle(); + battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['sonicboom']}]}); + battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); + assert.hurtsBy(battle.p1.active[0], 40, () => battle.makeChoices()); + }); + + it('should deal damage based on the last hit from the last Special attack', function () { + battle = common.createBattle(); + battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['watershuriken']}]}); + battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); + let lastDamage = 0; + battle.onEvent('Damage', battle.format, function (damage, attacker, defender, move) { + if (move.id === 'watershuriken') { + lastDamage = damage; + } + }); + + battle.makeChoices(); + assert.equal(battle.p1.active[0].maxhp - battle.p1.active[0].hp, 2 * lastDamage); + }); + + it('should fail if user is not damaged by Special attacks this turn', function () { + battle = common.createBattle(); + battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['tackle']}]}); + battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); + assert.false.hurts(battle.p1.active[0], () => battle.makeChoices()); + }); + + it('should target the opposing Pokemon that hit the user with a Special attack most recently that turn', function () { + battle = common.createBattle({gameType: 'doubles'}); + battle.setPlayer('p1', {team: [ + {species: 'Mew', ability: 'synchronize', moves: ['mirrorcoat']}, + {species: 'Lucario', ability: 'justified', item: 'laggingtail', moves: ['aurasphere']}, + ]}); + battle.setPlayer('p2', {team: [ + {species: 'Crobat', ability: 'innerfocus', moves: ['venoshock']}, + {species: 'Avalugg', ability: 'sturdy', moves: ['flashcannon']}, + ]}); + battle.makeChoices('move mirrorcoat, move aurasphere -1', 'move venoshock 1, move flashcannon 1'); + assert.fullHP(battle.p1.active[1]); + assert.fullHP(battle.p2.active[0]); + assert.false.fullHP(battle.p2.active[1]); + }); + + it('should respect Follow Me', function () { + battle = common.createBattle({gameType: 'doubles'}); + battle.setPlayer('p1', {team: [ + {species: 'Mew', ability: 'synchronize', moves: ['mirrorcoat']}, + {species: 'Magikarp', ability: 'rattled', moves: ['splash']}, + ]}); + battle.setPlayer('p2', {team: [ + {species: 'Crobat', ability: 'innerfocus', moves: ['venoshock']}, + {species: 'Clefable', ability: 'unaware', moves: ['followme']}, + ]}); + battle.makeChoices('move mirrorcoat, move splash', 'move venoshock 1, move followme'); + assert.false.fullHP(battle.p2.active[1]); + assert.fullHP(battle.p2.active[0]); + }); + + it(`should not have its target changed by Stalwart`, function () { + battle = common.createBattle({gameType: 'doubles'}, [[ + {species: "Duraludon", ability: 'stalwart', moves: ['mirrorcoat']}, + {species: "Diglett", moves: ['sleeptalk']}, + ], [ + {species: "Wynaut", moves: ['sleeptalk']}, + {species: "Noivern", moves: ['dragonpulse']}, + ]]); + + const wynaut = battle.p2.active[0]; + battle.makeChoices('auto', 'move sleeptalk, move dragonpulse 1'); + assert.equal(wynaut.maxhp, wynaut.hp); + }); +}); + +describe('Counter', function () { + afterEach(function () { + battle.destroy(); + }); + it(`[Gen 1] Counter Desync Clause`, function () { // seed chosen so Water Gun succeeds and Pound full paras battle = common.gen(1).createBattle({seed: [1, 2, 3, 3]}, [[ @@ -174,97 +274,54 @@ describe('Counter', function () { assert.false.fullHP(battle.p2.active[0]); }); - it(`should not have its target changed by Stalwart`, function () { - battle = common.createBattle({gameType: 'doubles'}, [[ - {species: "Duraludon", ability: 'stalwart', moves: ['counter']}, - {species: "Diglett", moves: ['sleeptalk']}, + it(`[Gen 1] (High) Jump Kick recoil can be countered`, function () { + battle = common.gen(1).createBattle([[ + {species: 'Gengar', moves: ['counter']}, ], [ - {species: "Wynaut", moves: ['sleeptalk']}, - {species: "Noivern", moves: ['dragonclaw']}, + {species: 'Hitmonlee', moves: ['highjumpkick']}, ]]); - - const wynaut = battle.p2.active[0]; - battle.makeChoices('auto', 'move sleeptalk, move dragonclaw 1'); - assert.equal(wynaut.maxhp, wynaut.hp); - }); -}); - -describe('Mirror Coat', function () { - afterEach(function () { - battle.destroy(); - }); - - it('should deal damage equal to twice the damage taken from the last Special attack', function () { - battle = common.createBattle(); - battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['sonicboom']}]}); - battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); - assert.hurtsBy(battle.p1.active[0], 40, () => battle.makeChoices()); - }); - - it('should deal damage based on the last hit from the last Special attack', function () { - battle = common.createBattle(); - battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['watershuriken']}]}); - battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); - let lastDamage = 0; - battle.onEvent('Damage', battle.format, function (damage, attacker, defender, move) { - if (move.id === 'watershuriken') { - lastDamage = damage; - } - }); - battle.makeChoices(); - assert.equal(battle.p1.active[0].maxhp - battle.p1.active[0].hp, 2 * lastDamage); + const hitmonlee = battle.p2.active[0]; + assert.equal(hitmonlee.maxhp - hitmonlee.hp, 3); }); - it('should fail if user is not damaged by Special attacks this turn', function () { - battle = common.createBattle(); - battle.setPlayer('p1', {team: [{species: 'Espeon', ability: 'synchronize', moves: ['tackle']}]}); - battle.setPlayer('p2', {team: [{species: 'Umbreon', ability: 'synchronize', moves: ['mirrorcoat']}]}); - assert.false.hurts(battle.p1.active[0], () => battle.makeChoices()); - }); - - it('should target the opposing Pokemon that hit the user with a Special attack most recently that turn', function () { - battle = common.createBattle({gameType: 'doubles'}); - battle.setPlayer('p1', {team: [ - {species: 'Mew', ability: 'synchronize', moves: ['mirrorcoat']}, - {species: 'Lucario', ability: 'justified', item: 'laggingtail', moves: ['aurasphere']}, - ]}); - battle.setPlayer('p2', {team: [ - {species: 'Crobat', ability: 'innerfocus', moves: ['venoshock']}, - {species: 'Avalugg', ability: 'sturdy', moves: ['flashcannon']}, - ]}); - battle.makeChoices('move mirrorcoat, move aurasphere -1', 'move venoshock 1, move flashcannon 1'); - assert.fullHP(battle.p1.active[1]); - assert.fullHP(battle.p2.active[0]); - assert.false.fullHP(battle.p2.active[1]); - }); - - it('should respect Follow Me', function () { - battle = common.createBattle({gameType: 'doubles'}); - battle.setPlayer('p1', {team: [ - {species: 'Mew', ability: 'synchronize', moves: ['mirrorcoat']}, - {species: 'Magikarp', ability: 'rattled', moves: ['splash']}, - ]}); - battle.setPlayer('p2', {team: [ - {species: 'Crobat', ability: 'innerfocus', moves: ['venoshock']}, - {species: 'Clefable', ability: 'unaware', moves: ['followme']}, - ]}); - battle.makeChoices('move mirrorcoat, move splash', 'move venoshock 1, move followme'); - assert.false.fullHP(battle.p2.active[1]); - assert.fullHP(battle.p2.active[0]); - }); - - it(`should not have its target changed by Stalwart`, function () { - battle = common.createBattle({gameType: 'doubles'}, [[ - {species: "Duraludon", ability: 'stalwart', moves: ['mirrorcoat']}, - {species: "Diglett", moves: ['sleeptalk']}, + it(`[Gen 1] confusion damage can be countered`, function () { + battle = common.gen(1).createBattle({seed: [1, 0, 0, 0]}, [[ + {species: 'Gengar', moves: ['confuseray', 'counter']}, ], [ - {species: "Wynaut", moves: ['sleeptalk']}, - {species: "Noivern", moves: ['dragonpulse']}, + {species: 'Alakazam', moves: ['seismictoss']}, ]]); + battle.makeChoices(); + battle.makeChoices('move counter', 'move seismictoss'); + const alakazam = battle.p2.active[0]; + assert.false.fullHP(alakazam); + // Confusion damage was countered, not Seismic Toss + assert.false.equal(alakazam.maxhp - alakazam.hp, 200); + }); - const wynaut = battle.p2.active[0]; - battle.makeChoices('auto', 'move sleeptalk, move dragonpulse 1'); - assert.equal(wynaut.maxhp, wynaut.hp); + it(`[Gen 1] draining can be countered`, function () { + battle = common.gen(1).createBattle({seed: [1, 0, 0, 0]}, [[ + {species: 'Gengar', moves: ['megadrain', 'counter']}, + ], [ + {species: 'Alakazam', moves: ['seismictoss']}, + {species: 'Exeggutor', moves: ['barrage']}, + ]]); + battle.makeChoices(); + battle.makeChoices('move counter', 'switch 2'); + const gengar = battle.p1.active[0]; + const exeggutor = battle.p2.active[0]; + assert.equal(exeggutor.maxhp - exeggutor.hp, (gengar.hp - (gengar.maxhp - 100)) * 2); + }); + + it(`[Gen 1] Mirror Move can be countered when it calls a counterable move`, function () { + battle = common.gen(1).createBattle([[ + {species: 'Pidgeot', moves: ['mirrormove']}, + ], [ + {species: 'Alakazam', moves: ['seismictoss', 'counter']}, + ]]); + battle.makeChoices(); + battle.makeChoices('move mirrormove', 'move counter'); + const pidgeot = battle.p1.active[0]; + assert.equal(pidgeot.maxhp - pidgeot.hp, 300); }); });