mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-21 06:37:09 -05:00
* Fix Linked last move checks At some point betweenaf6affband045fe456, Linked's concept of last absolute move changed from using the 2nd move in a link to using the 1st move, likely in an attempt to preserve the 1st move information that used to be stored for debugging purposes. However, the redefinition has impacted the mechanics of Sketch, and other moves for a long time. This commit restores the original mechanics, and gets rid of the last absolute move concept, in favor of the orthodox pokemon.lastMove. In consequence, all modded data entries that now match gen9 are also deleted. * Linked: Fix Grassy Glide priority calculation
698 lines
25 KiB
TypeScript
698 lines
25 KiB
TypeScript
export const Scripts: ModdedBattleScriptsData = {
|
|
gen: 9,
|
|
getActionSpeed(action) {
|
|
if (action.choice === 'move') {
|
|
let move = action.move;
|
|
if (action.zmove) {
|
|
const zMoveName = this.actions.getZMove(action.move, action.pokemon, true);
|
|
if (zMoveName) {
|
|
const zMove = this.dex.getActiveMove(zMoveName);
|
|
if (zMove.exists && zMove.isZ) {
|
|
move = zMove;
|
|
}
|
|
}
|
|
}
|
|
if (action.maxMove) {
|
|
const maxMoveName = this.actions.getMaxMove(action.maxMove, action.pokemon);
|
|
if (maxMoveName) {
|
|
const maxMove = this.actions.getActiveMaxMove(action.move, action.pokemon);
|
|
if (maxMove.exists && maxMove.isMax) {
|
|
move = maxMove;
|
|
}
|
|
}
|
|
}
|
|
// take priority from the base move, so abilities like Prankster only apply once
|
|
// (instead of compounding every time `getActionSpeed` is called)
|
|
let priority = this.dex.moves.get(move.id).priority;
|
|
// Linked mod
|
|
const linkedMoves: [ActiveMove, ActiveMove] = action.pokemon.getLinkedMoves();
|
|
let linkIndex = -1;
|
|
if (linkedMoves.length && !move.isZ && !move.isMax &&
|
|
(linkIndex = linkedMoves.findIndex(x => x.id === this.toID(action.move))) >= 0) {
|
|
const linkedActions = action.linked || linkedMoves;
|
|
const altMove = linkedActions[1 - linkIndex];
|
|
let thisPriority = this.singleEvent('ModifyPriority', move, null, action.pokemon, null, null, priority);
|
|
thisPriority = this.runEvent('ModifyPriority', action.pokemon, null, linkedActions[linkIndex], thisPriority);
|
|
let thatPriority = this.singleEvent('ModifyPriority', altMove, null, action.pokemon, null, null, altMove.priority);
|
|
thatPriority = this.runEvent('ModifyPriority', action.pokemon, null, altMove, thatPriority);
|
|
priority = Math.min(thisPriority, thatPriority);
|
|
action.priority = priority + action.fractionalPriority;
|
|
if (this.gen > 5) {
|
|
// Gen 6+: Quick Guard blocks moves with artificially enhanced priority.
|
|
// This also applies to Psychic Terrain.
|
|
linkedActions[linkIndex].priority = priority;
|
|
altMove.priority = priority;
|
|
}
|
|
} else {
|
|
priority = this.singleEvent('ModifyPriority', move, null, action.pokemon, null, null, priority);
|
|
priority = this.runEvent('ModifyPriority', action.pokemon, null, move, priority);
|
|
action.priority = priority + action.fractionalPriority;
|
|
// In Gen 6, Quick Guard blocks moves with artificially enhanced priority.
|
|
if (this.gen > 5) action.move.priority = priority;
|
|
}
|
|
}
|
|
|
|
if (!action.pokemon) {
|
|
action.speed = 1;
|
|
} else {
|
|
action.speed = action.pokemon.getActionSpeed();
|
|
}
|
|
},
|
|
runAction(action) {
|
|
const pokemonOriginalHP = action.pokemon?.hp;
|
|
let residualPokemon: (readonly [Pokemon, number])[] = [];
|
|
// returns whether or not we ended in a callback
|
|
switch (action.choice) {
|
|
case 'start': {
|
|
for (const side of this.sides) {
|
|
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
|
|
this.add('teamsize', side.id, side.pokemon.length);
|
|
}
|
|
|
|
this.add('start');
|
|
|
|
for (const pokemon of this.getAllPokemon()) {
|
|
this.singleEvent('BattleStart', this.dex.conditions.getByID(pokemon.species.id), pokemon.speciesState, pokemon);
|
|
}
|
|
|
|
this.format.onBattleStart?.call(this);
|
|
for (const rule of this.ruleTable.keys()) {
|
|
if ('+*-!'.includes(rule.charAt(0))) continue;
|
|
const subFormat = this.dex.formats.get(rule);
|
|
subFormat.onBattleStart?.call(this);
|
|
}
|
|
|
|
for (const side of this.sides) {
|
|
for (let i = 0; i < side.active.length; i++) {
|
|
if (!side.pokemonLeft) {
|
|
// forfeited before starting
|
|
side.active[i] = side.pokemon[i];
|
|
side.active[i].fainted = true;
|
|
side.active[i].hp = 0;
|
|
} else {
|
|
this.actions.switchIn(side.pokemon[i], i);
|
|
}
|
|
}
|
|
}
|
|
this.midTurn = true;
|
|
break;
|
|
}
|
|
|
|
case 'move':
|
|
if (!action.pokemon.isActive) return false;
|
|
if (action.pokemon.fainted) return false;
|
|
// Linked moves
|
|
// @ts-expect-error modded
|
|
if (action.linked) {
|
|
// @ts-expect-error modded
|
|
const linkedMoves: ActiveMove[] = action.linked;
|
|
for (let i = linkedMoves.length - 1; i >= 0; i--) {
|
|
const isValidTarget = this.validTargetLoc(action.targetLoc, action.pokemon, linkedMoves[i].target);
|
|
const randomTarget = this.getRandomTarget(action.pokemon, linkedMoves[i]);
|
|
const targetLoc = isValidTarget || !randomTarget ? action.targetLoc : action.pokemon.getLocOf(randomTarget);
|
|
const pseudoAction: Action = {
|
|
choice: 'move', priority: action.priority, speed: action.speed, pokemon: action.pokemon,
|
|
targetLoc, moveid: linkedMoves[i].id, move: linkedMoves[i], mega: action.mega,
|
|
order: action.order, fractionalPriority: action.fractionalPriority, originalTarget: action.originalTarget,
|
|
};
|
|
this.queue.unshift(pseudoAction);
|
|
}
|
|
return;
|
|
}
|
|
this.actions.runMove(action.move, action.pokemon, action.targetLoc, {
|
|
sourceEffect: action.sourceEffect, zMove: action.zmove,
|
|
maxMove: action.maxMove, originalTarget: action.originalTarget,
|
|
});
|
|
break;
|
|
case 'megaEvo':
|
|
this.actions.runMegaEvo(action.pokemon);
|
|
break;
|
|
case 'megaEvoX':
|
|
this.actions.runMegaEvoX?.(action.pokemon);
|
|
break;
|
|
case 'megaEvoY':
|
|
this.actions.runMegaEvoY?.(action.pokemon);
|
|
break;
|
|
case 'runDynamax':
|
|
action.pokemon.addVolatile('dynamax');
|
|
action.pokemon.side.dynamaxUsed = true;
|
|
if (action.pokemon.side.allySide) action.pokemon.side.allySide.dynamaxUsed = true;
|
|
break;
|
|
case 'terastallize':
|
|
this.actions.terastallize(action.pokemon);
|
|
break;
|
|
case 'beforeTurnMove':
|
|
if (!action.pokemon.isActive) return false;
|
|
if (action.pokemon.fainted) return false;
|
|
this.debug('before turn callback: ' + action.move.id);
|
|
const target = this.getTarget(action.pokemon, action.move, action.targetLoc);
|
|
if (!target) return false;
|
|
if (!action.move.beforeTurnCallback) throw new Error(`beforeTurnMove has no beforeTurnCallback`);
|
|
action.move.beforeTurnCallback.call(this, action.pokemon, target);
|
|
break;
|
|
case 'priorityChargeMove':
|
|
if (!action.pokemon.isActive) return false;
|
|
if (action.pokemon.fainted) return false;
|
|
this.debug('priority charge callback: ' + action.move.id);
|
|
if (!action.move.priorityChargeCallback) throw new Error(`priorityChargeMove has no priorityChargeCallback`);
|
|
action.move.priorityChargeCallback.call(this, action.pokemon);
|
|
break;
|
|
|
|
case 'event':
|
|
this.runEvent(action.event!, action.pokemon);
|
|
break;
|
|
case 'team':
|
|
if (action.index === 0) {
|
|
action.pokemon.side.pokemon = [];
|
|
}
|
|
action.pokemon.side.pokemon.push(action.pokemon);
|
|
action.pokemon.position = action.index;
|
|
// we return here because the update event would crash since there are no active pokemon yet
|
|
return;
|
|
|
|
case 'pass':
|
|
return;
|
|
case 'instaswitch':
|
|
case 'switch':
|
|
if (action.choice === 'switch' && action.pokemon.status) {
|
|
this.singleEvent('CheckShow', this.dex.abilities.getByID('naturalcure' as ID), null, action.pokemon);
|
|
}
|
|
if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') {
|
|
// a pokemon fainted from Pursuit before it could switch
|
|
if (this.gen <= 4) {
|
|
// in gen 2-4, the switch still happens
|
|
this.hint("Previously chosen switches continue in Gen 2-4 after a Pursuit target faints.");
|
|
action.priority = -101;
|
|
this.queue.unshift(action);
|
|
break;
|
|
} else {
|
|
// in gen 5+, the switch is cancelled
|
|
this.hint("A Pokemon can't switch between when it runs out of HP and when it faints");
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'revivalblessing':
|
|
action.pokemon.side.pokemonLeft++;
|
|
if (action.target.position < action.pokemon.side.active.length) {
|
|
this.queue.addChoice({
|
|
choice: 'instaswitch',
|
|
pokemon: action.target,
|
|
target: action.target,
|
|
});
|
|
}
|
|
action.target.fainted = false;
|
|
action.target.faintQueued = false;
|
|
action.target.subFainted = false;
|
|
action.target.status = '';
|
|
action.target.hp = 1; // Needed so hp functions works
|
|
action.target.sethp(action.target.maxhp / 2);
|
|
this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing');
|
|
action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing');
|
|
break;
|
|
case 'runSwitch':
|
|
this.actions.runSwitch(action.pokemon);
|
|
break;
|
|
case 'shift':
|
|
if (!action.pokemon.isActive) return false;
|
|
if (action.pokemon.fainted) return false;
|
|
this.swapPosition(action.pokemon, 1);
|
|
break;
|
|
|
|
case 'beforeTurn':
|
|
this.eachEvent('BeforeTurn');
|
|
break;
|
|
case 'residual':
|
|
this.add('');
|
|
this.clearActiveMove(true);
|
|
this.updateSpeed();
|
|
residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const);
|
|
this.fieldEvent('Residual');
|
|
if (!this.ended) this.add('upkeep');
|
|
break;
|
|
}
|
|
|
|
// phazing (Roar, etc)
|
|
for (const side of this.sides) {
|
|
for (const pokemon of side.active) {
|
|
if (pokemon.forceSwitchFlag) {
|
|
if (pokemon.hp) this.actions.dragIn(pokemon.side, pokemon.position);
|
|
pokemon.forceSwitchFlag = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.clearActiveMove();
|
|
|
|
// fainting
|
|
|
|
this.faintMessages();
|
|
if (this.ended) return true;
|
|
|
|
// switching (fainted pokemon, U-turn, Baton Pass, etc)
|
|
|
|
if (!this.queue.peek() || (this.gen <= 3 && ['move', 'residual'].includes(this.queue.peek()!.choice))) {
|
|
// in gen 3 or earlier, switching in fainted pokemon is done after
|
|
// every move, rather than only at the end of the turn.
|
|
this.checkFainted();
|
|
} else if (['megaEvo', 'megaEvoX', 'megaEvoY'].includes(action.choice) && this.gen === 7) {
|
|
this.eachEvent('Update');
|
|
// In Gen 7, the action order is recalculated for a Pokémon that mega evolves.
|
|
for (const [i, queuedAction] of this.queue.list.entries()) {
|
|
if (queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move') {
|
|
this.queue.list.splice(i, 1);
|
|
queuedAction.mega = 'done';
|
|
this.queue.insertChoice(queuedAction, true);
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
} else if (this.queue.peek()?.choice === 'instaswitch') {
|
|
return false;
|
|
}
|
|
|
|
if (this.gen >= 5 && action.choice !== 'start') {
|
|
this.eachEvent('Update');
|
|
for (const [pokemon, originalHP] of residualPokemon) {
|
|
const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp);
|
|
if (pokemon.hp && pokemon.getUndynamaxedHP() <= maxhp / 2 && originalHP > maxhp / 2) {
|
|
this.runEvent('EmergencyExit', pokemon);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (action.choice === 'runSwitch') {
|
|
const pokemon = action.pokemon;
|
|
if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP! > pokemon.maxhp / 2) {
|
|
this.runEvent('EmergencyExit', pokemon);
|
|
}
|
|
}
|
|
|
|
const switches = this.sides.map(
|
|
side => side.active.some(pokemon => pokemon && !!pokemon.switchFlag)
|
|
);
|
|
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
let reviveSwitch = false; // Used to ignore the fake switch for Revival Blessing
|
|
if (switches[i] && !this.canSwitch(this.sides[i])) {
|
|
for (const pokemon of this.sides[i].active) {
|
|
if (this.sides[i].slotConditions[pokemon.position]['revivalblessing']) {
|
|
reviveSwitch = true;
|
|
continue;
|
|
}
|
|
pokemon.switchFlag = false;
|
|
}
|
|
if (!reviveSwitch) switches[i] = false;
|
|
} else if (switches[i]) {
|
|
for (const pokemon of this.sides[i].active) {
|
|
if (
|
|
pokemon.hp && pokemon.switchFlag && pokemon.switchFlag !== 'revivalblessing' &&
|
|
!pokemon.skipBeforeSwitchOutEventFlag
|
|
) {
|
|
this.runEvent('BeforeSwitchOut', pokemon);
|
|
pokemon.skipBeforeSwitchOutEventFlag = true;
|
|
this.faintMessages(); // Pokemon may have fainted in BeforeSwitchOut
|
|
if (this.ended) return true;
|
|
if (pokemon.fainted) {
|
|
switches[i] = this.sides[i].active.some(sidePokemon => sidePokemon && !!sidePokemon.switchFlag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const playerSwitch of switches) {
|
|
if (playerSwitch) {
|
|
this.makeRequest('switch');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (this.gen < 5) this.eachEvent('Update');
|
|
|
|
const nextAction = this.queue.peek();
|
|
if (this.gen >= 8 &&
|
|
(nextAction?.choice === 'move' || nextAction?.choice === 'runDynamax') && nextAction?.pokemon !== action.pokemon) {
|
|
// In gen 8, speed is updated dynamically so update the queue's speed properties and sort it.
|
|
this.updateSpeed();
|
|
for (const queueAction of this.queue.list) {
|
|
if (queueAction.pokemon) this.getActionSpeed(queueAction);
|
|
}
|
|
this.queue.sort();
|
|
}
|
|
|
|
return false;
|
|
},
|
|
getTarget(pokemon, move, targetLoc, originalTarget) {
|
|
move = this.dex.moves.get(move);
|
|
|
|
// Delete tracksTarget stuff because it's useless in Linked anyway
|
|
|
|
// banning Dragon Darts from directly targeting itself is done in side.ts, but
|
|
// Dragon Darts can target itself if Ally Switch is used afterwards
|
|
if (move.smartTarget) {
|
|
const curTarget = pokemon.getAtLoc(targetLoc);
|
|
return curTarget && !curTarget.fainted ? curTarget : this.getRandomTarget(pokemon, move);
|
|
}
|
|
|
|
// Fails if the target is the user and the move can't target its own position
|
|
const selfLoc = pokemon.getLocOf(pokemon);
|
|
if (
|
|
['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === selfLoc &&
|
|
!pokemon.volatiles['twoturnmove'] && !pokemon.volatiles['iceball'] && !pokemon.volatiles['rollout']
|
|
) {
|
|
return move.flags['futuremove'] ? pokemon : null;
|
|
}
|
|
if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) {
|
|
const target = pokemon.getAtLoc(targetLoc);
|
|
if (target?.fainted) {
|
|
if (this.gameType === 'freeforall') {
|
|
// Target is a fainted opponent in a free-for-all battle; attack shouldn't retarget
|
|
return target;
|
|
}
|
|
if (target.isAlly(pokemon)) {
|
|
if (move.target === 'adjacentAllyOrSelf' && this.gen !== 5) {
|
|
return pokemon;
|
|
}
|
|
// Target is a fainted ally: attack shouldn't retarget
|
|
return target;
|
|
}
|
|
}
|
|
if (target && !target.fainted) {
|
|
// Target is unfainted: use selected target location
|
|
return target;
|
|
}
|
|
|
|
// Chosen target not valid,
|
|
// retarget randomly with getRandomTarget
|
|
}
|
|
return this.getRandomTarget(pokemon, move);
|
|
},
|
|
actions: {
|
|
runMove(moveOrMoveName, pokemon, targetLoc, options) {
|
|
pokemon.activeMoveActions++;
|
|
const zMove = options?.zMove;
|
|
const maxMove = options?.maxMove;
|
|
const externalMove = options?.externalMove;
|
|
const originalTarget = options?.originalTarget;
|
|
let sourceEffect = options?.sourceEffect;
|
|
let target = this.battle.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget);
|
|
let baseMove = this.dex.getActiveMove(moveOrMoveName);
|
|
const priority = baseMove.priority;
|
|
const pranksterBoosted = baseMove.pranksterBoosted;
|
|
if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) {
|
|
const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, baseMove);
|
|
if (changedMove && changedMove !== true) {
|
|
baseMove = this.dex.getActiveMove(changedMove);
|
|
baseMove.priority = priority;
|
|
if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted;
|
|
target = this.battle.getRandomTarget(pokemon, baseMove);
|
|
}
|
|
}
|
|
let move = baseMove;
|
|
if (zMove) {
|
|
move = this.getActiveZMove(baseMove, pokemon);
|
|
} else if (maxMove) {
|
|
move = this.getActiveMaxMove(baseMove, pokemon);
|
|
}
|
|
|
|
move.isExternal = externalMove;
|
|
|
|
this.battle.setActiveMove(move, pokemon, target);
|
|
|
|
/* if (pokemon.moveThisTurn) {
|
|
// THIS IS PURELY A SANITY CHECK
|
|
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
|
|
// USE this.queue.cancelMove INSTEAD
|
|
this.battle.debug(`${pokemon.id} INCONSISTENT STATE, ALREADY MOVED: ${pokemon.moveThisTurn}`);
|
|
this.battle.clearActiveMove(true);
|
|
return;
|
|
} */
|
|
const willTryMove = this.battle.runEvent('BeforeMove', pokemon, target, move);
|
|
if (!willTryMove) {
|
|
this.battle.runEvent('MoveAborted', pokemon, target, move);
|
|
this.battle.clearActiveMove(true);
|
|
// The event 'BeforeMove' could have returned false or null
|
|
// false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power
|
|
// null indicates the opposite, as the Pokemon didn't have an option to choose anything
|
|
pokemon.moveThisTurnResult = willTryMove;
|
|
return;
|
|
}
|
|
|
|
// Used exclusively for a hint later
|
|
if (move.flags['cantusetwice'] && pokemon.lastMove?.id === move.id) {
|
|
pokemon.addVolatile(move.id);
|
|
}
|
|
|
|
if (move.beforeMoveCallback) {
|
|
if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) {
|
|
this.battle.clearActiveMove(true);
|
|
pokemon.moveThisTurnResult = false;
|
|
return;
|
|
}
|
|
}
|
|
pokemon.lastDamage = 0;
|
|
let lockedMove;
|
|
if (!externalMove) {
|
|
lockedMove = this.battle.runEvent('LockMove', pokemon);
|
|
if (lockedMove === true) lockedMove = false;
|
|
if (!lockedMove) {
|
|
if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) {
|
|
this.battle.add('cant', pokemon, 'nopp', move);
|
|
this.battle.clearActiveMove(true);
|
|
pokemon.moveThisTurnResult = false;
|
|
return;
|
|
}
|
|
} else {
|
|
sourceEffect = this.dex.conditions.get('lockedmove');
|
|
}
|
|
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);
|
|
}
|
|
this.battle.add('-zpower', pokemon);
|
|
pokemon.side.zMoveUsed = true;
|
|
}
|
|
|
|
const oldActiveMove = move;
|
|
|
|
const moveDidSomething = this.useMove(baseMove, pokemon, { target, sourceEffect, zMove, maxMove });
|
|
this.battle.lastSuccessfulMoveThisTurn = moveDidSomething ? this.battle.activeMove && this.battle.activeMove.id : null;
|
|
if (this.battle.activeMove) move = this.battle.activeMove;
|
|
this.battle.singleEvent('AfterMove', move, null, pokemon, target, move);
|
|
this.battle.runEvent('AfterMove', pokemon, target, move);
|
|
if (move.flags['cantusetwice'] && pokemon.removeVolatile(move.id)) {
|
|
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();
|
|
|
|
if (this.battle.gen <= 4) {
|
|
// In gen 4, the outermost move is considered the last move for Copycat
|
|
this.battle.activeMove = oldActiveMove;
|
|
}
|
|
},
|
|
},
|
|
queue: {
|
|
resolveAction(action, midTurn = false) {
|
|
if (!action) throw new Error(`Action not passed to resolveAction`);
|
|
if (action.choice === 'pass') return [];
|
|
const actions = [action];
|
|
|
|
if (!action.side && action.pokemon) action.side = action.pokemon.side;
|
|
if (!action.move && action.moveid) action.move = this.battle.dex.getActiveMove(action.moveid);
|
|
if (!action.order) {
|
|
const orders: { [choice: string]: number } = {
|
|
team: 1,
|
|
start: 2,
|
|
instaswitch: 3,
|
|
beforeTurn: 4,
|
|
beforeTurnMove: 5,
|
|
revivalblessing: 6,
|
|
|
|
runSwitch: 101,
|
|
switch: 103,
|
|
megaEvo: 104,
|
|
megaEvoX: 104,
|
|
megaEvoY: 104,
|
|
runDynamax: 105,
|
|
terastallize: 106,
|
|
priorityChargeMove: 107,
|
|
|
|
shift: 200,
|
|
// default is 200 (for moves)
|
|
|
|
residual: 300,
|
|
};
|
|
if (action.choice in orders) {
|
|
action.order = orders[action.choice];
|
|
} else {
|
|
action.order = 200;
|
|
if (!['move', 'event'].includes(action.choice)) {
|
|
throw new Error(`Unexpected orderless action ${action.choice}`);
|
|
}
|
|
}
|
|
}
|
|
if (!midTurn) {
|
|
if (action.choice === 'move') {
|
|
if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc,
|
|
}));
|
|
}
|
|
if (action.mega && !action.pokemon.isSkyDropped()) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'megaEvo',
|
|
pokemon: action.pokemon,
|
|
}));
|
|
}
|
|
if (action.megax && !action.pokemon.isSkyDropped()) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'megaEvoX',
|
|
pokemon: action.pokemon,
|
|
}));
|
|
}
|
|
if (action.megay && !action.pokemon.isSkyDropped()) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'megaEvoY',
|
|
pokemon: action.pokemon,
|
|
}));
|
|
}
|
|
if (action.terastallize && !action.pokemon.terastallized) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'terastallize',
|
|
pokemon: action.pokemon,
|
|
}));
|
|
}
|
|
if (action.maxMove && !action.pokemon.volatiles['dynamax']) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'runDynamax',
|
|
pokemon: action.pokemon,
|
|
}));
|
|
}
|
|
if (!action.maxMove && !action.zmove && action.move.priorityChargeCallback) {
|
|
actions.unshift(...this.resolveAction({
|
|
choice: 'priorityChargeMove',
|
|
pokemon: action.pokemon,
|
|
move: action.move,
|
|
}));
|
|
}
|
|
action.fractionalPriority = this.battle.runEvent('FractionalPriority', action.pokemon, null, action.move, 0);
|
|
const linkedMoves: [ActiveMove, ActiveMove] = action.pokemon.getLinkedMoves();
|
|
if (
|
|
linkedMoves.length &&
|
|
!(action.pokemon.getItem().isChoice || action.pokemon.hasAbility('gorillatactics')) &&
|
|
!action.zmove && !action.maxMove
|
|
) {
|
|
const decisionMove = this.battle.toID(action.move);
|
|
if (linkedMoves.some(x => x.id === decisionMove)) {
|
|
action.linked = linkedMoves;
|
|
const linkedOtherMove = action.linked[1 - linkedMoves.findIndex(x => x.id === decisionMove)];
|
|
if (linkedOtherMove.beforeTurnCallback) {
|
|
this.addChoice({
|
|
choice: 'beforeTurnMove',
|
|
pokemon: action.pokemon,
|
|
move: linkedOtherMove,
|
|
targetLoc: action.targetLoc,
|
|
});
|
|
}
|
|
if (linkedOtherMove.priorityChargeCallback) {
|
|
this.addChoice({
|
|
choice: 'priorityChargeMove',
|
|
pokemon: action.pokemon,
|
|
move: linkedOtherMove,
|
|
targetLoc: action.targetLoc,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else if (['switch', 'instaswitch'].includes(action.choice)) {
|
|
if (typeof action.pokemon.switchFlag === 'string') {
|
|
action.sourceEffect = this.battle.dex.moves.get(action.pokemon.switchFlag as ID) as any;
|
|
}
|
|
action.pokemon.switchFlag = false;
|
|
}
|
|
}
|
|
|
|
const deferPriority = this.battle.gen === 7 && action.mega && action.mega !== 'done';
|
|
if (action.move) {
|
|
let target = null;
|
|
action.move = this.battle.dex.getActiveMove(action.move);
|
|
|
|
if (!action.targetLoc) {
|
|
target = this.battle.getRandomTarget(action.pokemon, action.move);
|
|
// TODO: what actually happens here?
|
|
if (target) action.targetLoc = action.pokemon.getLocOf(target);
|
|
}
|
|
action.originalTarget = action.pokemon.getAtLoc(action.targetLoc);
|
|
}
|
|
if (!deferPriority) this.battle.getActionSpeed(action);
|
|
return actions as any;
|
|
},
|
|
},
|
|
pokemon: {
|
|
moveUsed(move, targetLoc) {
|
|
this.lastMove = move;
|
|
this.moveThisTurn = move.id;
|
|
this.lastMoveTargetLoc = targetLoc;
|
|
},
|
|
getLinkedMoves(ignoreDisabled) {
|
|
const linkedMoves = this.moveSlots.slice(0, 2);
|
|
if (linkedMoves.length !== 2 || linkedMoves[0].pp <= 0 || linkedMoves[1].pp <= 0) return [];
|
|
const ret: [ActiveMove, ActiveMove] = [
|
|
this.battle.dex.getActiveMove(linkedMoves[0].id), this.battle.dex.getActiveMove(linkedMoves[1].id),
|
|
];
|
|
if (ignoreDisabled) return ret;
|
|
if (!this.ateBerry && ret.some(x => x.id === 'belch')) return [];
|
|
if (this.hasItem('assaultvest') && (ret[0].category === 'Status' || ret[1].category === 'Status')) {
|
|
return [];
|
|
}
|
|
return ret;
|
|
},
|
|
hasLinkedMove(move) {
|
|
// @ts-expect-error modded
|
|
const linkedMoves: [ActiveMove, ActiveMove] = this.getLinkedMoves(true);
|
|
if (!linkedMoves.length) return false;
|
|
return linkedMoves.some(x => x.id === move.id);
|
|
},
|
|
},
|
|
};
|