mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-22 01:35:31 -05:00
426 lines
12 KiB
TypeScript
426 lines
12 KiB
TypeScript
/**
|
|
* Simulator Battle Action Queue
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* The action queue is the core of the battle simulation. A rough overview of
|
|
* the core battle loop:
|
|
*
|
|
* - chosen moves/switches are added to the action queue
|
|
* - the action queue is sorted in speed/priority order
|
|
* - we go through the action queue
|
|
* - repeat
|
|
*
|
|
* @license MIT
|
|
*/
|
|
|
|
import type { Battle } from './battle';
|
|
|
|
/** Actions are sorted based on order (lower first)
|
|
* followed by priority (higher first)
|
|
* followed by speed (higher first)
|
|
* Ties are broken with Fischer-Yates.
|
|
*/
|
|
|
|
/** A move action */
|
|
export interface MoveAction {
|
|
/** action type */
|
|
choice: 'move' | 'beforeTurnMove' | 'priorityChargeMove';
|
|
order: 3 | 5 | 200 | 201 | 199 | 106;
|
|
/** priority of the action (higher first) */
|
|
priority: number;
|
|
/** fractional priority of the action (higher first) */
|
|
fractionalPriority: number;
|
|
/** speed of pokemon using move (higher first if priority tie) */
|
|
speed: number;
|
|
/** the pokemon doing the move */
|
|
pokemon: Pokemon;
|
|
/** location of the target, relative to pokemon's side */
|
|
targetLoc: number;
|
|
/** original target pokemon, for target-tracking moves */
|
|
originalTarget: Pokemon;
|
|
/** a move to use (move action only) */
|
|
moveid: ID;
|
|
/** a move to use (move action only) */
|
|
move: Move;
|
|
/** true if megaing or ultra bursting */
|
|
mega: boolean | 'done';
|
|
/** if zmoving, the name of the zmove */
|
|
zmove?: string;
|
|
/** if dynamaxed, the name of the max move */
|
|
maxMove?: string;
|
|
/** effect that called the move (eg Instruct) if any */
|
|
sourceEffect?: Effect | null;
|
|
}
|
|
|
|
/** A switch action */
|
|
export interface SwitchAction {
|
|
/** action type */
|
|
choice: 'switch' | 'instaswitch' | 'revivalblessing';
|
|
order: 3 | 6 | 103;
|
|
/** priority of the action (higher first) */
|
|
priority: number;
|
|
/** speed of pokemon switching (higher first if priority tie) */
|
|
speed: number;
|
|
/** the pokemon doing the switch */
|
|
pokemon: Pokemon;
|
|
/** pokemon to switch to */
|
|
target: Pokemon;
|
|
/** effect that called the switch (eg U */
|
|
sourceEffect: Effect | null;
|
|
}
|
|
|
|
/** A Team Preview choice action */
|
|
export interface TeamAction {
|
|
/** action type */
|
|
choice: 'team';
|
|
/** priority of the action (higher first) */
|
|
priority: number;
|
|
/** unused for this action type */
|
|
speed: 1;
|
|
/** the pokemon switching */
|
|
pokemon: Pokemon;
|
|
/** new index */
|
|
index: number;
|
|
}
|
|
|
|
/** A generic action not done by a pokemon */
|
|
export interface FieldAction {
|
|
/** action type */
|
|
choice: 'start' | 'residual' | 'pass' | 'beforeTurn';
|
|
/** priority of the action (higher first) */
|
|
priority: number;
|
|
/** unused for this action type */
|
|
speed: 1;
|
|
/** unused for this action type */
|
|
pokemon: null;
|
|
}
|
|
|
|
/** A generic action done by a single pokemon */
|
|
export interface PokemonAction {
|
|
/** action type */
|
|
choice: 'megaEvo' | 'megaEvoX' | 'megaEvoY' | 'shift' | 'runSwitch' | 'event' | 'runDynamax' | 'terastallize';
|
|
/** priority of the action (higher first) */
|
|
priority: number;
|
|
/** speed of pokemon doing action (higher first if priority tie) */
|
|
speed: number;
|
|
/** the pokemon doing action */
|
|
pokemon: Pokemon;
|
|
/** `runSwitch` only: the pokemon forcing this pokemon to switch in */
|
|
dragger?: Pokemon;
|
|
/** `event` only: the event to run */
|
|
event?: string;
|
|
}
|
|
|
|
export type Action = MoveAction | SwitchAction | TeamAction | FieldAction | PokemonAction;
|
|
|
|
/**
|
|
* An ActionChoice is like an Action and has the same structure, but it doesn't need to be fully filled out.
|
|
*
|
|
* Any Action or ChosenAction qualifies as an ActionChoice.
|
|
*
|
|
* The `[k: string]: any` part is required so TypeScript won't warn about unnecessary properties.
|
|
*/
|
|
export interface ActionChoice {
|
|
choice: string;
|
|
[k: string]: any;
|
|
}
|
|
|
|
/**
|
|
* Kind of like a priority queue, although not sorted mid-turn in Gen 1-7.
|
|
*
|
|
* Sort order is documented in `BattleQueue.comparePriority`.
|
|
*/
|
|
export class BattleQueue {
|
|
battle: Battle;
|
|
list: Action[];
|
|
constructor(battle: Battle) {
|
|
this.battle = battle;
|
|
this.list = [];
|
|
const queueScripts = battle.format.queue || battle.dex.data.Scripts.queue;
|
|
if (queueScripts) Object.assign(this, queueScripts);
|
|
}
|
|
|
|
shift() {
|
|
return this.list.shift();
|
|
}
|
|
peek(end?: boolean): Action | undefined {
|
|
return this.list[end ? this.list.length - 1 : 0];
|
|
}
|
|
push(action: Action) {
|
|
return this.list.push(action);
|
|
}
|
|
unshift(action: Action) {
|
|
return this.list.unshift(action);
|
|
}
|
|
[Symbol.iterator]() { return this.list[Symbol.iterator](); }
|
|
entries() {
|
|
return this.list.entries();
|
|
}
|
|
|
|
/**
|
|
* Takes an ActionChoice, and fills it out into a full Action object.
|
|
*
|
|
* Returns an array of Actions because some ActionChoices (like mega moves)
|
|
* resolve to two Actions (mega evolution + use move)
|
|
*/
|
|
resolveAction(action: ActionChoice, midTurn = false): Action[] {
|
|
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);
|
|
} 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;
|
|
}
|
|
|
|
/**
|
|
* Makes the passed action happen next (skipping speed order).
|
|
*/
|
|
prioritizeAction(action: MoveAction | SwitchAction, sourceEffect?: Effect) {
|
|
for (const [i, curAction] of this.list.entries()) {
|
|
if (curAction === action) {
|
|
this.list.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
action.sourceEffect = sourceEffect;
|
|
action.order = 3;
|
|
this.list.unshift(action);
|
|
}
|
|
|
|
/**
|
|
* Changes a pokemon's action, and inserts its new action
|
|
* in priority order.
|
|
*
|
|
* You'd normally want the OverrideAction event (which doesn't
|
|
* change priority order).
|
|
*/
|
|
changeAction(pokemon: Pokemon, action: ActionChoice) {
|
|
this.cancelAction(pokemon);
|
|
if (!action.pokemon) action.pokemon = pokemon;
|
|
this.insertChoice(action);
|
|
}
|
|
|
|
addChoice(choices: ActionChoice | ActionChoice[]) {
|
|
if (!Array.isArray(choices)) choices = [choices];
|
|
for (const choice of choices) {
|
|
const resolvedChoices = this.resolveAction(choice);
|
|
this.list.push(...resolvedChoices);
|
|
for (const resolvedChoice of resolvedChoices) {
|
|
if (resolvedChoice && resolvedChoice.choice === 'move' && resolvedChoice.move.id !== 'recharge') {
|
|
resolvedChoice.pokemon.side.lastSelectedMove = resolvedChoice.move.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
willAct() {
|
|
for (const action of this.list) {
|
|
if (['move', 'switch', 'instaswitch', 'shift'].includes(action.choice)) {
|
|
return action;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
willMove(pokemon: Pokemon) {
|
|
if (pokemon.fainted) return null;
|
|
for (const action of this.list) {
|
|
if (action.choice === 'move' && action.pokemon === pokemon) {
|
|
return action;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
cancelAction(pokemon: Pokemon) {
|
|
const oldLength = this.list.length;
|
|
for (let i = 0; i < this.list.length; i++) {
|
|
if (this.list[i].pokemon === pokemon) {
|
|
this.list.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
return this.list.length !== oldLength;
|
|
}
|
|
|
|
cancelMove(pokemon: Pokemon) {
|
|
for (const [i, action] of this.list.entries()) {
|
|
if (action.choice === 'move' && action.pokemon === pokemon) {
|
|
this.list.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
willSwitch(pokemon: Pokemon) {
|
|
for (const action of this.list) {
|
|
if (['switch', 'instaswitch'].includes(action.choice) && action.pokemon === pokemon) {
|
|
return action;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Inserts the passed action into the action queue when it normally
|
|
* would have happened (sorting by priority/speed), without
|
|
* re-sorting the existing actions.
|
|
*/
|
|
insertChoice(choices: ActionChoice | ActionChoice[], midTurn = false) {
|
|
if (Array.isArray(choices)) {
|
|
for (const choice of choices) {
|
|
this.insertChoice(choice);
|
|
}
|
|
return;
|
|
}
|
|
const choice = choices;
|
|
|
|
if (choice.pokemon) {
|
|
choice.pokemon.updateSpeed();
|
|
}
|
|
const actions = this.resolveAction(choice, midTurn);
|
|
|
|
let firstIndex = null;
|
|
let lastIndex = null;
|
|
for (const [i, curAction] of this.list.entries()) {
|
|
const compared = this.battle.comparePriority(actions[0], curAction);
|
|
if (compared <= 0 && firstIndex === null) {
|
|
firstIndex = i;
|
|
}
|
|
if (compared < 0) {
|
|
lastIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstIndex === null) {
|
|
this.list.push(...actions);
|
|
} else {
|
|
if (lastIndex === null) lastIndex = this.list.length;
|
|
const index = firstIndex === lastIndex ? firstIndex : this.battle.random(firstIndex, lastIndex + 1);
|
|
this.list.splice(index, 0, ...actions);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.list = [];
|
|
}
|
|
|
|
debug(action?: any): string {
|
|
if (action) {
|
|
return `${action.order || ''}:${action.priority || ''}:${action.speed || ''}:${action.subOrder || ''} - ${action.choice}${action.pokemon ? ' ' + action.pokemon : ''}${action.move ? ' ' + action.move : ''}`;
|
|
}
|
|
return this.list.map(
|
|
queueAction => this.debug(queueAction)
|
|
).join('\n') + '\n';
|
|
}
|
|
|
|
sort() {
|
|
// this.log.push('SORT ' + this.debugQueue());
|
|
this.battle.speedSort(this.list);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
export default BattleQueue;
|