Fix Emergency Exit timing

This is a really hacky implementation of Emergency Exit, but Emergency
Exit itself is a huge mess on cart, too.

Our previous implementation:
- activated Emergency Exit at AfterMoveSecondary timing for move damage
- activated Emergency Exit immediately after dealing any other damage

This new one:
- activates Emergency Exit only in three situations:
- right after AfterMoveSecondary timing, for move damage
- right after DamagingHit timing, for DamagingHit residual damage
  (Rough Skin, Iron Barbs, Rocky Helmet)
- right after the switch update, for switch-hazard residual damage
  (Stealth Rock, Spikes)
- does not otherwise activate (so Substitute, Hail, Toxic, etc no
  longer activate Emergency Exit)

This should much accurately simulate Emergency Exit behavior, including
most famously timing it after healing berries after hazards, as
documented in:

https://www.smogon.com/forums/threads/pokemon-sun-moon-battle-mechanics-research.3586701/#post-7075354

Fixes #6309
This commit is contained in:
Guangcong Luo 2020-02-08 07:48:20 -08:00
parent 5d44d642d7
commit 3546cde82c
5 changed files with 53 additions and 21 deletions

View File

@ -891,17 +891,15 @@ let BattleAbilities = {
"emergencyexit": {
desc: "When this Pokemon has more than 1/2 its maximum HP and takes damage bringing it to 1/2 or less of its maximum HP, it immediately switches out to a chosen ally. This effect applies after all hits from a multi-hit move; Sheer Force prevents it from activating if the move has a secondary effect. This effect applies to both direct and indirect damage, except Curse and Substitute on use, Belly Drum, Pain Split, and confusion damage.",
shortDesc: "This Pokemon switches out when it reaches 1/2 or less of its maximum HP.",
onAfterMoveSecondary(target, source, move) {
if (!source || source === target || !target.hp || !move.totalDamage) return;
const lastAttackedBy = target.getLastAttackedBy();
if (!lastAttackedBy) return;
const damage = move.multihit ? move.totalDamage : lastAttackedBy.damage;
if (target.hp <= target.maxhp / 2 && target.hp + damage > target.maxhp / 2) {
if (!this.canSwitch(target.side) || target.forceSwitchFlag || target.switchFlag) return;
target.switchFlag = true;
source.switchFlag = false;
this.add('-activate', target, 'ability: Emergency Exit');
onEmergencyExit(target) {
if (!this.canSwitch(target.side) || target.forceSwitchFlag || target.switchFlag) return;
for (const side of this.sides) {
for (const active of side.active) {
active.switchFlag = false;
}
}
target.switchFlag = true;
this.add('-activate', target, 'ability: Emergency Exit');
},
id: "emergencyexit",
name: "Emergency Exit",
@ -4589,17 +4587,15 @@ let BattleAbilities = {
"wimpout": {
desc: "When this Pokemon has more than 1/2 its maximum HP and takes damage bringing it to 1/2 or less of its maximum HP, it immediately switches out to a chosen ally. This effect applies after all hits from a multi-hit move; Sheer Force prevents it from activating if the move has a secondary effect. This effect applies to both direct and indirect damage, except Curse and Substitute on use, Belly Drum, Pain Split, and confusion damage.",
shortDesc: "This Pokemon switches out when it reaches 1/2 or less of its maximum HP.",
onAfterMoveSecondary(target, source, move) {
if (!source || source === target || !target.hp || !move.totalDamage) return;
const lastAttackedBy = target.getLastAttackedBy();
if (!lastAttackedBy) return;
const damage = move.multihit ? move.totalDamage : lastAttackedBy.damage;
if (target.hp <= target.maxhp / 2 && target.hp + damage > target.maxhp / 2) {
if (!this.canSwitch(target.side) || target.forceSwitchFlag || target.switchFlag) return;
target.switchFlag = true;
source.switchFlag = false;
this.add('-activate', target, 'ability: Wimp Out');
onEmergencyExit(target) {
if (!this.canSwitch(target.side) || target.forceSwitchFlag || target.switchFlag) return;
for (const side of this.sides) {
for (const active of side.active) {
active.switchFlag = false;
}
}
target.switchFlag = true;
this.add('-activate', target, 'ability: Wimp Out');
},
id: "wimpout",
name: "Wimp Out",

View File

@ -708,6 +708,15 @@ let BattleScripts = {
// @ts-ignore
this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val), pokemon, move);
if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) {
for (let i = 0; i < damage.length; i++) {
const curDamage = damage[i];
if (typeof curDamage === 'number' && targets[i].hp <= targets[i].maxhp / 2 && targets[i].hp + curDamage > targets[i].maxhp / 2) {
this.runEvent('EmergencyExit', targets[i], pokemon);
}
}
}
return damage;
},
spreadMoveHit(targets, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) {
@ -801,6 +810,7 @@ let BattleScripts = {
damagedDamage.push(damage[i]);
}
}
const pokemonOriginalHP = pokemon.hp;
if (damagedDamage.length) {
this.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage);
if (moveData.onAfterHit) {
@ -808,6 +818,9 @@ let BattleScripts = {
this.singleEvent('AfterHit', moveData, {}, target, pokemon, move);
}
}
if (pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP > pokemon.maxhp / 2) {
this.runEvent('EmergencyExit', pokemon);
}
}
return [damage, targets];

View File

@ -2608,6 +2608,7 @@ export class Battle {
}
runAction(action: Actions.Action) {
const pokemonOriginalHP = action.pokemon?.hp;
// returns whether or not we ended in a callback
switch (action.choice) {
case 'start': {
@ -2835,6 +2836,13 @@ export class Battle {
}
this.eachEvent('Update');
if (action.choice === 'runSwitch') {
if (action.pokemon.hp <= action.pokemon.maxhp / 2 && pokemonOriginalHP! > action.pokemon.maxhp / 2) {
this.runEvent('EmergencyExit', action.pokemon);
}
}
if (this.gen >= 8 && this.queue.length && this.queue[0].choice === 'move') {
// In gen 8, speed is updated dynamically so update the queue's speed properties and sort it.
this.updateSpeed();

View File

@ -195,6 +195,7 @@ interface PureEffectEventMethods {
interface EventMethods {
onDamagingHit?: (this: Battle, damage: number, target: Pokemon, source: Pokemon, move: ActiveMove) => void
onEmergencyExit?: (this: Battle, pokemon: Pokemon) => void
onAfterEachBoost?: (this: Battle, boost: SparseBoostsTable, target: Pokemon, source: Pokemon) => void
onAfterHit?: MoveEventMethods['onAfterHit']
onAfterSetStatus?: (this: Battle, status: PureEffect, target: Pokemon, source: Pokemon, effect: Effect) => void

View File

@ -23,7 +23,7 @@ describe(`Emergency Exit`, function () {
assert.strictEqual(battle.requestState, 'switch');
});
it(`should not request switch-out if first healed by berry`, function () {
it(`should not request switch-out if attacked and healed by berry`, function () {
battle = common.createBattle([
[{species: "Golisopod", ability: 'emergencyexit', moves: ['sleeptalk'], item: 'sitrusberry', ivs: EMPTY_IVS}, {species: "Clefable", ability: 'Unaware', moves: ['metronome']}],
[{species: "Raticate", ability: 'guts', moves: ['superfang']}],
@ -32,6 +32,20 @@ describe(`Emergency Exit`, function () {
assert.strictEqual(battle.requestState, 'move');
});
it(`should not request switch-out after taking residual damage and getting healed by berry`, function () {
battle = common.createBattle([
[{species: "Golisopod", ability: 'emergencyexit', moves: ['uturn', 'sleeptalk'], item: 'sitrusberry'}, {species: "Magikarp", ability: 'swiftswim', moves: ['splash']}],
[{species: "Ferrothorn", ability: 'ironbarbs', moves: ['stealthrock', 'spikes', 'protect']}],
]);
battle.makeChoices('move uturn', 'move stealthrock');
battle.makeChoices('switch 2', '');
battle.makeChoices('move splash', 'move spikes');
battle.makeChoices('move splash', 'move spikes');
battle.makeChoices('move splash', 'move spikes');
battle.makeChoices('switch 2', 'move protect');
assert.strictEqual(battle.requestState, 'move');
});
it(`should not request switch-out on usage of Substitute`, function () {
battle = common.createBattle([
[{species: "Golisopod", ability: 'emergencyexit', moves: ['substitute'], ivs: EMPTY_IVS}, {species: "Clefable", ability: 'Unaware', moves: ['metronome']}],