"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var battle_exports = {}; __export(battle_exports, { Battle: () => Battle, extractChannelMessages: () => extractChannelMessages }); module.exports = __toCommonJS(battle_exports); var import_dex = require("./dex"); var import_teams = require("./teams"); var import_field = require("./field"); var import_pokemon = require("./pokemon"); var import_prng = require("./prng"); var import_side = require("./side"); var import_state = require("./state"); var import_battle_queue = require("./battle-queue"); var import_battle_actions = require("./battle-actions"); var import_utils = require("../lib/utils"); /** * Simulator Battle * Pokemon Showdown - http://pokemonshowdown.com/ * * This file is where the battle simulation itself happens. * * The most important part of the simulation is the event system: * see the `runEvent` function definition for details. * * General battle mechanics are in `battle-actions`; move-specific, * item-specific, etc mechanics are in the corresponding file in * `data`. * * @license MIT */ const splitRegex = /^\|split\|p([1234])\n(.*)\n(.*)|.+/gm; function extractChannelMessages(message, channelIds) { const channelIdSet = new Set(channelIds); const channelMessages = { [-1]: [], 0: [], 1: [], 2: [], 3: [], 4: [] }; for (const [lineMatch, playerMatch, secretMessage, sharedMessage] of message.matchAll(splitRegex)) { const player = playerMatch ? parseInt(playerMatch) : 0; for (const channelId of channelIdSet) { let line = lineMatch; if (player) { line = channelId === -1 || player === channelId ? secretMessage : sharedMessage; if (!line) continue; } channelMessages[channelId].push(line); } } return channelMessages; } class Battle { constructor(options) { this.toID = import_dex.toID; this.log = []; this.add("t:", Math.floor(Date.now() / 1e3)); const format = options.format || import_dex.Dex.formats.get(options.formatid, true); this.format = format; this.dex = import_dex.Dex.forFormat(format); this.gen = this.dex.gen; this.ruleTable = this.dex.formats.getRuleTable(format); this.trunc = this.dex.trunc; this.clampIntRange = import_utils.Utils.clampIntRange; for (const i in this.dex.data.Scripts) { const entry = this.dex.data.Scripts[i]; if (typeof entry === "function") this[i] = entry; } if (format.battle) Object.assign(this, format.battle); this.id = ""; this.debugMode = format.debug || !!options.debug; this.forceRandomChance = this.debugMode && typeof options.forceRandomChance === "boolean" ? options.forceRandomChance : null; this.deserialized = !!options.deserialized; this.strictChoices = !!options.strictChoices; this.formatData = this.initEffectState({ id: format.id }); this.gameType = format.gameType || "singles"; this.field = new import_field.Field(this); this.sides = Array(format.playerCount).fill(null); this.activePerHalf = this.gameType === "triples" ? 3 : format.playerCount > 2 || this.gameType === "doubles" ? 2 : 1; this.prng = options.prng || new import_prng.PRNG(options.seed || void 0); this.prngSeed = this.prng.startingSeed; this.rated = options.rated || !!options.rated; this.reportExactHP = !!format.debug; this.reportPercentages = false; this.supportCancel = false; this.queue = new import_battle_queue.BattleQueue(this); this.actions = new import_battle_actions.BattleActions(this); this.faintQueue = []; this.inputLog = []; this.messageLog = []; this.sentLogPos = 0; this.sentEnd = false; this.requestState = ""; this.turn = 0; this.midTurn = false; this.started = false; this.ended = false; this.effect = { id: "" }; this.effectState = this.initEffectState({ id: "" }); this.event = { id: "" }; this.events = null; this.eventDepth = 0; this.activeMove = null; this.activePokemon = null; this.activeTarget = null; this.lastMove = null; this.lastMoveLine = -1; this.lastSuccessfulMoveThisTurn = null; this.lastDamage = 0; this.effectOrder = 0; this.quickClawRoll = false; this.speedOrder = []; for (let i = 0; i < this.activePerHalf * 2; i++) { this.speedOrder.push(i); } this.teamGenerator = null; this.hints = /* @__PURE__ */ new Set(); this.NOT_FAIL = ""; this.HIT_SUBSTITUTE = 0; this.FAIL = false; this.SILENT_FAIL = null; this.send = options.send || (() => { }); const inputOptions = { formatid: options.formatid, seed: this.prngSeed }; if (this.rated) inputOptions.rated = this.rated; if (typeof __version !== "undefined") { if (__version.head) { this.inputLog.push(`>version ${__version.head}`); } if (__version.origin) { this.inputLog.push(`>version-origin ${__version.origin}`); } } this.inputLog.push(`>start ` + JSON.stringify(inputOptions)); this.add("gametype", this.gameType); for (const rule of this.ruleTable.keys()) { if ("+*-!".includes(rule.charAt(0))) continue; const subFormat = this.dex.formats.get(rule); if (subFormat.exists) { const hasEventHandler = Object.keys(subFormat).some( // skip event handlers that are handled elsewhere (val) => val.startsWith("on") && ![ "onBegin", "onTeamPreview", "onBattleStart", "onValidateRule", "onValidateTeam", "onChangeSet", "onValidateSet" ].includes(val) ); if (hasEventHandler) this.field.addPseudoWeather(rule); } } const sides = ["p1", "p2", "p3", "p4"]; for (const side of sides) { if (options[side]) { this.setPlayer(side, options[side]); } } } toJSON() { return import_state.State.serializeBattle(this); } static fromJSON(serialized) { return import_state.State.deserializeBattle(serialized); } get p1() { return this.sides[0]; } get p2() { return this.sides[1]; } get p3() { return this.sides[2]; } get p4() { return this.sides[3]; } toString() { return `Battle: ${this.format}`; } random(m, n) { return this.prng.random(m, n); } randomChance(numerator, denominator) { if (this.forceRandomChance !== null) return this.forceRandomChance; return this.prng.randomChance(numerator, denominator); } sample(items) { return this.prng.sample(items); } /** Note that passing `undefined` resets to the starting seed, but `null` will roll a new seed */ resetRNG(seed = this.prngSeed) { this.prng = new import_prng.PRNG(seed); this.add("message", "The battle's RNG was reset."); } suppressingAbility(target) { return this.activePokemon && this.activePokemon.isActive && (this.activePokemon !== target || this.gen < 8) && this.activeMove && this.activeMove.ignoreAbility && !target?.hasItem("Ability Shield"); } setActiveMove(move, pokemon, target) { this.activeMove = move || null; this.activePokemon = pokemon || null; this.activeTarget = target || pokemon || null; } clearActiveMove(failed) { if (this.activeMove) { if (!failed) { this.lastMove = this.activeMove; } this.activeMove = null; this.activePokemon = null; this.activeTarget = null; } } updateSpeed() { for (const pokemon of this.getAllActive()) { pokemon.updateSpeed(); } } /** * The default sort order for actions, but also event listeners. * * 1. Order, low to high (default last) * 2. Priority, high to low (default 0) * 3. Speed, high to low (default 0) * 4. SubOrder, low to high (default 0) * 5. EffectOrder, low to high (default 0) * * Doesn't reference `this` so doesn't need to be bound. */ comparePriority(a, b) { return -((b.order || 4294967296) - (a.order || 4294967296)) || (b.priority || 0) - (a.priority || 0) || (b.speed || 0) - (a.speed || 0) || -((b.subOrder || 0) - (a.subOrder || 0)) || -((b.effectOrder || 0) - (a.effectOrder || 0)) || 0; } static compareRedirectOrder(a, b) { return (b.priority || 0) - (a.priority || 0) || (b.speed || 0) - (a.speed || 0) || (a.effectHolder?.abilityState && b.effectHolder?.abilityState ? -(b.effectHolder.abilityState.effectOrder - a.effectHolder.abilityState.effectOrder) : 0) || 0; } static compareLeftToRightOrder(a, b) { return -((b.order || 4294967296) - (a.order || 4294967296)) || (b.priority || 0) - (a.priority || 0) || -((b.index || 0) - (a.index || 0)) || 0; } /** Sort a list, resolving speed ties the way the games do. */ speedSort(list, comparator = this.comparePriority) { if (list.length < 2) return; let sorted = 0; while (sorted + 1 < list.length) { let nextIndexes = [sorted]; for (let i = sorted + 1; i < list.length; i++) { const delta = comparator(list[nextIndexes[0]], list[i]); if (delta < 0) continue; if (delta > 0) nextIndexes = [i]; if (delta === 0) nextIndexes.push(i); } for (let i = 0; i < nextIndexes.length; i++) { const index = nextIndexes[i]; if (index !== sorted + i) { [list[sorted + i], list[index]] = [list[index], list[sorted + i]]; } } if (nextIndexes.length > 1) { this.prng.shuffle(list, sorted, sorted + nextIndexes.length); } sorted += nextIndexes.length; } } /** * Runs an event with no source on each Pokémon on the field, in Speed order. */ eachEvent(eventid, effect, relayVar) { const actives = this.getAllActive(); if (!effect && this.effect) effect = this.effect; this.speedSort(actives, (a, b) => b.speed - a.speed); for (const pokemon of actives) { this.runEvent(eventid, pokemon, null, effect, relayVar); } if (eventid === "Weather" && this.gen >= 7) { this.eachEvent("Update"); } } /** * Runs an event with no source on each effect on the field, in Speed order. * * Unlike `eachEvent`, this contains a lot of other handling and is only intended for * the 'Residual' and 'SwitchIn' events. */ fieldEvent(eventid, targets) { const callbackName = `on${eventid}`; let getKey; if (eventid === "Residual") { getKey = "duration"; } let handlers = this.findFieldEventHandlers(this.field, `onField${eventid}`, getKey); for (const side of this.sides) { if (side.n < 2 || !side.allySide) { handlers = handlers.concat(this.findSideEventHandlers(side, `onSide${eventid}`, getKey)); } for (const active of side.active) { if (!active) continue; if (eventid === "SwitchIn") { handlers = handlers.concat(this.findPokemonEventHandlers(active, `onAny${eventid}`)); } if (targets && !targets.includes(active)) continue; handlers = handlers.concat(this.findPokemonEventHandlers(active, callbackName, getKey)); handlers = handlers.concat(this.findSideEventHandlers(side, callbackName, void 0, active)); handlers = handlers.concat(this.findFieldEventHandlers(this.field, callbackName, void 0, active)); handlers = handlers.concat(this.findBattleEventHandlers(callbackName, getKey, active)); } } this.speedSort(handlers); while (handlers.length) { const handler = handlers[0]; handlers.shift(); const effect = handler.effect; if (handler.effectHolder.fainted) { if (!handler.state?.isSlotCondition) continue; } if (eventid === "Residual" && handler.end && handler.state?.duration) { handler.state.duration--; if (!handler.state.duration) { const endCallArgs = handler.endCallArgs || [handler.effectHolder, effect.id]; handler.end.call(...endCallArgs); if (this.ended) return; continue; } } let handlerEventid = eventid; if (handler.effectHolder.sideConditions) handlerEventid = `Side${eventid}`; if (handler.effectHolder.pseudoWeather) handlerEventid = `Field${eventid}`; if (handler.callback) { this.singleEvent(handlerEventid, effect, handler.state, handler.effectHolder, null, null, void 0, handler.callback); } this.faintMessages(); if (this.ended) return; } } /** The entire event system revolves around this function and runEvent. */ singleEvent(eventid, effect, state, target, source, sourceEffect, relayVar, customCallback) { if (this.eventDepth >= 8) { this.add("message", "STACK LIMIT EXCEEDED"); this.add("message", "PLEASE REPORT IN BUG THREAD"); this.add("message", "Event: " + eventid); this.add("message", "Parent event: " + this.event.id); throw new Error("Stack overflow"); } if (this.log.length - this.sentLogPos > 1e3) { this.add("message", "LINE LIMIT EXCEEDED"); this.add("message", "PLEASE REPORT IN BUG THREAD"); this.add("message", "Event: " + eventid); this.add("message", "Parent event: " + this.event.id); throw new Error("Infinite loop"); } let hasRelayVar = true; if (relayVar === void 0) { relayVar = true; hasRelayVar = false; } if (effect.effectType === "Status" && target instanceof import_pokemon.Pokemon && target.status !== effect.id) { return relayVar; } if (eventid !== "Start" && eventid !== "TakeItem" && effect.effectType === "Item" && target instanceof import_pokemon.Pokemon && target.ignoringItem()) { this.debug(eventid + " handler suppressed by Embargo, Klutz or Magic Room"); return relayVar; } if (eventid !== "End" && effect.effectType === "Ability" && target instanceof import_pokemon.Pokemon && target.ignoringAbility()) { this.debug(eventid + " handler suppressed by Gastro Acid or Neutralizing Gas"); return relayVar; } if (effect.effectType === "Weather" && eventid !== "FieldStart" && eventid !== "FieldResidual" && eventid !== "FieldEnd" && this.field.suppressingWeather()) { this.debug(eventid + " handler suppressed by Air Lock"); return relayVar; } const callback = customCallback || effect[`on${eventid}`]; if (callback === void 0) return relayVar; const parentEffect = this.effect; const parentEffectState = this.effectState; const parentEvent = this.event; this.effect = effect; this.effectState = state || this.initEffectState({}); this.event = { id: eventid, target, source, effect: sourceEffect }; this.eventDepth++; const args = [target, source, sourceEffect]; if (hasRelayVar) args.unshift(relayVar); let returnVal; if (typeof callback === "function") { returnVal = callback.apply(this, args); } else { returnVal = callback; } this.eventDepth--; this.effect = parentEffect; this.effectState = parentEffectState; this.event = parentEvent; return returnVal === void 0 ? relayVar : returnVal; } /** * runEvent is the core of Pokemon Showdown's event system. * * Basic usage * =========== * * this.runEvent('Blah') * will trigger any onBlah global event handlers. * * this.runEvent('Blah', target) * will additionally trigger any onBlah handlers on the target, onAllyBlah * handlers on any active pokemon on the target's team, and onFoeBlah * handlers on any active pokemon on the target's foe's team * * this.runEvent('Blah', target, source) * will additionally trigger any onSourceBlah handlers on the source * * this.runEvent('Blah', target, source, effect) * will additionally pass the effect onto all event handlers triggered * * this.runEvent('Blah', target, source, effect, relayVar) * will additionally pass the relayVar as the first argument along all event * handlers * * You may leave any of these null. For instance, if you have a relayVar but * no source or effect: * this.runEvent('Damage', target, null, null, 50) * * Event handlers * ============== * * Items, abilities, statuses, and other effects like SR, confusion, weather, * or Trick Room can have event handlers. Event handlers are functions that * can modify what happens during an event. * * event handlers are passed: * function (target, source, effect) * although some of these can be blank. * * certain events have a relay variable, in which case they're passed: * function (relayVar, target, source, effect) * * Relay variables are variables that give additional information about the * event. For instance, the damage event has a relayVar which is the amount * of damage dealt. * * If a relay variable isn't passed to runEvent, there will still be a secret * relayVar defaulting to `true`, but it won't get passed to any event * handlers. * * After an event handler is run, its return value helps determine what * happens next: * 1. If the return value isn't `undefined`, relayVar is set to the return * value * 2. If relayVar is falsy, no more event handlers are run * 3. Otherwise, if there are more event handlers, the next one is run and * we go back to step 1. * 4. Once all event handlers are run (or one of them results in a falsy * relayVar), relayVar is returned by runEvent * * As a shortcut, an event handler that isn't a function will be interpreted * as a function that returns that value. * * You can have return values mean whatever you like, but in general, we * follow the convention that returning `false` or `null` means * stopping or interrupting the event. * * For instance, returning `false` from a TrySetStatus handler means that * the pokemon doesn't get statused. * * If a failed event usually results in a message like "But it failed!" * or "It had no effect!", returning `null` will suppress that message and * returning `false` will display it. Returning `null` is useful if your * event handler already gave its own custom failure message. * * Returning `undefined` means "don't change anything" or "keep going". * A function that does nothing but return `undefined` is the equivalent * of not having an event handler at all. * * Returning a value means that that value is the new `relayVar`. For * instance, if a Damage event handler returns 50, the damage event * will deal 50 damage instead of whatever it was going to deal before. * * Useful values * ============= * * In addition to all the methods and attributes of Dex, Battle, and * Scripts, event handlers have some additional values they can access: * * this.effect: * the Effect having the event handler * this.effectState: * the data store associated with the above Effect. This is a plain Object * and you can use it to store data for later event handlers. * this.effectState.target: * the Pokemon, Side, or Battle that the event handler's effect was * attached to. * this.event.id: * the event ID * this.event.target, this.event.source, this.event.effect: * the target, source, and effect of the event. These are the same * variables that are passed as arguments to the event handler, but * they're useful for functions called by the event handler. */ runEvent(eventid, target, source, sourceEffect, relayVar, onEffect, fastExit) { if (this.eventDepth >= 8) { this.add("message", "STACK LIMIT EXCEEDED"); this.add("message", "PLEASE REPORT IN BUG THREAD"); this.add("message", "Event: " + eventid); this.add("message", "Parent event: " + this.event.id); throw new Error("Stack overflow"); } if (!target) target = this; let effectSource = null; if (source instanceof import_pokemon.Pokemon) effectSource = source; const handlers = this.findEventHandlers(target, eventid, effectSource); if (onEffect) { if (!sourceEffect) throw new Error("onEffect passed without an effect"); const callback = sourceEffect[`on${eventid}`]; if (callback !== void 0) { if (Array.isArray(target)) throw new Error(""); handlers.unshift(this.resolvePriority({ effect: sourceEffect, callback, state: this.initEffectState({}), end: null, effectHolder: target }, `on${eventid}`)); } } if (["Invulnerability", "TryHit", "DamagingHit", "EntryHazard"].includes(eventid)) { handlers.sort(Battle.compareLeftToRightOrder); } else if (fastExit) { handlers.sort(Battle.compareRedirectOrder); } else { this.speedSort(handlers); } let hasRelayVar = 1; const args = [target, source, sourceEffect]; if (relayVar === void 0 || relayVar === null) { relayVar = true; hasRelayVar = 0; } else { args.unshift(relayVar); } const parentEvent = this.event; this.event = { id: eventid, target, source, effect: sourceEffect, modifier: 1 }; this.eventDepth++; let targetRelayVars = []; if (Array.isArray(target)) { if (Array.isArray(relayVar)) { targetRelayVars = relayVar; } else { for (let i = 0; i < target.length; i++) targetRelayVars[i] = true; } } for (const handler of handlers) { if (handler.index !== void 0) { if (!targetRelayVars[handler.index] && !(targetRelayVars[handler.index] === 0 && eventid === "DamagingHit")) continue; if (handler.target) { args[hasRelayVar] = handler.target; this.event.target = handler.target; } if (hasRelayVar) args[0] = targetRelayVars[handler.index]; } const effect = handler.effect; const effectHolder = handler.effectHolder; if (effect.effectType === "Status" && effectHolder.status !== effect.id) { continue; } if (effect.effectType === "Ability" && effect.flags["breakable"] && this.suppressingAbility(effectHolder)) { if (effect.flags["breakable"]) { this.debug(eventid + " handler suppressed by Mold Breaker"); continue; } if (!effect.num) { const AttackingEvents = { BeforeMove: 1, BasePower: 1, Immunity: 1, RedirectTarget: 1, Heal: 1, SetStatus: 1, CriticalHit: 1, ModifyAtk: 1, ModifyDef: 1, ModifySpA: 1, ModifySpD: 1, ModifySpe: 1, ModifyAccuracy: 1, ModifyBoost: 1, ModifyDamage: 1, ModifySecondaries: 1, ModifyWeight: 1, TryAddVolatile: 1, TryHit: 1, TryHitSide: 1, TryMove: 1, Boost: 1, DragOut: 1, Effectiveness: 1 }; if (eventid in AttackingEvents) { this.debug(eventid + " handler suppressed by Mold Breaker"); continue; } else if (eventid === "Damage" && sourceEffect && sourceEffect.effectType === "Move") { this.debug(eventid + " handler suppressed by Mold Breaker"); continue; } } } if (eventid !== "Start" && eventid !== "SwitchIn" && eventid !== "TakeItem" && effect.effectType === "Item" && effectHolder instanceof import_pokemon.Pokemon && effectHolder.ignoringItem()) { if (eventid !== "Update") { this.debug(eventid + " handler suppressed by Embargo, Klutz or Magic Room"); } continue; } else if (eventid !== "End" && effect.effectType === "Ability" && effectHolder instanceof import_pokemon.Pokemon && effectHolder.ignoringAbility()) { if (eventid !== "Update") { this.debug(eventid + " handler suppressed by Gastro Acid or Neutralizing Gas"); } continue; } if ((effect.effectType === "Weather" || eventid === "Weather") && eventid !== "Residual" && eventid !== "End" && this.field.suppressingWeather()) { this.debug(eventid + " handler suppressed by Air Lock"); continue; } let returnVal; if (typeof handler.callback === "function") { const parentEffect = this.effect; const parentEffectState = this.effectState; this.effect = handler.effect; this.effectState = handler.state || this.initEffectState({}); this.effectState.target = effectHolder; returnVal = handler.callback.apply(this, args); this.effect = parentEffect; this.effectState = parentEffectState; } else { returnVal = handler.callback; } if (returnVal !== void 0) { relayVar = returnVal; if (!relayVar || fastExit) { if (handler.index !== void 0) { targetRelayVars[handler.index] = relayVar; if (targetRelayVars.every((val) => !val)) break; } else { break; } } if (hasRelayVar) { args[0] = relayVar; } } } this.eventDepth--; if (typeof relayVar === "number" && relayVar === Math.abs(Math.floor(relayVar))) { relayVar = this.modify(relayVar, this.event.modifier); } this.event = parentEvent; return Array.isArray(target) ? targetRelayVars : relayVar; } /** * priorityEvent works just like runEvent, except it exits and returns * on the first non-undefined value instead of only on null/false. */ priorityEvent(eventid, target, source, effect, relayVar, onEffect) { return this.runEvent(eventid, target, source, effect, relayVar, onEffect, true); } resolvePriority(h, callbackName) { const handler = h; handler.order = handler.effect[`${callbackName}Order`] || false; handler.priority = handler.effect[`${callbackName}Priority`] || 0; handler.subOrder = handler.effect[`${callbackName}SubOrder`] || 0; if (!handler.subOrder) { const effectTypeOrder = { // Z-Move: 1, Condition: 2, // Slot Condition: 3, // Side Condition: 4, // Field Condition: 5, (includes weather but also terrains and pseudoweathers) Weather: 5, Format: 5, Rule: 5, Ruleset: 5, // Poison Touch: 6, (also includes Perish Body) Ability: 7, Item: 8 // Stall: 9, }; handler.subOrder = effectTypeOrder[handler.effect.effectType] || 0; if (handler.effect.effectType === "Condition") { if (handler.state?.target instanceof import_side.Side) { if (handler.state.isSlotCondition) { handler.subOrder = 3; } else { handler.subOrder = 4; } } else if (handler.state?.target instanceof import_field.Field) { handler.subOrder = 5; } } else if (handler.effect.effectType === "Ability") { if (handler.effect.name === "Poison Touch" || handler.effect.name === "Perish Body") { handler.subOrder = 6; } else if (handler.effect.name === "Stall") { handler.subOrder = 9; } } } if (callbackName.endsWith("SwitchIn") || callbackName.endsWith("RedirectTarget")) { handler.effectOrder = handler.state?.effectOrder; } if (handler.effectHolder && handler.effectHolder.getStat) { const pokemon = handler.effectHolder; handler.speed = pokemon.speed; if (callbackName.endsWith("SwitchIn")) { const fieldPositionValue = pokemon.side.n * this.sides.length + pokemon.position; handler.speed -= this.speedOrder.indexOf(fieldPositionValue) / (this.activePerHalf * 2); } } return handler; } findEventHandlers(target, eventName, source) { let handlers = []; if (Array.isArray(target)) { for (const [i, pokemon] of target.entries()) { const curHandlers = this.findEventHandlers(pokemon, eventName, source); for (const handler of curHandlers) { handler.target = pokemon; handler.index = i; } handlers = handlers.concat(curHandlers); } return handlers; } const prefixedHandlers = !["BeforeTurn", "Update", "Weather", "WeatherChange", "TerrainChange"].includes(eventName); if (target instanceof import_pokemon.Pokemon && (target.isActive || source?.isActive)) { handlers = this.findPokemonEventHandlers(target, `on${eventName}`); if (prefixedHandlers) { for (const allyActive of target.alliesAndSelf()) { handlers.push(...this.findPokemonEventHandlers(allyActive, `onAlly${eventName}`)); handlers.push(...this.findPokemonEventHandlers(allyActive, `onAny${eventName}`)); } for (const foeActive of target.foes()) { handlers.push(...this.findPokemonEventHandlers(foeActive, `onFoe${eventName}`)); handlers.push(...this.findPokemonEventHandlers(foeActive, `onAny${eventName}`)); } } target = target.side; } if (source && prefixedHandlers) { handlers.push(...this.findPokemonEventHandlers(source, `onSource${eventName}`)); } if (target instanceof import_side.Side) { for (const side of this.sides) { if (side.n >= 2 && side.allySide) break; if (side === target || side === target.allySide) { handlers.push(...this.findSideEventHandlers(side, `on${eventName}`)); } else if (prefixedHandlers) { handlers.push(...this.findSideEventHandlers(side, `onFoe${eventName}`)); } if (prefixedHandlers) handlers.push(...this.findSideEventHandlers(side, `onAny${eventName}`)); } } handlers.push(...this.findFieldEventHandlers(this.field, `on${eventName}`)); handlers.push(...this.findBattleEventHandlers(`on${eventName}`)); return handlers; } findPokemonEventHandlers(pokemon, callbackName, getKey) { const handlers = []; const status = pokemon.getStatus(); let callback = status[callbackName]; if (callback !== void 0 || getKey && pokemon.statusState[getKey]) { handlers.push(this.resolvePriority({ effect: status, callback, state: pokemon.statusState, end: pokemon.clearStatus, effectHolder: pokemon }, callbackName)); } for (const id in pokemon.volatiles) { const volatileState = pokemon.volatiles[id]; const volatile = this.dex.conditions.getByID(id); callback = volatile[callbackName]; if (callback !== void 0 || getKey && volatileState[getKey]) { handlers.push(this.resolvePriority({ effect: volatile, callback, state: volatileState, end: pokemon.removeVolatile, effectHolder: pokemon }, callbackName)); } else if (["ability", "item"].includes(volatile.id.split(":")[0])) { if (this.gen >= 5 && callbackName === "onSwitchIn" && !volatile.onAnySwitchIn) { callback = volatile.onStart; if (callback !== void 0 || getKey && volatileState[getKey]) { handlers.push(this.resolvePriority({ effect: volatile, callback, state: volatileState, end: pokemon.removeVolatile, effectHolder: pokemon }, callbackName)); } } } } const ability = pokemon.getAbility(); callback = ability[callbackName]; if (callback !== void 0 || getKey && pokemon.abilityState[getKey]) { handlers.push(this.resolvePriority({ effect: ability, callback, state: pokemon.abilityState, end: pokemon.clearAbility, effectHolder: pokemon }, callbackName)); } else if (this.gen >= 5 && callbackName === "onSwitchIn" && !ability.onAnySwitchIn) { callback = ability.onStart; if (callback !== void 0 || getKey && pokemon.abilityState[getKey]) { handlers.push(this.resolvePriority({ effect: ability, callback, state: pokemon.abilityState, end: pokemon.clearAbility, effectHolder: pokemon }, callbackName)); } } const item = pokemon.getItem(); callback = item[callbackName]; if (callback !== void 0 || getKey && pokemon.itemState[getKey]) { handlers.push(this.resolvePriority({ effect: item, callback, state: pokemon.itemState, end: pokemon.clearItem, effectHolder: pokemon }, callbackName)); } else if (this.gen >= 5 && callbackName === "onSwitchIn" && !item.onAnySwitchIn) { callback = item.onStart; if (callback !== void 0 || getKey && pokemon.itemState[getKey]) { handlers.push(this.resolvePriority({ effect: item, callback, state: pokemon.itemState, end: pokemon.clearItem, effectHolder: pokemon }, callbackName)); } } const species = pokemon.baseSpecies; callback = species[callbackName]; if (callback !== void 0) { handlers.push(this.resolvePriority({ effect: species, callback, state: pokemon.speciesState, end() { }, effectHolder: pokemon }, callbackName)); } const side = pokemon.side; for (const conditionid in side.slotConditions[pokemon.position]) { const slotConditionState = side.slotConditions[pokemon.position][conditionid]; const slotCondition = this.dex.conditions.getByID(conditionid); callback = slotCondition[callbackName]; if (callback !== void 0 || getKey && slotConditionState[getKey]) { handlers.push(this.resolvePriority({ effect: slotCondition, callback, state: slotConditionState, end: side.removeSlotCondition, endCallArgs: [side, pokemon, slotCondition.id], effectHolder: pokemon }, callbackName)); } } return handlers; } findBattleEventHandlers(callbackName, getKey, customHolder) { const handlers = []; let callback; const format = this.format; callback = format[callbackName]; if (callback !== void 0 || getKey && this.formatData[getKey]) { handlers.push(this.resolvePriority({ effect: format, callback, state: this.formatData, end: null, effectHolder: customHolder || this }, callbackName)); } if (this.events && (callback = this.events[callbackName]) !== void 0) { for (const handler of callback) { const state = handler.target.effectType === "Format" ? this.formatData : null; handlers.push({ effect: handler.target, callback: handler.callback, state, end: null, effectHolder: customHolder || this, priority: handler.priority, order: handler.order, subOrder: handler.subOrder }); } } return handlers; } findFieldEventHandlers(field, callbackName, getKey, customHolder) { const handlers = []; let callback; for (const id in field.pseudoWeather) { const pseudoWeatherState = field.pseudoWeather[id]; const pseudoWeather = this.dex.conditions.getByID(id); callback = pseudoWeather[callbackName]; if (callback !== void 0 || getKey && pseudoWeatherState[getKey]) { handlers.push(this.resolvePriority({ effect: pseudoWeather, callback, state: pseudoWeatherState, end: customHolder ? null : field.removePseudoWeather, effectHolder: customHolder || field }, callbackName)); } } const weather = field.getWeather(); callback = weather[callbackName]; if (callback !== void 0 || getKey && this.field.weatherState[getKey]) { handlers.push(this.resolvePriority({ effect: weather, callback, state: this.field.weatherState, end: customHolder ? null : field.clearWeather, effectHolder: customHolder || field }, callbackName)); } const terrain = field.getTerrain(); callback = terrain[callbackName]; if (callback !== void 0 || getKey && field.terrainState[getKey]) { handlers.push(this.resolvePriority({ effect: terrain, callback, state: field.terrainState, end: customHolder ? null : field.clearTerrain, effectHolder: customHolder || field }, callbackName)); } return handlers; } findSideEventHandlers(side, callbackName, getKey, customHolder) { const handlers = []; for (const id in side.sideConditions) { const sideConditionData = side.sideConditions[id]; const sideCondition = this.dex.conditions.getByID(id); const callback = sideCondition[callbackName]; if (callback !== void 0 || getKey && sideConditionData[getKey]) { handlers.push(this.resolvePriority({ effect: sideCondition, callback, state: sideConditionData, end: customHolder ? null : side.removeSideCondition, effectHolder: customHolder || side }, callbackName)); } } return handlers; } /** * Use this function to attach custom event handlers to a battle. See Battle#runEvent for * more information on how to write callbacks for event handlers. * * Try to use this sparingly. Most event handlers can be simply placed in a format instead. * * this.onEvent(eventid, target, callback) * will set the callback as an event handler for the target when eventid is called with the * default priority. Currently only valid formats are supported as targets but this will * eventually be expanded to support other target types. * * this.onEvent(eventid, target, priority, callback) * will set the callback as an event handler for the target when eventid is called with the * provided priority. Priority can either be a number or an object that contains the priority, * order, and subOrder for the event handler as needed (undefined keys will use default values) */ onEvent(eventid, target, ...rest) { if (!eventid) throw new TypeError("Event handlers must have an event to listen to"); if (!target) throw new TypeError("Event handlers must have a target"); if (!rest.length) throw new TypeError("Event handlers must have a callback"); if (target.effectType !== "Format") { throw new TypeError(`${target.name} is a ${target.effectType} but only Format targets are supported right now`); } let callback, priority, order, subOrder, data; if (rest.length === 1) { [callback] = rest; priority = 0; order = false; subOrder = 0; } else { [data, callback] = rest; if (typeof data === "object") { priority = data["priority"] || 0; order = data["order"] || false; subOrder = data["subOrder"] || 0; } else { priority = data || 0; order = false; subOrder = 0; } } const eventHandler = { callback, target, priority, order, subOrder }; if (!this.events) this.events = {}; const callbackName = `on${eventid}`; const eventHandlers = this.events[callbackName]; if (eventHandlers === void 0) { this.events[callbackName] = [eventHandler]; } else { eventHandlers.push(eventHandler); } } checkMoveMakesContact(move, attacker, defender, announcePads = false) { if (move.flags["contact"] && attacker.hasItem("protectivepads")) { if (announcePads) { this.add("-activate", defender, this.effect.fullname); this.add("-activate", attacker, "item: Protective Pads"); } return false; } return !!move.flags["contact"]; } getPokemon(fullname) { if (typeof fullname !== "string") fullname = fullname.fullname; for (const side of this.sides) { for (const pokemon of side.pokemon) { if (pokemon.fullname === fullname) return pokemon; } } return null; } getAllPokemon() { const pokemonList = []; for (const side of this.sides) { pokemonList.push(...side.pokemon); } return pokemonList; } getAllActive(includeFainted) { const pokemonList = []; for (const side of this.sides) { for (const pokemon of side.active) { if (pokemon && (includeFainted || !pokemon.fainted)) { pokemonList.push(pokemon); } } } return pokemonList; } makeRequest(type) { if (type) { this.requestState = type; for (const side of this.sides) { side.clearChoice(); } } else { type = this.requestState; } for (const side of this.sides) { side.activeRequest = null; } if (type === "teampreview") { const pickedTeamSize = this.ruleTable.pickedTeamSize; this.add(`teampreview${pickedTeamSize ? `|${pickedTeamSize}` : ""}`); } const requests = this.getRequests(type); for (let i = 0; i < this.sides.length; i++) { this.sides[i].emitRequest(requests[i]); } if (this.sides.every((side) => side.isChoiceDone())) { throw new Error(`Choices are done immediately after a request`); } } clearRequest() { this.requestState = ""; for (const side of this.sides) { side.activeRequest = null; side.clearChoice(); } } getRequests(type) { const requests = Array(this.sides.length).fill(null); switch (type) { case "switch": for (let i = 0; i < this.sides.length; i++) { const side = this.sides[i]; if (!side.pokemonLeft) continue; const switchTable = side.active.map((pokemon) => !!pokemon?.switchFlag); if (switchTable.some(Boolean)) { requests[i] = { forceSwitch: switchTable, side: side.getRequestData() }; } } break; case "teampreview": for (let i = 0; i < this.sides.length; i++) { const side = this.sides[i]; const maxChosenTeamSize = this.ruleTable.pickedTeamSize || void 0; requests[i] = { teamPreview: true, maxChosenTeamSize, side: side.getRequestData() }; } break; default: for (let i = 0; i < this.sides.length; i++) { const side = this.sides[i]; if (!side.pokemonLeft) continue; const activeData = side.active.map((pokemon) => pokemon?.getMoveRequestData()); requests[i] = { active: activeData, side: side.getRequestData() }; if (side.allySide) { requests[i].ally = side.allySide.getRequestData(true); } } break; } const multipleRequestsExist = requests.filter(Boolean).length >= 2; for (let i = 0; i < this.sides.length; i++) { if (requests[i]) { if (!this.supportCancel || !multipleRequestsExist) requests[i].noCancel = true; } else { requests[i] = { wait: true, side: this.sides[i].getRequestData() }; } } return requests; } tiebreak() { if (this.ended) return false; this.inputLog.push(`>tiebreak`); this.add("message", "Time's up! Going to tiebreaker..."); const notFainted = this.sides.map((side) => side.pokemon.filter((pokemon) => !pokemon.fainted).length); this.add("-message", this.sides.map((side, i) => `${side.name}: ${notFainted[i]} Pokemon left`).join("; ")); const maxNotFainted = Math.max(...notFainted); let tiedSides = this.sides.filter((side, i) => notFainted[i] === maxNotFainted); if (tiedSides.length <= 1) { return this.win(tiedSides[0]); } const hpPercentage = tiedSides.map((side) => side.pokemon.map((pokemon) => pokemon.hp / pokemon.maxhp).reduce((a, b) => a + b) * 100 / 6); this.add("-message", tiedSides.map((side, i) => `${side.name}: ${Math.round(hpPercentage[i])}% total HP left`).join("; ")); const maxPercentage = Math.max(...hpPercentage); tiedSides = tiedSides.filter((side, i) => hpPercentage[i] === maxPercentage); if (tiedSides.length <= 1) { return this.win(tiedSides[0]); } const hpTotal = tiedSides.map((side) => side.pokemon.map((pokemon) => pokemon.hp).reduce((a, b) => a + b)); this.add("-message", tiedSides.map((side, i) => `${side.name}: ${Math.round(hpTotal[i])} total HP left`).join("; ")); const maxTotal = Math.max(...hpTotal); tiedSides = tiedSides.filter((side, i) => hpTotal[i] === maxTotal); if (tiedSides.length <= 1) { return this.win(tiedSides[0]); } return this.tie(); } forceWin(side = null) { if (this.ended) return false; this.inputLog.push(side ? `>forcewin ${side}` : `>forcetie`); return this.win(side); } tie() { return this.win(); } win(side) { if (this.ended) return false; if (side && typeof side === "string") { side = this.getSide(side); } else if (!side || !this.sides.includes(side)) { side = null; } this.winner = side ? side.name : ""; this.add(""); if (side?.allySide) { this.add("win", side.name + " & " + side.allySide.name); } else if (side) { this.add("win", side.name); } else { this.add("tie"); } this.ended = true; this.requestState = ""; for (const s of this.sides) { if (s) s.activeRequest = null; } return true; } lose(side) { if (typeof side === "string") { side = this.getSide(side); } if (!side) return; if (this.gameType !== "freeforall") { return this.win(side.foe); } if (!side.pokemonLeft) return; side.pokemonLeft = 0; side.active[0]?.faint(); this.faintMessages(false, true); if (!this.ended && side.requestState) { side.emitRequest({ wait: true, side: side.getRequestData() }); side.clearChoice(); if (this.allChoicesDone()) this.commitChoices(); } return true; } canSwitch(side) { return this.possibleSwitches(side).length; } getRandomSwitchable(side) { const canSwitchIn = this.possibleSwitches(side); return canSwitchIn.length ? this.sample(canSwitchIn) : null; } possibleSwitches(side) { if (!side.pokemonLeft) return []; const canSwitchIn = []; for (let i = side.active.length; i < side.pokemon.length; i++) { const pokemon = side.pokemon[i]; if (!pokemon.fainted) { canSwitchIn.push(pokemon); } } return canSwitchIn; } swapPosition(pokemon, newPosition, attributes) { if (newPosition >= pokemon.side.active.length) { throw new Error("Invalid swap position"); } const target = pokemon.side.active[newPosition]; if (newPosition !== 1 && (!target || target.fainted)) return false; this.add("swap", pokemon, newPosition, attributes || ""); const side = pokemon.side; side.pokemon[pokemon.position] = target; side.pokemon[newPosition] = pokemon; side.active[pokemon.position] = side.pokemon[pokemon.position]; side.active[newPosition] = side.pokemon[newPosition]; if (target) target.position = pokemon.position; pokemon.position = newPosition; this.runEvent("Swap", target, pokemon); this.runEvent("Swap", pokemon, target); return true; } getAtSlot(slot) { if (!slot) return null; const side = this.sides[slot.charCodeAt(1) - 49]; const position = slot.charCodeAt(2) - 97; const positionOffset = Math.floor(side.n / 2) * side.active.length; return side.active[position - positionOffset]; } faint(pokemon, source, effect) { pokemon.faint(source, effect); } endTurn() { this.turn++; this.lastSuccessfulMoveThisTurn = null; const dynamaxEnding = []; for (const pokemon of this.getAllActive()) { if (pokemon.volatiles["dynamax"]?.turns === 3) { dynamaxEnding.push(pokemon); } } if (dynamaxEnding.length > 1) { this.updateSpeed(); this.speedSort(dynamaxEnding); } for (const pokemon of dynamaxEnding) { pokemon.removeVolatile("dynamax"); } if (this.gen === 1) { for (const pokemon of this.getAllActive()) { if (pokemon.volatiles["partialtrappinglock"]) { const target = pokemon.volatiles["partialtrappinglock"].locked; if (target.hp <= 0 || !target.volatiles["partiallytrapped"]) { delete pokemon.volatiles["partialtrappinglock"]; } } if (pokemon.volatiles["partiallytrapped"]) { const source = pokemon.volatiles["partiallytrapped"].source; if (source.hp <= 0 || !source.volatiles["partialtrappinglock"]) { delete pokemon.volatiles["partiallytrapped"]; } } } } const trappedBySide = []; const stalenessBySide = []; for (const side of this.sides) { let sideTrapped = true; let sideStaleness; for (const pokemon of side.active) { if (!pokemon) continue; pokemon.moveThisTurn = ""; pokemon.newlySwitched = false; pokemon.moveLastTurnResult = pokemon.moveThisTurnResult; pokemon.moveThisTurnResult = void 0; if (this.turn !== 1) { pokemon.usedItemThisTurn = false; pokemon.statsRaisedThisTurn = false; pokemon.statsLoweredThisTurn = false; pokemon.hurtThisTurn = null; } pokemon.maybeDisabled = false; for (const moveSlot of pokemon.moveSlots) { moveSlot.disabled = false; moveSlot.disabledSource = ""; } this.runEvent("DisableMove", pokemon); for (const moveSlot of pokemon.moveSlots) { const activeMove = this.dex.getActiveMove(moveSlot.id); this.singleEvent("DisableMove", activeMove, null, pokemon); if (activeMove.flags["cantusetwice"] && pokemon.lastMove?.id === moveSlot.id) { pokemon.disableMove(pokemon.lastMove.id); } } if (pokemon.getLastAttackedBy() && this.gen >= 7) pokemon.knownType = true; for (let i = pokemon.attackedBy.length - 1; i >= 0; i--) { const attack = pokemon.attackedBy[i]; if (attack.source.isActive) { attack.thisTurn = false; } else { pokemon.attackedBy.splice(pokemon.attackedBy.indexOf(attack), 1); } } if (this.gen >= 7 && !pokemon.terastallized) { const seenPokemon = pokemon.illusion || pokemon; const realTypeString = seenPokemon.getTypes(true).join("/"); if (realTypeString !== seenPokemon.apparentType) { this.add("-start", pokemon, "typechange", realTypeString, "[silent]"); seenPokemon.apparentType = realTypeString; if (pokemon.addedType) { this.add("-start", pokemon, "typeadd", pokemon.addedType, "[silent]"); } } } pokemon.trapped = pokemon.maybeTrapped = false; this.runEvent("TrapPokemon", pokemon); if (!pokemon.knownType || this.dex.getImmunity("trapped", pokemon)) { this.runEvent("MaybeTrapPokemon", pokemon); } if (this.gen > 2) { for (const source of pokemon.foes()) { const species = (source.illusion || source).species; if (!species.abilities) continue; for (const abilitySlot in species.abilities) { const abilityName = species.abilities[abilitySlot]; if (abilityName === source.ability) { continue; } const ruleTable = this.ruleTable; if ((ruleTable.has("+hackmons") || !ruleTable.has("obtainableabilities")) && !this.format.team) { continue; } else if (abilitySlot === "H" && species.unreleasedHidden) { continue; } const ability = this.dex.abilities.get(abilityName); if (ruleTable.has("-ability:" + ability.id)) continue; if (pokemon.knownType && !this.dex.getImmunity("trapped", pokemon)) continue; this.singleEvent("FoeMaybeTrapPokemon", ability, {}, pokemon, source); } } } if (pokemon.fainted) continue; sideTrapped = sideTrapped && pokemon.trapped; const staleness = pokemon.volatileStaleness || pokemon.staleness; if (staleness) sideStaleness = sideStaleness === "external" ? sideStaleness : staleness; pokemon.activeTurns++; } trappedBySide.push(sideTrapped); stalenessBySide.push(sideStaleness); side.faintedLastTurn = side.faintedThisTurn; side.faintedThisTurn = null; } if (this.maybeTriggerEndlessBattleClause(trappedBySide, stalenessBySide)) return; if (this.gameType === "triples" && this.sides.every((side) => side.pokemonLeft === 1)) { const actives = this.getAllActive(); if (actives.length > 1 && !actives[0].isAdjacent(actives[1])) { this.swapPosition(actives[0], 1, "[silent]"); this.swapPosition(actives[1], 1, "[silent]"); this.add("-center"); } } this.add("turn", this.turn); if (this.gameType === "multi") { for (const side of this.sides) { if (side.canDynamaxNow()) { if (this.turn === 1) { this.addSplit(side.id, ["-candynamax", side.id]); } else { this.add("-candynamax", side.id); } } } } if (this.gen === 2) this.quickClawRoll = this.randomChance(60, 256); if (this.gen === 3) this.quickClawRoll = this.randomChance(1, 5); this.makeRequest("move"); } maybeTriggerEndlessBattleClause(trappedBySide, stalenessBySide) { if (this.gen <= 1) { const noProgressPossible = this.sides.every((side) => { const foeAllGhosts = side.foe.pokemon.every((pokemon) => pokemon.fainted || pokemon.hasType("Ghost")); const foeAllTransform = side.foe.pokemon.every((pokemon) => pokemon.fainted || (this.dex.currentMod !== "gen1stadium" || pokemon.species.id !== "ditto") && // there are some subtleties such as a Mew with only Transform and auto-fail moves, // but it's unlikely to come up in a real game so there's no need to handle it pokemon.moves.every((moveid) => moveid === "transform")); return side.pokemon.every((pokemon) => pokemon.fainted || // frozen pokemon can't thaw in gen 1 without outside help pokemon.status === "frz" || pokemon.moves.every((moveid) => moveid === "transform") && foeAllTransform || pokemon.moveSlots.every((slot) => slot.pp === 0) && foeAllGhosts); }); if (noProgressPossible) { this.add("-message", `This battle cannot progress. Endless Battle Clause activated!`); return this.tie(); } } if (this.turn <= 100) return; if (this.turn >= 1e3) { this.add("message", `It is turn 1000. You have hit the turn limit!`); this.tie(); return true; } if (this.turn >= 500 && this.turn % 100 === 0 || this.turn >= 900 && this.turn % 10 === 0 || // every 10 turns past turn 900, this.turn >= 990) { const turnsLeft = 1e3 - this.turn; const turnsLeftText = turnsLeft === 1 ? `1 turn` : `${turnsLeft} turns`; this.add("bigerror", `You will auto-tie if the battle doesn't end in ${turnsLeftText} (on turn 1000).`); } if (!this.ruleTable.has("endlessbattleclause")) return; if (this.format.gameType === "freeforall") return; if (!stalenessBySide.every((s) => !!s) || !stalenessBySide.some((s) => s === "external")) return; const canSwitch = []; for (const [i, trapped] of trappedBySide.entries()) { canSwitch[i] = false; if (trapped) break; const side = this.sides[i]; for (const pokemon of side.pokemon) { if (!pokemon.fainted && !(pokemon.volatileStaleness || pokemon.staleness)) { canSwitch[i] = true; break; } } } if (canSwitch.every((s) => s)) return; const losers = []; for (const side of this.sides) { let berry = false; let cycle = false; for (const pokemon of side.pokemon) { berry = import_pokemon.RESTORATIVE_BERRIES.has((0, import_dex.toID)(pokemon.set.item)); if (["harvest", "pickup"].includes((0, import_dex.toID)(pokemon.set.ability)) || pokemon.set.moves.map(import_dex.toID).includes("recycle")) { cycle = true; } if (berry && cycle) break; } if (berry && cycle) losers.push(side); } if (losers.length === 1) { const loser = losers[0]; this.add("-message", `${loser.name}'s team started with the rudimentary means to perform restorative berry-cycling and thus loses.`); return this.win(loser.foe); } if (losers.length === this.sides.length) { this.add("-message", `Each side's team started with the rudimentary means to perform restorative berry-cycling.`); } return this.tie(); } start() { if (this.deserialized) return; if (!this.sides.every((side) => !!side)) throw new Error(`Missing sides: ${this.sides}`); if (this.started) throw new Error(`Battle already started`); const format = this.format; this.started = true; if (this.gameType === "multi") { this.sides[1].foe = this.sides[2]; this.sides[0].foe = this.sides[3]; this.sides[2].foe = this.sides[1]; this.sides[3].foe = this.sides[0]; this.sides[1].allySide = this.sides[3]; this.sides[0].allySide = this.sides[2]; this.sides[2].allySide = this.sides[0]; this.sides[3].allySide = this.sides[1]; this.sides[2].sideConditions = this.sides[0].sideConditions; this.sides[3].sideConditions = this.sides[1].sideConditions; } else { this.sides[1].foe = this.sides[0]; this.sides[0].foe = this.sides[1]; if (this.sides.length > 2) { this.sides[2].foe = this.sides[3]; this.sides[3].foe = this.sides[2]; } } for (const side of this.sides) { this.add("teamsize", side.id, side.pokemon.length); } this.add("gen", this.gen); this.add("tier", format.name); if (this.rated) { if (this.rated === "Rated battle") this.rated = true; this.add("rated", typeof this.rated === "string" ? this.rated : ""); } format.onBegin?.call(this); for (const rule of this.ruleTable.keys()) { if ("+*-!".includes(rule.charAt(0))) continue; const subFormat = this.dex.formats.get(rule); subFormat.onBegin?.call(this); } if (this.sides.some((side) => !side.pokemon[0])) { throw new Error("Battle not started: A player has an empty team."); } if (this.debugMode) { this.checkEVBalance(); } format.onTeamPreview?.call(this); for (const rule of this.ruleTable.keys()) { if ("+*-!".includes(rule.charAt(0))) continue; const subFormat = this.dex.formats.get(rule); subFormat.onTeamPreview?.call(this); } this.queue.addChoice({ choice: "start" }); this.midTurn = true; if (!this.requestState) this.turnLoop(); } restart(send) { if (!this.deserialized) throw new Error("Attempt to restart a battle which has not been deserialized"); this.send = send; } checkEVBalance() { let limitedEVs = null; for (const side of this.sides) { const sideLimitedEVs = !side.pokemon.some( (pokemon) => Object.values(pokemon.set.evs).reduce((a, b) => a + b, 0) > 510 ); if (limitedEVs === null) { limitedEVs = sideLimitedEVs; } else if (limitedEVs !== sideLimitedEVs) { this.add("bigerror", "Warning: One player isn't adhering to a 510 EV limit, and the other player is."); } } } boost(boost, target = null, source = null, effect = null, isSecondary = false, isSelf = false) { if (this.event) { target || (target = this.event.target); source || (source = this.event.source); effect || (effect = this.effect); } if (!target?.hp) return 0; if (!target.isActive) return false; if (this.gen > 5 && !target.side.foePokemonLeft()) return false; boost = this.runEvent("ChangeBoost", target, source, effect, { ...boost }); boost = target.getCappedBoost(boost); boost = this.runEvent("TryBoost", target, source, effect, { ...boost }); let success = null; let boosted = isSecondary; let boostName; for (boostName in boost) { const currentBoost = { [boostName]: boost[boostName] }; let boostBy = target.boostBy(currentBoost); let msg = "-boost"; if (boost[boostName] < 0 || target.boosts[boostName] === -6) { msg = "-unboost"; boostBy = -boostBy; } if (boostBy) { success = true; switch (effect?.id) { case "bellydrum": case "angerpoint": this.add("-setboost", target, "atk", target.boosts["atk"], "[from] " + effect.fullname); break; case "bellydrum2": this.add(msg, target, boostName, boostBy, "[silent]"); this.hint("In Gen 2, Belly Drum boosts by 2 when it fails."); break; case "zpower": this.add(msg, target, boostName, boostBy, "[zeffect]"); break; default: if (!effect) break; if (effect.effectType === "Move") { this.add(msg, target, boostName, boostBy); } else if (effect.effectType === "Item") { this.add(msg, target, boostName, boostBy, "[from] item: " + effect.name); } else { if (effect.effectType === "Ability" && !boosted) { this.add("-ability", target, effect.name, "boost"); boosted = true; } this.add(msg, target, boostName, boostBy); } break; } this.runEvent("AfterEachBoost", target, source, effect, currentBoost); } else if (effect?.effectType === "Ability") { if (isSecondary || isSelf) this.add(msg, target, boostName, boostBy); } else if (!isSecondary && !isSelf) { this.add(msg, target, boostName, boostBy); } } this.runEvent("AfterBoost", target, source, effect, boost); if (success) { if (Object.values(boost).some((x) => x > 0)) target.statsRaisedThisTurn = true; if (Object.values(boost).some((x) => x < 0)) target.statsLoweredThisTurn = true; } return success; } spreadDamage(damage, targetArray = null, source = null, effect = null, instafaint = false) { if (!targetArray) return [0]; const retVals = []; if (typeof effect === "string" || !effect) effect = this.dex.conditions.getByID(effect || ""); for (const [i, curDamage] of damage.entries()) { const target = targetArray[i]; let targetDamage = curDamage; if (!(targetDamage || targetDamage === 0)) { retVals[i] = targetDamage; continue; } if (!target || !target.hp) { retVals[i] = 0; continue; } if (!target.isActive) { retVals[i] = false; continue; } if (targetDamage !== 0) targetDamage = this.clampIntRange(targetDamage, 1); if (effect.id !== "struggle-recoil") { if (effect.effectType === "Weather" && !target.runStatusImmunity(effect.id)) { this.debug("weather immunity"); retVals[i] = 0; continue; } targetDamage = this.runEvent("Damage", target, source, effect, targetDamage, true); if (!(targetDamage || targetDamage === 0)) { this.debug("damage event failed"); retVals[i] = curDamage === true ? void 0 : targetDamage; continue; } } if (targetDamage !== 0) targetDamage = this.clampIntRange(targetDamage, 1); if (this.gen <= 1) { if (this.dex.currentMod === "gen1stadium" || !["recoil", "drain", "leechseed"].includes(effect.id) && effect.effectType !== "Status") { this.lastDamage = targetDamage; } } retVals[i] = targetDamage = target.damage(targetDamage, source, effect); if (targetDamage !== 0) target.hurtThisTurn = target.hp; if (source && effect.effectType === "Move") source.lastDamage = targetDamage; const name = effect.fullname === "tox" ? "psn" : effect.fullname; switch (effect.id) { case "partiallytrapped": this.add("-damage", target, target.getHealth, "[from] " + this.effectState.sourceEffect.fullname, "[partiallytrapped]"); break; case "powder": this.add("-damage", target, target.getHealth, "[silent]"); break; case "confused": this.add("-damage", target, target.getHealth, "[from] confusion"); break; default: if (effect.effectType === "Move" || !name) { this.add("-damage", target, target.getHealth); } else if (source && (source !== target || effect.effectType === "Ability")) { this.add("-damage", target, target.getHealth, `[from] ${name}`, `[of] ${source}`); } else { this.add("-damage", target, target.getHealth, `[from] ${name}`); } break; } if (targetDamage && effect.effectType === "Move") { if (this.gen <= 1 && effect.recoil && source) { if (this.dex.currentMod !== "gen1stadium" || target.hp > 0) { const amount = this.clampIntRange(Math.floor(targetDamage * effect.recoil[0] / effect.recoil[1]), 1); this.damage(amount, source, target, "recoil"); } } if (this.gen <= 4 && effect.drain && source) { const amount = this.clampIntRange(Math.floor(targetDamage * effect.drain[0] / effect.drain[1]), 1); if (this.gen <= 1) this.lastDamage = amount; this.heal(amount, source, target, "drain"); } if (this.gen > 4 && effect.drain && source) { const amount = Math.round(targetDamage * effect.drain[0] / effect.drain[1]); this.heal(amount, source, target, "drain"); } } } if (instafaint) { for (const [i, target] of targetArray.entries()) { if (!retVals[i] || !target) continue; if (target.hp <= 0) { this.debug(`instafaint: ${this.faintQueue.map((entry) => entry.target.name)}`); this.faintMessages(true); if (this.gen <= 2) { target.faint(); if (this.gen <= 1) { this.queue.clear(); for (const pokemon of this.getAllActive()) { if (pokemon.volatiles["bide"]?.damage) { pokemon.volatiles["bide"].damage = 0; this.hint("Desync Clause Mod activated!"); this.hint("In Gen 1, Bide's accumulated damage is reset to 0 when a Pokemon faints."); } } } } } } } return retVals; } damage(damage, target = null, source = null, effect = null, instafaint = false) { if (this.event) { target || (target = this.event.target); source || (source = this.event.source); effect || (effect = this.effect); } return this.spreadDamage([damage], [target], source, effect, instafaint)[0]; } directDamage(damage, target, source = null, effect = null) { if (this.event) { target || (target = this.event.target); source || (source = this.event.source); effect || (effect = this.effect); } if (!target?.hp) return 0; if (!damage) return 0; damage = this.clampIntRange(damage, 1); if (typeof effect === "string" || !effect) effect = this.dex.conditions.getByID(effect || ""); if (this.gen <= 1 && this.dex.currentMod !== "gen1stadium" && ["confusion", "jumpkick", "highjumpkick"].includes(effect.id)) { this.lastDamage = damage; if (target.volatiles["substitute"]) { const hint = "In Gen 1, if a Pokemon with a Substitute hurts itself due to confusion or Jump Kick/Hi Jump Kick recoil and the target"; const foe = target.side.foe.active[0]; if (foe?.volatiles["substitute"]) { foe.volatiles["substitute"].hp -= damage; if (foe.volatiles["substitute"].hp <= 0) { foe.removeVolatile("substitute"); foe.subFainted = true; } else { this.add("-activate", foe, "Substitute", "[damage]"); } this.hint(hint + " has a Substitute, the target's Substitute takes the damage."); return damage; } else { this.hint(hint + " does not have a Substitute there is no damage dealt."); return 0; } } } damage = target.damage(damage, source, effect); switch (effect.id) { case "strugglerecoil": this.add("-damage", target, target.getHealth, "[from] recoil"); break; case "confusion": this.add("-damage", target, target.getHealth, "[from] confusion"); break; default: this.add("-damage", target, target.getHealth); break; } if (target.fainted) this.faint(target); return damage; } heal(damage, target, source = null, effect = null) { if (this.event) { target || (target = this.event.target); source || (source = this.event.source); effect || (effect = this.effect); } if (effect === "drain") effect = this.dex.conditions.getByID(effect); if (damage && damage <= 1) damage = 1; damage = this.trunc(damage); damage = this.runEvent("TryHeal", target, source, effect, damage); if (!damage) return damage; if (!target?.hp) return false; if (!target.isActive) return false; if (target.hp >= target.maxhp) return false; const finalDamage = target.heal(damage, source, effect); switch (effect?.id) { case "leechseed": case "rest": this.add("-heal", target, target.getHealth, "[silent]"); break; case "drain": this.add("-heal", target, target.getHealth, "[from] drain", `[of] ${source}`); break; case "wish": break; case "zpower": this.add("-heal", target, target.getHealth, "[zeffect]"); break; default: if (!effect) break; if (effect.effectType === "Move") { this.add("-heal", target, target.getHealth); } else if (source && source !== target) { this.add("-heal", target, target.getHealth, `[from] ${effect.fullname}`, `[of] ${source}`); } else { this.add("-heal", target, target.getHealth, `[from] ${effect.fullname}`); } break; } this.runEvent("Heal", target, source, effect, finalDamage); return finalDamage; } chain(previousMod, nextMod) { if (Array.isArray(previousMod)) { previousMod = this.trunc(previousMod[0] * 4096 / previousMod[1]); } else { previousMod = this.trunc(previousMod * 4096); } if (Array.isArray(nextMod)) { nextMod = this.trunc(nextMod[0] * 4096 / nextMod[1]); } else { nextMod = this.trunc(nextMod * 4096); } return (previousMod * nextMod + 2048 >> 12) / 4096; } chainModify(numerator, denominator = 1) { const previousMod = this.trunc(this.event.modifier * 4096); if (Array.isArray(numerator)) { denominator = numerator[1]; numerator = numerator[0]; } const nextMod = this.trunc(numerator * 4096 / denominator); this.event.modifier = (previousMod * nextMod + 2048 >> 12) / 4096; } modify(value, numerator, denominator = 1) { if (Array.isArray(numerator)) { denominator = numerator[1]; numerator = numerator[0]; } const tr = this.trunc; const modifier = tr(numerator * 4096 / denominator); return tr((tr(value * modifier) + 2048 - 1) / 4096); } /** Given a table of base stats and a pokemon set, return the actual stats. */ spreadModify(baseStats, set) { const modStats = { atk: 10, def: 10, spa: 10, spd: 10, spe: 10 }; const tr = this.trunc; let statName; for (statName in modStats) { const stat = baseStats[statName]; modStats[statName] = tr(tr(2 * stat + set.ivs[statName] + tr(set.evs[statName] / 4)) * set.level / 100 + 5); } if ("hp" in baseStats) { const stat = baseStats["hp"]; modStats["hp"] = tr(tr(2 * stat + set.ivs["hp"] + tr(set.evs["hp"] / 4) + 100) * set.level / 100 + 10); } return this.natureModify(modStats, set); } natureModify(stats, set) { const tr = this.trunc; const nature = this.dex.natures.get(set.nature); let s; if (nature.plus) { s = nature.plus; const stat = this.ruleTable.has("overflowstatmod") ? Math.min(stats[s], 595) : stats[s]; stats[s] = tr(tr(stat * 110, 16) / 100); } if (nature.minus) { s = nature.minus; const stat = this.ruleTable.has("overflowstatmod") ? Math.min(stats[s], 728) : stats[s]; stats[s] = tr(tr(stat * 90, 16) / 100); } return stats; } finalModify(relayVar) { relayVar = this.modify(relayVar, this.event.modifier); this.event.modifier = 1; return relayVar; } getCategory(move) { return this.dex.moves.get(move).category || "Physical"; } randomizer(baseDamage) { const tr = this.trunc; return tr(tr(baseDamage * (100 - this.random(16))) / 100); } /** * Returns whether a proposed target for a move is valid. */ validTargetLoc(targetLoc, source, targetType) { if (targetLoc === 0) return true; const numSlots = this.activePerHalf; const sourceLoc = source.getLocOf(source); if (Math.abs(targetLoc) > numSlots) return false; const isSelf = sourceLoc === targetLoc; const isFoe = this.gameType === "freeforall" ? !isSelf : targetLoc > 0; const acrossFromTargetLoc = -(numSlots + 1 - targetLoc); const isAdjacent = targetLoc > 0 ? Math.abs(acrossFromTargetLoc - sourceLoc) <= 1 : Math.abs(targetLoc - sourceLoc) === 1; if (this.gameType === "freeforall" && targetType === "adjacentAlly") { return isAdjacent; } switch (targetType) { case "randomNormal": case "scripted": case "normal": return isAdjacent; case "adjacentAlly": return isAdjacent && !isFoe; case "adjacentAllyOrSelf": return isAdjacent && !isFoe || isSelf; case "adjacentFoe": return isAdjacent && isFoe; case "any": return !isSelf; } return false; } validTarget(target, source, targetType) { return this.validTargetLoc(source.getLocOf(target), source, targetType); } getTarget(pokemon, move, targetLoc, originalTarget) { move = this.dex.moves.get(move); let tracksTarget = move.tracksTarget; if (pokemon.hasAbility(["stalwart", "propellertail"])) tracksTarget = true; if (tracksTarget && originalTarget?.isActive) { return originalTarget; } if (move.smartTarget) { const curTarget = pokemon.getAtLoc(targetLoc); return curTarget && !curTarget.fainted ? curTarget : this.getRandomTarget(pokemon, move); } 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") { return target; } if (target.isAlly(pokemon)) { if (move.target === "adjacentAllyOrSelf" && this.gen !== 5) { return pokemon; } return target; } } if (target && !target.fainted) { return target; } } return this.getRandomTarget(pokemon, move); } getRandomTarget(pokemon, move) { move = this.dex.moves.get(move); if (["self", "all", "allySide", "allyTeam", "adjacentAllyOrSelf"].includes(move.target)) { return pokemon; } else if (move.target === "adjacentAlly") { if (this.gameType === "singles") return null; const adjacentAllies = pokemon.adjacentAllies(); return adjacentAllies.length ? this.sample(adjacentAllies) : null; } if (this.gameType === "singles") return pokemon.side.foe.active[0]; if (this.activePerHalf > 2) { if (move.target === "adjacentFoe" || move.target === "normal" || move.target === "randomNormal") { const adjacentFoes = pokemon.adjacentFoes(); if (adjacentFoes.length) return this.sample(adjacentFoes); return pokemon.side.foe.active[pokemon.side.foe.active.length - 1 - pokemon.position]; } } return pokemon.side.randomFoe() || pokemon.side.foe.active[0]; } checkFainted() { for (const side of this.sides) { for (const pokemon of side.active) { if (pokemon.fainted) { pokemon.status = "fnt"; pokemon.switchFlag = true; } } } } faintMessages(lastFirst = false, forceCheck = false, checkWin = true) { if (this.ended) return; const length = this.faintQueue.length; if (!length) { if (forceCheck && this.checkWin()) return true; return false; } if (lastFirst) { this.faintQueue.unshift(this.faintQueue[this.faintQueue.length - 1]); this.faintQueue.pop(); } let faintQueueLeft, faintData; while (this.faintQueue.length) { faintQueueLeft = this.faintQueue.length; faintData = this.faintQueue.shift(); const pokemon = faintData.target; if (!pokemon.fainted && this.runEvent("BeforeFaint", pokemon, faintData.source, faintData.effect)) { this.add("faint", pokemon); if (pokemon.side.pokemonLeft) pokemon.side.pokemonLeft--; if (pokemon.side.totalFainted < 100) pokemon.side.totalFainted++; this.runEvent("Faint", pokemon, faintData.source, faintData.effect); this.singleEvent("End", pokemon.getAbility(), pokemon.abilityState, pokemon); this.singleEvent("End", pokemon.getItem(), pokemon.itemState, pokemon); if (pokemon.regressionForme) { pokemon.baseSpecies = this.dex.species.get(pokemon.set.species || pokemon.set.name); pokemon.baseAbility = (0, import_dex.toID)(pokemon.set.ability); } pokemon.clearVolatile(false); pokemon.fainted = true; pokemon.illusion = null; pokemon.isActive = false; pokemon.isStarted = false; delete pokemon.terastallized; if (pokemon.regressionForme) { pokemon.details = pokemon.getUpdatedDetails(); pokemon.regressionForme = false; } pokemon.side.faintedThisTurn = pokemon; if (this.faintQueue.length >= faintQueueLeft) checkWin = true; } } if (this.gen <= 1) { this.queue.clear(); for (const pokemon of this.getAllActive()) { if (pokemon.volatiles["bide"]?.damage) { pokemon.volatiles["bide"].damage = 0; this.hint("Desync Clause Mod activated!"); this.hint("In Gen 1, Bide's accumulated damage is reset to 0 when a Pokemon faints."); } } } else if (this.gen <= 3 && this.gameType === "singles") { for (const pokemon of this.getAllActive()) { if (this.gen <= 2) { this.queue.cancelMove(pokemon); } else { this.queue.cancelAction(pokemon); } } } if (checkWin && this.checkWin(faintData)) return true; if (faintData && length) { this.runEvent("AfterFaint", faintData.target, faintData.source, faintData.effect, length); } return false; } checkWin(faintData) { const team1PokemonLeft = this.sides[0].pokemonLeft + (this.sides[0].allySide?.pokemonLeft || 0); const team2PokemonLeft = this.sides[1].pokemonLeft + (this.sides[1].allySide?.pokemonLeft || 0); const team3PokemonLeft = this.gameType === "freeforall" && this.sides[2].pokemonLeft; const team4PokemonLeft = this.gameType === "freeforall" && this.sides[3].pokemonLeft; if (!team1PokemonLeft && !team2PokemonLeft && !team3PokemonLeft && !team4PokemonLeft) { this.win(faintData && this.gen > 4 ? faintData.target.side : null); return true; } for (const side of this.sides) { if (!side.foePokemonLeft()) { this.win(side); return true; } } } 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; } } } 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; 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 = []; switch (action.choice) { case "start": { for (const side of this.sides) { if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length; } this.add("start"); for (const pokemon of this.getAllPokemon()) { let rawSpecies = null; if (pokemon.species.id === "zacian" && pokemon.item === "rustedsword") { rawSpecies = this.dex.species.get("Zacian-Crowned"); } else if (pokemon.species.id === "zamazenta" && pokemon.item === "rustedshield") { rawSpecies = this.dex.species.get("Zamazenta-Crowned"); } if (!rawSpecies) continue; const species = pokemon.setSpecies(rawSpecies); if (!species) continue; pokemon.baseSpecies = rawSpecies; pokemon.details = pokemon.getUpdatedDetails(); pokemon.setAbility(species.abilities["0"], null, true); pokemon.baseAbility = pokemon.ability; const behemothMove = { "Zacian-Crowned": "behemothblade", "Zamazenta-Crowned": "behemothbash" }; const ironHead = pokemon.baseMoves.indexOf("ironhead"); if (ironHead >= 0) { const move = this.dex.moves.get(behemothMove[rawSpecies.name]); pokemon.baseMoveSlots[ironHead] = { move: move.name, id: move.id, pp: move.noPPBoosts ? move.pp : move.pp * 8 / 5, maxpp: move.noPPBoosts ? move.pp : move.pp * 8 / 5, target: move.target, disabled: false, disabledSource: "", used: false }; pokemon.moveSlots = pokemon.baseMoveSlots.slice(); } } if (this.format.onBattleStart) 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); if (subFormat.onBattleStart) subFormat.onBattleStart.call(this); } for (const side of this.sides) { for (let i = 0; i < side.active.length; i++) { if (!side.pokemonLeft) { side.active[i] = side.pokemon[i]; side.active[i].fainted = true; side.active[i].hp = 0; } else { this.actions.switchIn(side.pokemon[i], i); } } } for (const pokemon of this.getAllPokemon()) { this.singleEvent("Start", this.dex.conditions.getByID(pokemon.species.id), pokemon.speciesState, pokemon); } this.midTurn = true; break; } case "move": if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; 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; return; case "pass": return; case "instaswitch": case "switch": if (action.choice === "switch" && action.pokemon.status) { this.singleEvent("CheckShow", this.dex.abilities.getByID("naturalcure"), null, action.pokemon); } if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === "pursuitfaint") { if (this.gen <= 4) { this.hint("Previously chosen switches continue in Gen 2-4 after a Pursuit target faints."); action.priority = -101; this.queue.unshift(action); break; } else { 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; 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()]); this.fieldEvent("Residual"); this.add("upkeep"); break; } 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(); this.faintMessages(); if (this.ended) return true; if (!this.queue.peek() || this.gen <= 3 && ["move", "residual"].includes(this.queue.peek().choice)) { this.checkFainted(); } else if (["megaEvo", "megaEvoX", "megaEvoY"].includes(action.choice) && this.gen === 7) { this.eachEvent("Update"); 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; 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(); 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"); if (this.gen >= 8 && (this.queue.peek()?.choice === "move" || this.queue.peek()?.choice === "runDynamax")) { this.updateSpeed(); for (const queueAction of this.queue.list) { if (queueAction.pokemon) this.getActionSpeed(queueAction); } this.queue.sort(); } return false; } /** * Generally called at the beginning of a turn, to go through the * turn one action at a time. * * If there is a mid-turn decision (like U-Turn), this will return * and be called again later to resume the turn. */ turnLoop() { this.add(""); this.add("t:", Math.floor(Date.now() / 1e3)); if (this.requestState) this.requestState = ""; if (!this.midTurn) { this.queue.insertChoice({ choice: "beforeTurn" }); this.queue.addChoice({ choice: "residual" }); this.midTurn = true; } let action; while (action = this.queue.shift()) { this.runAction(action); if (this.requestState || this.ended) return; } this.endTurn(); this.midTurn = false; this.queue.clear(); } /** * Takes a choice string passed from the client. Starts the next * turn if all required choices have been made. */ choose(sideid, input) { const side = this.getSide(sideid); if (!side.choose(input)) { if (!side.choice.error) { side.emitChoiceError(`Unknown error for choice: ${input}. If you're not using a custom client, please report this as a bug.`); } return false; } if (!side.isChoiceDone()) { side.emitChoiceError(`Incomplete choice: ${input} - missing other pokemon`); return false; } if (this.allChoicesDone()) this.commitChoices(); return true; } /** * Convenience method for easily making choices. */ makeChoices(...inputs) { if (inputs.length) { for (const [i, input] of inputs.entries()) { if (input) this.sides[i].choose(input); } } else { for (const side of this.sides) { side.autoChoose(); } } this.commitChoices(); } commitChoices() { this.updateSpeed(); const oldQueue = this.queue.list; this.queue.clear(); if (!this.allChoicesDone()) throw new Error("Not all choices done"); for (const side of this.sides) { const choice = side.getChoice(); if (choice) this.inputLog.push(`>${side.id} ${choice}`); } for (const side of this.sides) { this.queue.addChoice(side.choice.actions); } this.clearRequest(); this.queue.sort(); this.queue.list.push(...oldQueue); this.requestState = ""; for (const side of this.sides) { side.activeRequest = null; } this.turnLoop(); if (this.log.length - this.sentLogPos > 500) this.sendUpdates(); } undoChoice(sideid) { const side = this.getSide(sideid); if (!side.requestState) return; if (side.choice.cantUndo) { side.emitChoiceError(`Can't undo: A trapping/disabling effect would cause undo to leak information`); return; } side.clearChoice(); } /** * returns true if both decisions are complete */ allChoicesDone() { let totalActions = 0; for (const side of this.sides) { if (side.isChoiceDone()) { if (!this.supportCancel) side.choice.cantUndo = true; totalActions++; } } return totalActions >= this.sides.length; } hint(hint, once, side) { if (this.hints.has(hint)) return; if (side) { this.addSplit(side.id, ["-hint", hint]); } else { this.add("-hint", hint); } if (once) this.hints.add(hint); } addSplit(side, secret, shared) { this.log.push(`|split|${side}`); this.add(...secret); if (shared) { this.add(...shared); } else { this.log.push(""); } } add(...parts) { if (!parts.some((part) => typeof part === "function")) { this.log.push(`|${parts.join("|")}`); return; } let side = null; const secret = []; const shared = []; for (const part of parts) { if (typeof part === "function") { const split = part(); if (side && side !== split.side) throw new Error("Multiple sides passed to add"); side = split.side; secret.push(split.secret); shared.push(split.shared); } else { secret.push(part); shared.push(part); } } this.addSplit(side, secret, shared); } addMove(...args) { this.lastMoveLine = this.log.length; this.log.push(`|${args.join("|")}`); } attrLastMove(...args) { if (this.lastMoveLine < 0) return; if (this.log[this.lastMoveLine].startsWith("|-anim|")) { if (args.includes("[still]")) { this.log.splice(this.lastMoveLine, 1); this.lastMoveLine = -1; return; } } else if (args.includes("[still]")) { const parts = this.log[this.lastMoveLine].split("|"); parts[4] = ""; this.log[this.lastMoveLine] = parts.join("|"); } this.log[this.lastMoveLine] += `|${args.join("|")}`; } retargetLastMove(newTarget) { if (this.lastMoveLine < 0) return; const parts = this.log[this.lastMoveLine].split("|"); parts[4] = newTarget.toString(); this.log[this.lastMoveLine] = parts.join("|"); } debug(activity) { if (this.debugMode) { this.add("debug", activity); } } getDebugLog() { const channelMessages = extractChannelMessages(this.log.join("\n"), [-1]); return channelMessages[-1].join("\n"); } debugError(activity) { this.add("debug", activity); } // players getTeam(options) { let team = options.team; if (typeof team === "string") team = import_teams.Teams.unpack(team); if (team) return team; if (!options.seed) { options.seed = import_prng.PRNG.generateSeed(); } if (!this.teamGenerator) { this.teamGenerator = import_teams.Teams.getGenerator(this.format, options.seed); } else { this.teamGenerator.setSeed(options.seed); } team = this.teamGenerator.getTeam(options); return team; } showOpenTeamSheets() { if (this.turn !== 0) return; for (const side of this.sides) { const team = side.pokemon.map((pokemon) => { const set = pokemon.set; const newSet = { name: "", species: set.species, item: set.item, ability: set.ability, moves: set.moves, nature: "", gender: pokemon.gender, evs: null, ivs: null, level: set.level }; if (this.gen === 8) newSet.gigantamax = set.gigantamax; if (this.gen === 9) newSet.teraType = set.teraType; if (set.moves.some((m) => this.dex.moves.get(m).id === "hiddenpower")) newSet.hpType = set.hpType; if ((0, import_dex.toID)(set.species) === "zacian" && (0, import_dex.toID)(set.item) === "rustedsword" || (0, import_dex.toID)(set.species) === "zamazenta" && (0, import_dex.toID)(set.item) === "rustedshield") { newSet.species = import_dex.Dex.species.get(set.species + "crowned").name; const crowned = { "Zacian-Crowned": "behemothblade", "Zamazenta-Crowned": "behemothbash" }; const ironHead = set.moves.map(import_dex.toID).indexOf("ironhead"); if (ironHead >= 0) { newSet.moves[ironHead] = crowned[newSet.species]; } } return newSet; }); this.add("showteam", side.id, import_teams.Teams.pack(team)); } } setPlayer(slot, options) { let side; let didSomething = true; const slotNum = parseInt(slot[1]) - 1; if (!this.sides[slotNum]) { const team = this.getTeam(options); side = new import_side.Side(options.name || `Player ${slotNum + 1}`, this, slotNum, team); if (options.avatar) side.avatar = `${options.avatar}`; this.sides[slotNum] = side; } else { side = this.sides[slotNum]; didSomething = false; if (options.name && side.name !== options.name) { side.name = options.name; didSomething = true; } if (options.avatar && side.avatar !== `${options.avatar}`) { side.avatar = `${options.avatar}`; didSomething = true; } if (options.team) throw new Error(`Player ${slot} already has a team!`); } if (options.team && typeof options.team !== "string") { options.team = import_teams.Teams.pack(options.team); } if (!didSomething) return; this.inputLog.push(`>player ${slot} ` + JSON.stringify(options)); this.add("player", side.id, side.name, side.avatar, options.rating || ""); if (this.sides.every((playerSide) => !!playerSide) && !this.started) this.start(); } /** @deprecated */ join(slot, name, avatar, team) { this.setPlayer(slot, { name, avatar, team }); return this.getSide(slot); } sendUpdates() { if (this.sentLogPos >= this.log.length) return; this.send("update", this.log.slice(this.sentLogPos)); this.sentLogPos = this.log.length; if (!this.sentEnd && this.ended) { const log = { winner: this.winner, seed: this.prngSeed, turns: this.turn, p1: this.sides[0].name, p2: this.sides[1].name, p3: this.sides[2]?.name, p4: this.sides[3]?.name, p1team: this.sides[0].team, p2team: this.sides[1].team, p3team: this.sides[2]?.team, p4team: this.sides[3]?.team, score: [this.sides[0].pokemonLeft, this.sides[1].pokemonLeft], inputLog: this.inputLog }; if (this.sides[2]) { log.score.push(this.sides[2].pokemonLeft); } else { delete log.p3; delete log.p3team; } if (this.sides[3]) { log.score.push(this.sides[3].pokemonLeft); } else { delete log.p4; delete log.p4team; } this.send("end", JSON.stringify(log)); this.sentEnd = true; } } getSide(sideid) { return this.sides[parseInt(sideid[1]) - 1]; } /** * Currently, we treat Team Preview as turn 0, but the games start counting their turns at turn 0 * There is also overflow that occurs in Gen 8+ that affects moves like Wish / Future Sight * https://www.smogon.com/forums/threads/10352797 */ getOverflowedTurnCount() { return this.gen >= 8 ? (this.turn - 1) % 256 : this.turn - 1; } initEffectState(obj, effectOrder) { if (!obj.id) obj.id = ""; if (effectOrder !== void 0) { obj.effectOrder = effectOrder; } else if (obj.id && obj.target && (!(obj.target instanceof import_pokemon.Pokemon) || obj.target.isActive)) { obj.effectOrder = this.effectOrder++; } else { obj.effectOrder = 0; } return obj; } clearEffectState(state) { state.id = ""; for (const k in state) { if (k === "id" || k === "target") { continue; } else if (k === "effectOrder") { state.effectOrder = 0; } else { delete state[k]; } } } destroy() { this.field.destroy(); this.field = null; for (let i = 0; i < this.sides.length; i++) { if (this.sides[i]) { this.sides[i].destroy(); this.sides[i] = null; } } for (const action of this.queue.list) { delete action.pokemon; } this.queue.battle = null; this.queue = null; this.log = []; } } //# sourceMappingURL=battle.js.map