mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-09-10 10:15:37 -05:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user