mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
732 lines
26 KiB
TypeScript
732 lines
26 KiB
TypeScript
export const Scripts: ModdedBattleScriptsData = {
|
|
gen: 9,
|
|
init() {
|
|
// Since twoturnmove isn't currently implemented using linked volatiles,
|
|
// patch related moves so that 'twoturnmove' and e.g. 'skullbash' end simultaneously.
|
|
const removeTwoTurnMove = function (target: Pokemon) {
|
|
// Cannot use target.removeVolatile, since it would cause stack overflow.
|
|
delete target.volatiles['twoturnmove'];
|
|
};
|
|
for (const id in this.data.Moves) {
|
|
if (this.data.Moves[id].flags['charge'] && id !== 'skydrop') {
|
|
this.modData("Moves", id).condition ||= {};
|
|
if ('onEnd' in this.modData("Moves", id).condition) throw new Error(`onEnd needs manual override for move ${id}`);
|
|
this.modData("Moves", id).condition.onEnd = removeTwoTurnMove;
|
|
}
|
|
}
|
|
},
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
// Linked mod
|
|
const { linkIndex, linkedMoves } = action.pokemon.queryLinkMove(action.move);
|
|
if (linkIndex >= 0 && action.pokemon.getCanLinkMove(action.move)) {
|
|
const linkedActions = action.linked || linkedMoves;
|
|
const altMove = linkedActions[1 - linkIndex];
|
|
let thisPriority = this.dex.moves.get(linkedActions[linkIndex].id).priority;
|
|
thisPriority = this.singleEvent('ModifyPriority', linkedActions[linkIndex], null, action.pokemon, null, null, thisPriority);
|
|
thisPriority = this.runEvent('ModifyPriority', action.pokemon, null, linkedActions[linkIndex], thisPriority);
|
|
let thatPriority = this.dex.moves.get(altMove.id).priority;
|
|
thatPriority = this.singleEvent('ModifyPriority', altMove, null, action.pokemon, null, null, thatPriority);
|
|
thatPriority = this.runEvent('ModifyPriority', action.pokemon, null, altMove, thatPriority);
|
|
const 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 {
|
|
// 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;
|
|
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.getWillLockMove!() &&
|
|
!action.pokemon.getIsMoveLocked!() &&
|
|
!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);
|
|
},
|
|
getIsMoveLocked() {
|
|
// Detects active Outrage.
|
|
return !!this.volatiles['choicelock'] || !!this.volatiles['lockedmove'];
|
|
},
|
|
getWillLockMove() {
|
|
// Ignores Outrage, etc, since they can miss.
|
|
return this.hasItem(['choiceband', 'choicescarf', 'choicespecs']) || this.hasAbility('gorillatactics');
|
|
},
|
|
getCanLinkMove(move) {
|
|
// @ts-expect-error modded
|
|
return !move.isZ && !move.isMax && !this.getWillLockMove() && !this.getIsMoveLocked();
|
|
},
|
|
queryLinkMove(move, ignoreDisabled) {
|
|
// @ts-expect-error modded
|
|
const linkedMoves: [ActiveMove, ActiveMove] = this.getLinkedMoves!(ignoreDisabled);
|
|
if (!linkedMoves.length) return { linkIndex: -1, linkedMoves };
|
|
return { linkIndex: linkedMoves.findIndex(x => x.id === move.id), linkedMoves };
|
|
},
|
|
},
|
|
};
|