/** * 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'; /** A move action */ export interface MoveAction { /** action type */ choice: 'move' | 'beforeTurnMove' | 'priorityChargeMove'; order: 3 | 5 | 200 | 201 | 199 | 106; /** priority of the action (lower first) */ priority: number; /** fractional priority of the action (lower 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 (lower 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 (lower 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 (lower 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 (lower 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;