diff --git a/data/abilities.ts b/data/abilities.ts index 21a8a105d916..35109d184bfc 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -827,7 +827,43 @@ 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); + const action = this.queue.resolveAction({ + choice: 'move', + order: 198, + effectOrder: dancer.abilityState.effectOrder, + pokemon: dancer, + moveid: move.id, + targetLoc: dancersTargetLoc, + sourceEffect: this.dex.abilities.get('dancer'), + externalMove: true, + })[0]; + // 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 + action.speed = -dancer.getStat('spe', true, true); + this.queue.insertAction(action); + }, + 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/sim/battle-actions.ts b/sim/battle-actions.ts index 3385af9b8a82..955d948fc675 100644 --- a/sim/battle-actions.ts +++ b/sim/battle-actions.ts @@ -290,10 +290,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); @@ -313,36 +309,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 30f1d710c43d..84db4260ca1f 100644 --- a/sim/battle-queue.ts +++ b/sim/battle-queue.ts @@ -19,7 +19,7 @@ import type { Battle } from './battle'; export interface MoveAction { /** action type */ choice: 'move' | 'beforeTurnMove' | 'priorityChargeMove'; - order: 3 | 5 | 200 | 201 | 199 | 106; + order: 3 | 5 | 107 | 198 | 199 | 200 | 201; /** priority of the action (lower first) */ priority: number; /** fractional priority of the action (lower first) */ @@ -44,6 +44,8 @@ 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 */ @@ -374,6 +376,14 @@ export class BattleQueue { } const actions = this.resolveAction(choice, midTurn); + this.insertAction(actions); + } + + insertAction(actions: Action | Action[]) { + if (!Array.isArray(actions)) { + actions = [actions]; + } + let firstIndex = null; let lastIndex = null; for (const [i, curAction] of this.list.entries()) { diff --git a/sim/battle.ts b/sim/battle.ts index 54e6fc21ce02..20f8e3a438b9 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -2682,7 +2682,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 78279cd9009c..0a38371964f7 100644 --- a/sim/dex-conditions.ts +++ b/sim/dex-conditions.ts @@ -427,6 +427,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 591da14aa496..34e82d980ebb 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', 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'] }, + { species: 'Shedinja', level: 98, ability: 'dancer', moves: ['sleeptalk'] }, ]]); const [, fastDancer] = battle.p1.active; const [wwDanceSource, foeDancer] = battle.p2.active; @@ -37,23 +37,6 @@ describe('Dancer', () => { assert.fainted(foeDancer); }); - it('should activate in order of lowest to highest raw speed 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: '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); - }); - it(`should not copy a move that was blocked by Protect`, () => { battle = common.createBattle([[ { species: 'Oricorio', ability: 'dancer', moves: ['protect'] }, @@ -183,4 +166,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); + }); });