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:
livid washed
2022-11-30 22:14:03 +01:00
committed by GitHub
parent baba846ca4
commit 6cc75a6f88
7 changed files with 172 additions and 105 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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);
});
});