This commit is contained in:
André Bastos Dias
2026-07-18 13:17:02 +01:00
committed by GitHub
9 changed files with 142 additions and 130 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -433,6 +433,7 @@ export interface EventMethods {
onAfterMoveSecondarySelfPriority?: number;
onAfterMoveSelfPriority?: number;
onAfterSetStatusPriority?: number;
onAnyAfterMovePriority?: number;
onAnyBasePowerPriority?: number;
onAnyInvulnerabilityPriority?: number;
onAnyModifyAccuracyPriority?: number;

View File

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

View File

@@ -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'] },