Spaces:
Paused
Paused
| /** | |
| * 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; | |