diff --git a/data/abilities.ts b/data/abilities.ts index 72f6e3f090..aef5c29be8 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -819,7 +819,40 @@ export const Abilities: import('../sim/dex-abilities').AbilityDataTable = { dancer: { flags: {}, name: "Dancer", - // implemented in runMove in scripts.js + onAnyAfterMovePriority: -200, + onAnyAfterMove(source, target, move) { + const dancer = this.effectState.target as Pokemon; + if (dancer === source || dancer.isSemiInvulnerable() || !move.flags['dance'] || + !this.lastSuccessfulMoveThisTurn || move.isExternal) return; + const targetOf1stDance = this.activeTarget!; + const dancersTarget = !targetOf1stDance.isAlly(dancer) && source.isAlly(dancer) ? + targetOf1stDance : source; + const dancersTargetLoc = dancer.getLocOf(dancersTarget); + // Gen 7: Dancer activates in order of lowest speed stat to highest + // Note that the speed stat used is after any volatile replacements like Speed Swap, + // but before any multipliers like Agility or Choice Scarf + // Ties go to whichever Pokemon has had the ability for the least amount of time + this.queue.prioritizeAction(this.queue.resolveAction({ + choice: 'move', + effectOrder: dancer.abilityState.effectOrder, + pokemon: dancer, + moveid: move.id, + targetLoc: dancersTargetLoc, + sourceEffect: this.effect, + externalMove: true, + })[0] as MoveAction, this.effect); + }, + onBeforeMovePriority: 200, + onBeforeMove(source, target, move) { + if (move.isExternal) { + this.add('-activate', source, 'ability: Dancer'); + } + }, + onTryAddVolatile(status, target, source, sourceEffect) { + if (status.id === 'lockedmove' && (sourceEffect as ActiveMove)?.isExternal) { + return null; + } + }, rating: 1.5, num: 216, }, diff --git a/data/mods/gen9ssb/scripts.ts b/data/mods/gen9ssb/scripts.ts index 4ce793a5dc..2079127bb9 100644 --- a/data/mods/gen9ssb/scripts.ts +++ b/data/mods/gen9ssb/scripts.ts @@ -1093,8 +1093,6 @@ export const Scripts: ModdedBattleScriptsData = { move = this.getActiveMaxMove(baseMove, pokemon); } - move.isExternal = externalMove; - this.battle.setActiveMove(move, pokemon, target); /* if (pokemon.moveThisTurn) { @@ -1140,10 +1138,6 @@ export const Scripts: ModdedBattleScriptsData = { pokemon.moveUsed(move, targetLoc); } - // Dancer Petal Dance hack - // TODO: implement properly - const noLock = externalMove && !pokemon.volatiles['lockedmove']; - if (zMove) { if (pokemon.illusion) { this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon); @@ -1161,35 +1155,6 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); this.battle.runEvent('AfterMove', pokemon, target, move); - // Dancer's activation order is completely different from any other event, so it's handled separately - if (move.flags['dance'] && moveDidSomething && !move.isExternal) { - const dancers = []; - for (const currentPoke of this.battle.getAllActive()) { - if (pokemon === currentPoke) continue; - if (currentPoke.hasAbility(['dancer', 'virtualidol']) && !currentPoke.isSemiInvulnerable()) { - dancers.push(currentPoke); - } - } - // Dancer activates in order of lowest speed stat to highest - // Note that the speed stat used is after any volatile replacements like Speed Swap, - // but before any multipliers like Agility or Choice Scarf - // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder - ); - const targetOf1stDance = this.battle.activeTarget!; - for (const dancer of dancers) { - if (this.battle.faintMessages()) break; - if (dancer.fainted) continue; - this.battle.add('-activate', dancer, 'ability: ' + dancer.getAbility().name); - const dancersTarget = !targetOf1stDance.isAlly(dancer) && pokemon.isAlly(dancer) ? - targetOf1stDance : - pokemon; - const dancersTargetLoc = dancer.getLocOf(dancersTarget); - this.runMove(move.id, dancer, dancersTargetLoc, { sourceEffect: dancer.getAbility(), externalMove: true }); - } - } - if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; this.battle.faintMessages(); this.battle.checkWin(); diff --git a/data/mods/linked/scripts.ts b/data/mods/linked/scripts.ts index 2963bcb0ae..112438c379 100644 --- a/data/mods/linked/scripts.ts +++ b/data/mods/linked/scripts.ts @@ -398,8 +398,6 @@ export const Scripts: ModdedBattleScriptsData = { move = this.getActiveMaxMove(baseMove, pokemon); } - move.isExternal = externalMove; - this.battle.setActiveMove(move, pokemon, target); /* if (pokemon.moveThisTurn) { @@ -451,10 +449,6 @@ export const Scripts: ModdedBattleScriptsData = { pokemon.moveUsed(move, targetLoc); } - // Dancer Petal Dance hack - // TODO: implement properly - const noLock = externalMove && !pokemon.volatiles['lockedmove']; - if (zMove) { if (pokemon.illusion) { this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon); @@ -474,36 +468,6 @@ export const Scripts: ModdedBattleScriptsData = { this.battle.add('-hint', `Some effects can force a Pokemon to use ${move.name} again in a row.`); } - // TODO: Refactor to use BattleQueue#prioritizeAction in onAnyAfterMove handlers - // Dancer's activation order is completely different from any other event, so it's handled separately - if (move.flags['dance'] && moveDidSomething && !move.isExternal) { - const dancers = []; - for (const currentPoke of this.battle.getAllActive()) { - if (pokemon === currentPoke) continue; - if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { - dancers.push(currentPoke); - } - } - // Dancer activates in order of lowest speed stat to highest - // Note that the speed stat used is after any volatile replacements like Speed Swap, - // but before any multipliers like Agility or Choice Scarf - // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder - ); - const targetOf1stDance = this.battle.activeTarget!; - for (const dancer of dancers) { - if (this.battle.faintMessages()) break; - if (dancer.fainted) continue; - this.battle.add('-activate', dancer, 'ability: Dancer'); - const dancersTarget = !targetOf1stDance.isAlly(dancer) && pokemon.isAlly(dancer) ? - targetOf1stDance : - pokemon; - const dancersTargetLoc = dancer.getLocOf(dancersTarget); - this.runMove(move.id, dancer, dancersTargetLoc, { sourceEffect: this.dex.abilities.get('dancer'), externalMove: true }); - } - } - if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; this.battle.faintMessages(); this.battle.checkWin(); diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts index 8626b26bac..14845713f4 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -291,10 +291,6 @@ export class BattleActions { pokemon.moveUsed(move, targetLoc); } - // Dancer Petal Dance hack - // TODO: implement properly - const noLock = externalMove && !pokemon.volatiles['lockedmove']; - if (zMove) { if (pokemon.illusion) { this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon); @@ -314,36 +310,6 @@ export class BattleActions { this.battle.add('-hint', `Some effects can force a Pokemon to use ${move.name} again in a row.`); } - // TODO: Refactor to use BattleQueue#prioritizeAction in onAnyAfterMove handlers - // Dancer's activation order is completely different from any other event, so it's handled separately - if (move.flags['dance'] && moveDidSomething && !move.isExternal) { - const dancers = []; - for (const currentPoke of this.battle.getAllActive()) { - if (pokemon === currentPoke) continue; - if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { - dancers.push(currentPoke); - } - } - // Dancer activates in order of lowest speed stat to highest - // Note that the speed stat used is after any volatile replacements like Speed Swap, - // but before any multipliers like Agility or Choice Scarf - // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder - ); - const targetOf1stDance = this.battle.activeTarget!; - for (const dancer of dancers) { - if (this.battle.faintMessages()) break; - if (dancer.fainted) continue; - this.battle.add('-activate', dancer, 'ability: Dancer'); - const dancersTarget = !targetOf1stDance.isAlly(dancer) && pokemon.isAlly(dancer) ? - targetOf1stDance : - pokemon; - const dancersTargetLoc = dancer.getLocOf(dancersTarget); - this.runMove(move.id, dancer, dancersTargetLoc, { sourceEffect: this.dex.abilities.get('dancer'), externalMove: true }); - } - } - if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; this.battle.faintMessages(); this.battle.checkWin(); diff --git a/sim/battle-queue.ts b/sim/battle-queue.ts index 59e5f80650..22658bdef0 100644 --- a/sim/battle-queue.ts +++ b/sim/battle-queue.ts @@ -25,7 +25,7 @@ import type { Battle } from './battle'; export interface MoveAction { /** action type */ choice: 'move' | 'beforeTurnMove' | 'priorityChargeMove'; - order: 3 | 5 | 200 | 201 | 199 | 106; + order: 5 | 105 | 199 | 200 | 201; /** priority of the action (higher first) */ priority: number; /** fractional priority of the action (higher first) */ @@ -50,13 +50,15 @@ export interface MoveAction { maxMove?: string; /** effect that called the move (eg Instruct) if any */ sourceEffect?: Effect | null; + /** if external, skips LockMove and PP deduction, mostly for use by Dancer */ + externalMove?: boolean; } /** A switch action */ export interface SwitchAction { /** action type */ choice: 'switch' | 'instaswitch' | 'revivalblessing'; - order: 3 | 6 | 103; + order: 3 | 6 | 101; /** priority of the action (higher first) */ priority: number; /** speed of pokemon switching (higher first if priority tie) */ @@ -179,14 +181,12 @@ export class BattleQueue { beforeTurnMove: 5, revivalblessing: 6, - runSwitch: 101, - switch: 103, - megaEvo: 104, - megaEvoX: 104, - megaEvoY: 104, - runDynamax: 105, - terastallize: 106, - priorityChargeMove: 107, + runSwitch: 100, + switch: 101, + megaEvo: 102, megaEvoX: 102, megaEvoY: 102, + runDynamax: 103, + terastallize: 104, + priorityChargeMove: 105, shift: 200, // default is 200 (for moves) @@ -282,7 +282,7 @@ export class BattleQueue { } } action.sourceEffect = sourceEffect; - action.order = 3; + action.order = action.choice === 'move' ? 199 : 3; this.list.unshift(action); } diff --git a/sim/battle.ts b/sim/battle.ts index c616ee74e3..8dce0dd84a 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -1001,8 +1001,11 @@ export class Battle { if (handler.effectHolder && (handler.effectHolder as Pokemon).getStat) { const pokemon = handler.effectHolder as Pokemon; handler.speed = pokemon.speed; - if (handler.effect.effectType === 'Ability' && handler.effect.name === 'Magic Bounce' && - callbackName === 'onAllyTryHitSide') { + if (handler.effect.effectType === 'Ability' && ( + (handler.effect.name === 'Dancer' && callbackName === 'onAnyAfterMove') || + (handler.effect.name === 'Magic Bounce' && callbackName === 'onAllyTryHitSide') + )) { + // TODO: Check which other events are sorted based on the unmodified speed handler.speed = pokemon.getStat('spe', true, true); } if (callbackName.endsWith('SwitchIn')) { @@ -2703,7 +2706,7 @@ export class Battle { if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; this.actions.runMove(action.move, action.pokemon, action.targetLoc, { - sourceEffect: action.sourceEffect, zMove: action.zmove, + sourceEffect: action.sourceEffect, zMove: action.zmove, externalMove: action.externalMove, maxMove: action.maxMove, originalTarget: action.originalTarget, }); break; diff --git a/sim/dex-conditions.ts b/sim/dex-conditions.ts index 9445d46083..c2e07b8bad 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -433,6 +433,7 @@ export interface EventMethods { onAfterMoveSecondarySelfPriority?: number; onAfterMoveSelfPriority?: number; onAfterSetStatusPriority?: number; + onAnyAfterMovePriority?: number; onAnyBasePowerPriority?: number; onAnyInvulnerabilityPriority?: number; onAnyModifyAccuracyPriority?: number; diff --git a/test/sim/abilities/dancer.js b/test/sim/abilities/dancer.js index 591da14aa4..abed9bf253 100644 --- a/test/sim/abilities/dancer.js +++ b/test/sim/abilities/dancer.js @@ -21,13 +21,13 @@ describe('Dancer', () => { assert.statStage(battle.p2.active[0], 'atk', 3); }); - it('should activate in order of lowest to highest raw speed', () => { + it('should activate in order of fastest to slowest', () => { battle = common.createBattle({ gameType: 'doubles' }, [[ - { species: 'Shedinja', level: 98, ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, - { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, + { species: 'Shedinja', ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 98, ability: 'dancer', moves: ['sleeptalk'] }, ], [ { species: 'Shedinja', ability: 'wonderguard', moves: ['fierydance'] }, - { species: 'Shedinja', ability: 'dancer', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, ]]); const [, fastDancer] = battle.p1.active; const [wwDanceSource, foeDancer] = battle.p2.active; @@ -37,23 +37,36 @@ describe('Dancer', () => { assert.fainted(foeDancer); }); - it('should activate in order of lowest to highest raw speed inside Trick Room', () => { + it('should activate in order of slowest to faster inside Trick Room', () => { battle = common.createBattle({ gameType: 'doubles' }, [[ - { species: 'Shedinja', level: 98, ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, - { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, + { species: 'Shedinja', ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 98, ability: 'dancer', moves: ['sleeptalk'] }, ], [ { species: 'Shedinja', ability: 'wonderguard', moves: ['fierydance', 'trickroom'] }, - { species: 'Shedinja', ability: 'dancer', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, ]]); - const [, fastDancer] = battle.p1.active; const [wwDanceSource, foeDancer] = battle.p2.active; - fastDancer.boostBy({ spe: 6 }); + foeDancer.boostBy({ spe: 6 }); battle.makeChoices('move sleeptalk, move sleeptalk', 'move trickroom, move sleeptalk'); battle.makeChoices('move sleeptalk, move sleeptalk', 'move fierydance 1, move sleeptalk'); assert.fainted(wwDanceSource); assert.fainted(foeDancer); }); + it(`should copy a move that was called by Instruct`, () => { + battle = common.createBattle({ gameType: 'doubles' }, [[ + { species: 'oricoriopau', ability: 'dancer', moves: ['instruct'] }, + { species: 'volcarona', moves: ['fierydance'] }, + ], [ + { species: 'shedinja', item: 'focussash', ability: 'wonderguard', moves: ['sleeptalk'] }, + { species: 'shedinja', item: 'focussash', ability: 'wonderguard', moves: ['sleeptalk'] }, + ]]); + + battle.makeChoices('move instruct -2, move fierydance 1', 'auto'); + assert.fainted(battle.p2.active[0]); + assert.fainted(battle.p2.active[1]); + }); + it(`should not copy a move that was blocked by Protect`, () => { battle = common.createBattle([[ { species: 'Oricorio', ability: 'dancer', moves: ['protect'] }, @@ -183,4 +196,58 @@ describe('Dancer', () => { assert.equal(fletchinder.boosts.atk, -2); assert.equal(squawkabilly.boosts.atk, -4); }); + + it('should activate after Eject Button', () => { + battle = common.createBattle({ gameType: 'doubles' }, [[ + { species: 'oricoriopau', ability: 'dancer', moves: ['sleeptalk'] }, + { species: 'volcarona', moves: ['fierydance'] }, + ], [ + { species: 'fletchinder', item: 'ejectbutton', moves: ['sleeptalk'] }, + { species: 'squawkabilly', moves: ['sleeptalk'] }, + { species: 'suicune', moves: ['sleeptalk'] }, + ]]); + const suicune = battle.p2.pokemon[2]; + battle.makeChoices('move sleeptalk, move fierydance 1', 'move sleeptalk, move sleeptalk'); + battle.makeChoices(); + assert.notEqual(suicune.hp, suicune.fullHP); + }); +}); + +describe('[Gen 7] Dancer', () => { + afterEach(() => { + battle.destroy(); + }); + + it('should activate in order of lowest to highest raw speed', () => { + battle = common.gen(7).createBattle({ gameType: 'doubles' }, [[ + { species: 'Shedinja', level: 98, ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, + ], [ + { species: 'Shedinja', ability: 'wonderguard', moves: ['fierydance'] }, + { species: 'Shedinja', ability: 'dancer', moves: ['sleeptalk'] }, + ]]); + const [, fastDancer] = battle.p1.active; + const [wwDanceSource, foeDancer] = battle.p2.active; + fastDancer.boostBy({ spe: 6 }); + battle.makeChoices('move sleeptalk, move sleeptalk', 'move fierydance 1, move sleeptalk'); + assert.fainted(wwDanceSource); + assert.fainted(foeDancer); + }); + + it('should activate in order of lowest to highest raw speed inside Trick Room', () => { + battle = common.gen(7).createBattle({ gameType: 'doubles' }, [[ + { species: 'Shedinja', level: 98, ability: 'dancer', item: 'focussash', moves: ['sleeptalk'] }, + { species: 'Shedinja', level: 99, ability: 'dancer', moves: ['sleeptalk'] }, + ], [ + { species: 'Shedinja', ability: 'wonderguard', moves: ['fierydance', 'trickroom'] }, + { species: 'Shedinja', ability: 'dancer', moves: ['sleeptalk'] }, + ]]); + const [, fastDancer] = battle.p1.active; + const [wwDanceSource, foeDancer] = battle.p2.active; + fastDancer.boostBy({ spe: 6 }); + battle.makeChoices('move sleeptalk, move sleeptalk', 'move trickroom, move sleeptalk'); + battle.makeChoices('move sleeptalk, move sleeptalk', 'move fierydance 1, move sleeptalk'); + assert.fainted(wwDanceSource); + assert.fainted(foeDancer); + }); }); diff --git a/test/sim/moves/instruct.js b/test/sim/moves/instruct.js index 7eaf126696..886b99927d 100644 --- a/test/sim/moves/instruct.js +++ b/test/sim/moves/instruct.js @@ -18,6 +18,19 @@ describe(`Instruct`, () => { assert.equal(battle.p1.active[0].boosts.def, 2); }); + it(`should ignore moves called by Dancer`, () => { + battle = common.createBattle([ + [{ species: "Murkrow", ability: "prankster", moves: ['aquastep', 'instruct'] }], + [{ species: "Oricorio", ability: "dancer", moves: ['stockpile'] }], + ]); + battle.makeChoices(); + battle.makeChoices('move instruct', 'auto'); + const oricorio = battle.p2.active[0]; + assert.equal(oricorio.boosts.def, 3); + assert.equal(oricorio.boosts.spd, 3); + assert.equal(oricorio.boosts.spe, 1); + }); + it(`should not trigger AfterMove effects of the instructed move for the Instruct user`, () => { battle = common.createBattle([[ { species: "Swalot", moves: ['stockpile', 'spitup'] },