diff --git a/data/abilities.ts b/data/abilities.ts index 2bbe4b3de8..87822c4229 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -3192,7 +3192,7 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { num: 253, }, pickpocket: { - onAfterMoveSecondary(target, source, move) { + onAfterMoveSecondaryLast(target, source, move) { if (source && source !== target && move?.flags['contact']) { if (target.item || target.switchFlag || target.forceSwitchFlag || source.switchFlag === true) { return; diff --git a/data/mods/champions/scripts.ts b/data/mods/champions/scripts.ts index 78b37ae99d..a27ad3cb88 100644 --- a/data/mods/champions/scripts.ts +++ b/data/mods/champions/scripts.ts @@ -552,5 +552,172 @@ export const Scripts: ModdedBattleScriptsData = { return damage; }, + // Sheer Force shouldnt suppress Pickpocket + useMoveInner(moveOrMoveName, pokemon, options) { + let target = options?.target; + let sourceEffect = options?.sourceEffect; + const zMove = options?.zMove; + const maxMove = options?.maxMove; + if (!sourceEffect && this.battle.effect.id) sourceEffect = this.battle.effect; + if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null; + + let move = this.dex.getActiveMove(moveOrMoveName); + pokemon.lastMoveUsed = move; + if (move.id === 'weatherball' && zMove) { + // Z-Weather Ball only changes types if it's used directly, + // not if it's called by Z-Sleep Talk or something. + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + if (move.type !== 'Normal') sourceEffect = move; + } + if (zMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isZ)) { + move = this.getActiveZMove(move, pokemon); + } + if (maxMove && move.category !== 'Status') { + // Max move outcome is dependent on the move type after type modifications from ability and the move itself + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.runEvent('ModifyType', pokemon, target, move, move); + } + if (maxMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isMax)) { + move = this.getActiveMaxMove(move, pokemon); + } + + if (this.battle.activeMove) { + move.priority = this.battle.activeMove.priority; + if (!move.hasBounced) move.pranksterBoosted = this.battle.activeMove.pranksterBoosted; + } + const baseTarget = move.target; + let targetRelayVar = { target }; + targetRelayVar = this.battle.runEvent('ModifyTarget', pokemon, target, move, targetRelayVar, true); + if (targetRelayVar.target !== undefined) target = targetRelayVar.target; + if (target === undefined) target = this.battle.getRandomTarget(pokemon, move); + if (move.target === 'self' || move.target === 'allies') { + target = pokemon; + } + if (sourceEffect) { + move.sourceEffect = sourceEffect.id; + move.ignoreAbility = (sourceEffect as ActiveMove).ignoreAbility; + } + let moveResult = false; + + this.battle.setActiveMove(move, pokemon, target); + + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.singleEvent('ModifyMove', move, null, pokemon, target, move, move); + if (baseTarget !== move.target) { + // Target changed in ModifyMove, so we must adjust it here + // Adjust before the next event so the correct target is passed to the + // event + target = this.battle.getRandomTarget(pokemon, move); + } + move = this.battle.runEvent('ModifyType', pokemon, target, move, move); + move = this.battle.runEvent('ModifyMove', pokemon, target, move, move); + if (baseTarget !== move.target) { + // Adjust again + target = this.battle.getRandomTarget(pokemon, move); + } + if (!move || pokemon.fainted) { + return false; + } + + let attrs = ''; + + let movename = move.name; + if (move.id === 'hiddenpower') movename = 'Hidden Power'; + if (sourceEffect) attrs += `|[from] ${sourceEffect.fullname}`; + if (zMove && move.isZ === true) { + attrs = `|[anim]${movename}${attrs}`; + movename = `Z-${movename}`; + } + this.battle.addMove('move', pokemon, movename, `${target}${attrs}`); + + if (zMove) this.runZPower(move, pokemon); + + if (!target) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; + } + + const { targets, pressureTargets } = pokemon.getMoveTargets(move, target); + if (targets.length) { + target = targets[targets.length - 1]; // in case of redirection + } + + const callerMoveForPressure = sourceEffect && (sourceEffect as ActiveMove).pp ? sourceEffect as ActiveMove : null; + if (!sourceEffect || callerMoveForPressure || sourceEffect.id === 'pursuit') { + let extraPP = 0; + for (const source of pressureTargets) { + const ppDrop = this.battle.runEvent('DeductPP', source, pokemon, move); + if (ppDrop !== true) { + extraPP += ppDrop || 0; + } + } + if (extraPP > 0) { + pokemon.deductPP(callerMoveForPressure || moveOrMoveName, extraPP); + } + } + + let tryMoveResult = this.battle.singleEvent('TryMove', move, null, pokemon, target, move); + if (tryMoveResult) { + tryMoveResult = this.battle.runEvent('TryMove', pokemon, target, move); + } + if (!tryMoveResult) { + move.mindBlownRecoil = false; + return tryMoveResult; + } + + this.battle.singleEvent('UseMoveMessage', move, null, pokemon, target, move); + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + if (this.battle.gen !== 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } + + let damage: number | false | undefined | '' = false; + if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { + damage = this.tryMoveHit(targets, pokemon, move); + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + if (damage || damage === 0 || damage === undefined) moveResult = true; + } else { + if (!targets.length) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; + } + if (this.battle.gen === 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } + moveResult = this.trySpreadMoveHit(targets, pokemon, move); + } + if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); + if (!pokemon.hp) { + this.battle.faint(pokemon, pokemon, move); + } + + if (!moveResult) { + this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + return false; + } + + if (!(move.hasSheerForce && pokemon.hasAbility('sheerforce')) && !move.flags['futuremove']) { + const originalHp = pokemon.hp; + this.battle.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); + this.battle.runEvent('AfterMoveSecondarySelf', pokemon, target, move); + if (pokemon && pokemon !== target && move.category !== 'Status') { + if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } + } + } + for (const curTarget of targets) { + this.battle.singleEvent('AfterMoveSecondaryLast', move, null, curTarget, pokemon, move); + this.battle.runEvent('AfterMoveSecondaryLast', curTarget, pokemon, move); + } + + return true; + }, }, }; diff --git a/data/moves.ts b/data/moves.ts index 67fc257f2d..8e058ad5f2 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -9425,11 +9425,8 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = { pp: 15, priority: 0, flags: { contact: 1, protect: 1, mirror: 1, metronome: 1 }, - onAfterHit(target, source) { - this.field.clearTerrain(); - }, - onAfterSubDamage(damage, target, source) { - if (source.hp) { + onAfterMoveSecondaryLast(target, source) { + if (source.hp && !source.forceSwitchFlag) { this.field.clearTerrain(); } }, diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 66fd617e6d..a58ff2cd0c 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -537,6 +537,10 @@ export class BattleActions { this.battle.runEvent('EmergencyExit', pokemon, pokemon); } } + for (const curTarget of targets) { + this.battle.singleEvent('AfterMoveSecondaryLast', move, null, curTarget, pokemon, move); + this.battle.runEvent('AfterMoveSecondaryLast', curTarget, pokemon, move); + } } return true; @@ -1747,7 +1751,7 @@ export class BattleActions { } // weather modifier - baseDamage = this.battle.priorityEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); + baseDamage = this.battle.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); // crit - not a modifier const isCrit = target.getMoveHitData(move).crit; diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts index 9445d46083..a1c2fd1a08 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -24,6 +24,7 @@ export interface EventMethods { onAfterTakeItem?: (this: Battle, item: Item, pokemon: Pokemon) => void; onAfterBoost?: (this: Battle, boost: SparseBoostsTable, target: Pokemon, source: Pokemon, effect: Effect) => void; onAfterFaint?: (this: Battle, length: number, target: Pokemon, source: Pokemon, effect: Effect) => void; + onAfterMoveSecondaryLast?: MoveEventMethods['onAfterMoveSecondaryLast']; onAfterMoveSecondarySelf?: MoveEventMethods['onAfterMoveSecondarySelf']; onAfterMoveSecondary?: MoveEventMethods['onAfterMoveSecondary']; onAfterMove?: MoveEventMethods['onAfterMove']; diff --git a/sim/dex-moves.ts b/sim/dex-moves.ts index f159dc1155..fe6b0523a8 100644 --- a/sim/dex-moves.ts +++ b/sim/dex-moves.ts @@ -108,6 +108,7 @@ export interface MoveEventMethods { onAfterHit?: CommonHandlers['VoidSourceMove']; onAfterSubDamage?: (this: Battle, damage: number, target: Pokemon, source: Pokemon, move: ActiveMove) => void; + onAfterMoveSecondaryLast?: CommonHandlers['VoidMove']; onAfterMoveSecondarySelf?: CommonHandlers['VoidSourceMove']; onAfterMoveSecondary?: CommonHandlers['VoidMove']; onAfterMove?: CommonHandlers['VoidSourceMove']; diff --git a/test/sim/abilities/pickpocket.js b/test/sim/abilities/pickpocket.js index 9e94e9f215..e89678b192 100644 --- a/test/sim/abilities/pickpocket.js +++ b/test/sim/abilities/pickpocket.js @@ -46,7 +46,7 @@ describe('Pickpocket', () => { assert.holdsItem(battle.p2.active[0]); }); - it.skip(`should steal items back and forth when hit by a Magician user`, () => { + it(`should steal items back and forth when hit by a Magician user`, () => { battle = common.createBattle([[ { species: 'Weavile', ability: 'pickpocket', item: 'cheriberry', moves: ['agility'] }, ], [ diff --git a/test/sim/moves/icespinner.js b/test/sim/moves/icespinner.js index d1916dbd36..2c44daad8e 100644 --- a/test/sim/moves/icespinner.js +++ b/test/sim/moves/icespinner.js @@ -21,7 +21,19 @@ describe(`Ice Spinner`, () => { assert.false(battle.field.isTerrain('psychicterrain')); }); - it.skip(`should not remove Terrains if the user faints from Life Orb`, () => { + it(`should remove Terrains if target has a substitute`, () => { + battle = common.createBattle([[ + { species: 'wynaut', moves: ['sleeptalk', 'icespinner'] }, + ], [ + { species: 'registeel', ability: 'psychicsurge', moves: ['substitute', 'sleeptalk'] }, + ]]); + + battle.makeChoices('move sleeptalk', 'move substitute'); + battle.makeChoices('move ice spinner', 'move sleeptalk'); + assert.false(battle.field.isTerrain('psychicterrain')); + }); + + it(`should not remove Terrains if the user faints from Life Orb`, () => { battle = common.createBattle([[ { species: 'shedinja', item: 'lifeorb', moves: ['icespinner'] }, { species: 'wynaut', moves: ['sleeptalk'] }, @@ -45,7 +57,7 @@ describe(`Ice Spinner`, () => { assert(battle.field.isTerrain('psychicterrain')); }); - it.skip(`should not remove Terrains if the user is forced out via Red Card`, () => { + it(`should not remove Terrains if the user is forced out via Red Card`, () => { battle = common.createBattle([[ { species: 'shedinja', moves: ['icespinner'] }, { species: 'wynaut', moves: ['sleeptalk'] },