Spaces:
Running
Running
; | |
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 side_exports = {}; | |
__export(side_exports, { | |
Side: () => Side | |
}); | |
module.exports = __toCommonJS(side_exports); | |
var import_utils = require("../lib/utils"); | |
var import_pokemon = require("./pokemon"); | |
var import_state = require("./state"); | |
var import_dex = require("./dex"); | |
/** | |
* Simulator Side | |
* Pokemon Showdown - http://pokemonshowdown.com/ | |
* | |
* There's a lot of ambiguity between the terms "player", "side", "team", | |
* and "half-field", which I'll try to explain here: | |
* | |
* These terms usually all mean the same thing. The exceptions are: | |
* | |
* - Multi-battle: there are 2 half-fields, 2 teams, 4 sides | |
* | |
* - Free-for-all: there are 2 half-fields, 4 teams, 4 sides | |
* | |
* "Half-field" is usually abbreviated to "half". | |
* | |
* Function naming will be very careful about which term to use. Pay attention | |
* if it's relevant to your code. | |
* | |
* @license MIT | |
*/ | |
class Side { | |
constructor(name, battle, sideNum, team) { | |
this.foe = null; | |
// set in battle.start() | |
/** Only exists in multi battle, for the allied side */ | |
this.allySide = null; | |
/** only used by Gen 1 Counter */ | |
this.lastSelectedMove = ""; | |
const sideScripts = battle.dex.data.Scripts.side; | |
if (sideScripts) | |
Object.assign(this, sideScripts); | |
this.battle = battle; | |
if (this.battle.format.side) | |
Object.assign(this, this.battle.format.side); | |
this.id = ["p1", "p2", "p3", "p4"][sideNum]; | |
this.n = sideNum; | |
this.name = name; | |
this.avatar = ""; | |
this.team = team; | |
this.pokemon = []; | |
for (const set of this.team) { | |
this.addPokemon(set); | |
} | |
switch (this.battle.gameType) { | |
case "doubles": | |
this.active = [null, null]; | |
break; | |
case "triples": | |
case "rotation": | |
this.active = [null, null, null]; | |
break; | |
default: | |
this.active = [null]; | |
} | |
this.pokemonLeft = this.pokemon.length; | |
this.faintedLastTurn = null; | |
this.faintedThisTurn = null; | |
this.totalFainted = 0; | |
this.zMoveUsed = false; | |
this.dynamaxUsed = this.battle.gen !== 8; | |
this.sideConditions = {}; | |
this.slotConditions = []; | |
for (let i = 0; i < this.active.length; i++) | |
this.slotConditions[i] = {}; | |
this.activeRequest = null; | |
this.choice = { | |
cantUndo: false, | |
error: ``, | |
actions: [], | |
forcedSwitchesLeft: 0, | |
forcedPassesLeft: 0, | |
switchIns: /* @__PURE__ */ new Set(), | |
zMove: false, | |
mega: false, | |
ultra: false, | |
dynamax: false, | |
terastallize: false | |
}; | |
this.lastMove = null; | |
} | |
toJSON() { | |
return import_state.State.serializeSide(this); | |
} | |
get requestState() { | |
if (!this.activeRequest || this.activeRequest.wait) | |
return ""; | |
if (this.activeRequest.teamPreview) | |
return "teampreview"; | |
if (this.activeRequest.forceSwitch) | |
return "switch"; | |
return "move"; | |
} | |
addPokemon(set) { | |
if (this.pokemon.length >= 24) | |
return null; | |
const newPokemon = new import_pokemon.Pokemon(set, this); | |
newPokemon.position = this.pokemon.length; | |
this.pokemon.push(newPokemon); | |
this.pokemonLeft++; | |
return newPokemon; | |
} | |
canDynamaxNow() { | |
if (this.battle.gen !== 8) | |
return false; | |
if (this.battle.gameType === "multi" && this.battle.turn % 2 !== [1, 1, 0, 0][this.n]) | |
return false; | |
return !this.dynamaxUsed; | |
} | |
/** convert a Choice into a choice string */ | |
getChoice() { | |
if (this.choice.actions.length > 1 && this.choice.actions.every((action) => action.choice === "team")) { | |
return `team ` + this.choice.actions.map((action) => action.pokemon.position + 1).join(", "); | |
} | |
return this.choice.actions.map((action) => { | |
switch (action.choice) { | |
case "move": | |
let details = ``; | |
if (action.targetLoc && this.active.length > 1) | |
details += ` ${action.targetLoc > 0 ? "+" : ""}${action.targetLoc}`; | |
if (action.mega) | |
details += action.pokemon.item === "ultranecroziumz" ? ` ultra` : ` mega`; | |
if (action.zmove) | |
details += ` zmove`; | |
if (action.maxMove) | |
details += ` dynamax`; | |
if (action.terastallize) | |
details += ` terastallize`; | |
return `move ${action.moveid}${details}`; | |
case "switch": | |
case "instaswitch": | |
case "revivalblessing": | |
return `switch ${action.target.position + 1}`; | |
case "team": | |
return `team ${action.pokemon.position + 1}`; | |
default: | |
return action.choice; | |
} | |
}).join(", "); | |
} | |
toString() { | |
return `${this.id}: ${this.name}`; | |
} | |
getRequestData(forAlly) { | |
const data = { | |
name: this.name, | |
id: this.id, | |
pokemon: [] | |
}; | |
for (const pokemon of this.pokemon) { | |
data.pokemon.push(pokemon.getSwitchRequestData(forAlly)); | |
} | |
return data; | |
} | |
randomFoe() { | |
const actives = this.foes(); | |
if (!actives.length) | |
return null; | |
return this.battle.sample(actives); | |
} | |
/** Intended as a way to iterate through all foe side conditions - do not use for anything else. */ | |
foeSidesWithConditions() { | |
if (this.battle.gameType === "freeforall") | |
return this.battle.sides.filter((side) => side !== this); | |
return [this.foe]; | |
} | |
foePokemonLeft() { | |
if (this.battle.gameType === "freeforall") { | |
return this.battle.sides.filter((side) => side !== this).map((side) => side.pokemonLeft).reduce((a, b) => a + b); | |
} | |
if (this.foe.allySide) | |
return this.foe.pokemonLeft + this.foe.allySide.pokemonLeft; | |
return this.foe.pokemonLeft; | |
} | |
allies(all) { | |
let allies = this.activeTeam().filter((ally) => ally); | |
if (!all) | |
allies = allies.filter((ally) => !!ally.hp); | |
return allies; | |
} | |
foes(all) { | |
if (this.battle.gameType === "freeforall") { | |
return this.battle.sides.map((side) => side.active[0]).filter((pokemon) => pokemon && pokemon.side !== this && (all || !!pokemon.hp)); | |
} | |
return this.foe.allies(all); | |
} | |
activeTeam() { | |
if (this.battle.gameType !== "multi") | |
return this.active; | |
return this.battle.sides[this.n % 2].active.concat(this.battle.sides[this.n % 2 + 2].active); | |
} | |
hasAlly(pokemon) { | |
return pokemon.side === this || pokemon.side === this.allySide; | |
} | |
addSideCondition(status, source = null, sourceEffect = null) { | |
if (!source && this.battle.event?.target) | |
source = this.battle.event.target; | |
if (source === "debug") | |
source = this.active[0]; | |
if (!source) | |
throw new Error(`setting sidecond without a source`); | |
if (!source.getSlot) | |
source = source.active[0]; | |
status = this.battle.dex.conditions.get(status); | |
if (this.sideConditions[status.id]) { | |
if (!status.onSideRestart) | |
return false; | |
return this.battle.singleEvent("SideRestart", status, this.sideConditions[status.id], this, source, sourceEffect); | |
} | |
this.sideConditions[status.id] = this.battle.initEffectState({ | |
id: status.id, | |
target: this, | |
source, | |
sourceSlot: source.getSlot(), | |
duration: status.duration | |
}); | |
if (status.durationCallback) { | |
this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); | |
} | |
if (!this.battle.singleEvent("SideStart", status, this.sideConditions[status.id], this, source, sourceEffect)) { | |
delete this.sideConditions[status.id]; | |
return false; | |
} | |
this.battle.runEvent("SideConditionStart", source, source, status); | |
return true; | |
} | |
getSideCondition(status) { | |
status = this.battle.dex.conditions.get(status); | |
if (!this.sideConditions[status.id]) | |
return null; | |
return status; | |
} | |
getSideConditionData(status) { | |
status = this.battle.dex.conditions.get(status); | |
return this.sideConditions[status.id] || null; | |
} | |
removeSideCondition(status) { | |
status = this.battle.dex.conditions.get(status); | |
if (!this.sideConditions[status.id]) | |
return false; | |
this.battle.singleEvent("SideEnd", status, this.sideConditions[status.id], this); | |
delete this.sideConditions[status.id]; | |
return true; | |
} | |
addSlotCondition(target, status, source = null, sourceEffect = null) { | |
source ?? (source = this.battle.event?.target || null); | |
if (source === "debug") | |
source = this.active[0]; | |
if (target instanceof import_pokemon.Pokemon) | |
target = target.position; | |
if (!source) | |
throw new Error(`setting sidecond without a source`); | |
status = this.battle.dex.conditions.get(status); | |
if (this.slotConditions[target][status.id]) { | |
if (!status.onRestart) | |
return false; | |
return this.battle.singleEvent("Restart", status, this.slotConditions[target][status.id], this, source, sourceEffect); | |
} | |
const conditionState = this.slotConditions[target][status.id] = this.battle.initEffectState({ | |
id: status.id, | |
target: this, | |
source, | |
sourceSlot: source.getSlot(), | |
isSlotCondition: true, | |
duration: status.duration | |
}); | |
if (status.durationCallback) { | |
conditionState.duration = status.durationCallback.call(this.battle, this.active[0], source, sourceEffect); | |
} | |
if (!this.battle.singleEvent("Start", status, conditionState, this.active[target], source, sourceEffect)) { | |
delete this.slotConditions[target][status.id]; | |
return false; | |
} | |
return true; | |
} | |
getSlotCondition(target, status) { | |
if (target instanceof import_pokemon.Pokemon) | |
target = target.position; | |
status = this.battle.dex.conditions.get(status); | |
if (!this.slotConditions[target][status.id]) | |
return null; | |
return status; | |
} | |
removeSlotCondition(target, status) { | |
if (target instanceof import_pokemon.Pokemon) | |
target = target.position; | |
status = this.battle.dex.conditions.get(status); | |
if (!this.slotConditions[target][status.id]) | |
return false; | |
this.battle.singleEvent("End", status, this.slotConditions[target][status.id], this.active[target]); | |
delete this.slotConditions[target][status.id]; | |
return true; | |
} | |
send(...parts) { | |
const sideUpdate = "|" + parts.map((part) => { | |
if (typeof part !== "function") | |
return part; | |
return part(this); | |
}).join("|"); | |
this.battle.send("sideupdate", `${this.id} | |
${sideUpdate}`); | |
} | |
emitRequest(update) { | |
this.battle.send("sideupdate", `${this.id} | |
|request|${JSON.stringify(update)}`); | |
this.activeRequest = update; | |
} | |
emitChoiceError(message, unavailable) { | |
this.choice.error = message; | |
const type = `[${unavailable ? "Unavailable" : "Invalid"} choice]`; | |
this.battle.send("sideupdate", `${this.id} | |
|error|${type} ${message}`); | |
if (this.battle.strictChoices) | |
throw new Error(`${type} ${message}`); | |
return false; | |
} | |
isChoiceDone() { | |
if (!this.requestState) | |
return true; | |
if (this.choice.forcedSwitchesLeft) | |
return false; | |
if (this.requestState === "teampreview") { | |
return this.choice.actions.length >= this.pickedTeamSize(); | |
} | |
this.getChoiceIndex(); | |
return this.choice.actions.length >= this.active.length; | |
} | |
chooseMove(moveText, targetLoc = 0, event = "") { | |
if (this.requestState !== "move") { | |
return this.emitChoiceError(`Can't move: You need a ${this.requestState} response`); | |
} | |
const index = this.getChoiceIndex(); | |
if (index >= this.active.length) { | |
return this.emitChoiceError(`Can't move: You sent more choices than unfainted Pok\xE9mon.`); | |
} | |
const autoChoose = !moveText; | |
const pokemon = this.active[index]; | |
const request = pokemon.getMoveRequestData(); | |
let moveid = ""; | |
let targetType = ""; | |
if (autoChoose) | |
moveText = 1; | |
if (typeof moveText === "number" || moveText && /^[0-9]+$/.test(moveText)) { | |
const moveIndex = Number(moveText) - 1; | |
if (moveIndex < 0 || moveIndex >= request.moves.length || !request.moves[moveIndex]) { | |
return this.emitChoiceError(`Can't move: Your ${pokemon.name} doesn't have a move ${moveIndex + 1}`); | |
} | |
moveid = request.moves[moveIndex].id; | |
targetType = request.moves[moveIndex].target; | |
} else { | |
moveid = (0, import_dex.toID)(moveText); | |
if (moveid.startsWith("hiddenpower")) { | |
moveid = "hiddenpower"; | |
} | |
for (const move2 of request.moves) { | |
if (move2.id !== moveid) | |
continue; | |
targetType = move2.target || "normal"; | |
break; | |
} | |
if (!targetType && ["", "dynamax"].includes(event) && request.maxMoves) { | |
for (const [i, moveRequest] of request.maxMoves.maxMoves.entries()) { | |
if (moveid === moveRequest.move) { | |
moveid = request.moves[i].id; | |
targetType = moveRequest.target; | |
event = "dynamax"; | |
break; | |
} | |
} | |
} | |
if (!targetType && ["", "zmove"].includes(event) && request.canZMove) { | |
for (const [i, moveRequest] of request.canZMove.entries()) { | |
if (!moveRequest) | |
continue; | |
if (moveid === (0, import_dex.toID)(moveRequest.move)) { | |
moveid = request.moves[i].id; | |
targetType = moveRequest.target; | |
event = "zmove"; | |
break; | |
} | |
} | |
} | |
if (!targetType) { | |
return this.emitChoiceError(`Can't move: Your ${pokemon.name} doesn't have a move matching ${moveid}`); | |
} | |
} | |
const moves = pokemon.getMoves(); | |
if (autoChoose) { | |
for (const [i, move2] of request.moves.entries()) { | |
if (move2.disabled) | |
continue; | |
if (i < moves.length && move2.id === moves[i].id && moves[i].disabled) | |
continue; | |
moveid = move2.id; | |
targetType = move2.target; | |
break; | |
} | |
} | |
const move = this.battle.dex.moves.get(moveid); | |
const zMove = event === "zmove" ? this.battle.actions.getZMove(move, pokemon) : void 0; | |
if (event === "zmove" && !zMove) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't use ${move.name} as a Z-move`); | |
} | |
if (zMove && this.choice.zMove) { | |
return this.emitChoiceError(`Can't move: You can't Z-move more than once per battle`); | |
} | |
if (zMove) | |
targetType = this.battle.dex.moves.get(zMove).target; | |
const maxMove = event === "dynamax" || pokemon.volatiles["dynamax"] ? this.battle.actions.getMaxMove(move, pokemon) : void 0; | |
if (event === "dynamax" && !maxMove) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't use ${move.name} as a Max Move`); | |
} | |
if (maxMove) | |
targetType = this.battle.dex.moves.get(maxMove).target; | |
if (autoChoose) { | |
targetLoc = 0; | |
} else if (this.battle.actions.targetTypeChoices(targetType)) { | |
if (!targetLoc && this.active.length >= 2) { | |
return this.emitChoiceError(`Can't move: ${move.name} needs a target`); | |
} | |
if (!this.battle.validTargetLoc(targetLoc, pokemon, targetType)) { | |
return this.emitChoiceError(`Can't move: Invalid target for ${move.name}`); | |
} | |
} else { | |
if (targetLoc) { | |
return this.emitChoiceError(`Can't move: You can't choose a target for ${move.name}`); | |
} | |
} | |
const lockedMove = pokemon.getLockedMove(); | |
if (lockedMove) { | |
let lockedMoveTargetLoc = pokemon.lastMoveTargetLoc || 0; | |
const lockedMoveID = (0, import_dex.toID)(lockedMove); | |
if (pokemon.volatiles[lockedMoveID]?.targetLoc) { | |
lockedMoveTargetLoc = pokemon.volatiles[lockedMoveID].targetLoc; | |
} | |
this.choice.actions.push({ | |
choice: "move", | |
pokemon, | |
targetLoc: lockedMoveTargetLoc, | |
moveid: lockedMoveID | |
}); | |
return true; | |
} else if (!moves.length && !zMove) { | |
if (this.battle.gen <= 4) | |
this.send("-activate", pokemon, "move: Struggle"); | |
moveid = "struggle"; | |
} else if (maxMove) { | |
if (pokemon.maxMoveDisabled(move)) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name}'s ${maxMove.name} is disabled`); | |
} | |
} else if (!zMove) { | |
let isEnabled = false; | |
let disabledSource = ""; | |
for (const m of moves) { | |
if (m.id !== moveid) | |
continue; | |
if (!m.disabled) { | |
isEnabled = true; | |
break; | |
} else if (m.disabledSource) { | |
disabledSource = m.disabledSource; | |
} | |
} | |
if (!isEnabled) { | |
if (autoChoose) | |
throw new Error(`autoChoose chose a disabled move`); | |
const includeRequest = this.updateRequestForPokemon(pokemon, (req) => { | |
let updated = false; | |
for (const m of req.moves) { | |
if (m.id === moveid) { | |
if (!m.disabled) { | |
m.disabled = true; | |
updated = true; | |
} | |
if (m.disabledSource !== disabledSource) { | |
m.disabledSource = disabledSource; | |
updated = true; | |
} | |
break; | |
} | |
} | |
return updated; | |
}); | |
const status = this.emitChoiceError(`Can't move: ${pokemon.name}'s ${move.name} is disabled`, includeRequest); | |
if (includeRequest) | |
this.emitRequest(this.activeRequest); | |
return status; | |
} | |
} | |
const mixandmega = this.battle.format.mod === "mixandmega"; | |
const mega = event === "mega"; | |
const megax = event === "megax"; | |
const megay = event === "megay"; | |
if (mega && !pokemon.canMegaEvo) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't mega evolve`); | |
} | |
if (megax && !pokemon.canMegaEvoX) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't mega evolve X`); | |
} | |
if (megay && !pokemon.canMegaEvoY) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't mega evolve Y`); | |
} | |
if ((mega || megax || megay) && this.choice.mega && !mixandmega) { | |
return this.emitChoiceError(`Can't move: You can only mega-evolve once per battle`); | |
} | |
const ultra = event === "ultra"; | |
if (ultra && !pokemon.canUltraBurst) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't ultra burst`); | |
} | |
if (ultra && this.choice.ultra && !mixandmega) { | |
return this.emitChoiceError(`Can't move: You can only ultra burst once per battle`); | |
} | |
let dynamax = event === "dynamax"; | |
const canDynamax = this.activeRequest?.active[this.active.indexOf(pokemon)].canDynamax; | |
if (dynamax && (this.choice.dynamax || !canDynamax)) { | |
if (pokemon.volatiles["dynamax"]) { | |
dynamax = false; | |
} else { | |
if (this.battle.gen !== 8) { | |
return this.emitChoiceError(`Can't move: Dynamaxing doesn't outside of Gen 8.`); | |
} else if (pokemon.side.canDynamaxNow()) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't Dynamax now.`); | |
} else if (pokemon.side.allySide?.canDynamaxNow()) { | |
return this.emitChoiceError(`Can't move: It's your partner's turn to Dynamax.`); | |
} | |
return this.emitChoiceError(`Can't move: You can only Dynamax once per battle.`); | |
} | |
} | |
const terastallize = event === "terastallize"; | |
if (terastallize && !pokemon.canTerastallize) { | |
return this.emitChoiceError(`Can't move: ${pokemon.name} can't Terastallize.`); | |
} | |
if (terastallize && this.choice.terastallize) { | |
return this.emitChoiceError(`Can't move: You can only Terastallize once per battle.`); | |
} | |
if (terastallize && this.battle.gen !== 9) { | |
return this.emitChoiceError(`Can't move: You can only Terastallize in Gen 9.`); | |
} | |
this.choice.actions.push({ | |
choice: "move", | |
pokemon, | |
targetLoc, | |
moveid, | |
mega: mega || ultra, | |
megax, | |
megay, | |
zmove: zMove, | |
maxMove: maxMove ? maxMove.id : void 0, | |
terastallize: terastallize ? pokemon.teraType : void 0 | |
}); | |
if (pokemon.maybeDisabled) { | |
this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive(); | |
} | |
if (mega || megax || megay) | |
this.choice.mega = true; | |
if (ultra) | |
this.choice.ultra = true; | |
if (zMove) | |
this.choice.zMove = true; | |
if (dynamax) | |
this.choice.dynamax = true; | |
if (terastallize) | |
this.choice.terastallize = true; | |
return true; | |
} | |
updateRequestForPokemon(pokemon, update) { | |
if (!this.activeRequest?.active) { | |
throw new Error(`Can't update a request without active Pokemon`); | |
} | |
const req = this.activeRequest.active[pokemon.position]; | |
if (!req) | |
throw new Error(`Pokemon not found in request's active field`); | |
return update(req); | |
} | |
chooseSwitch(slotText) { | |
if (this.requestState !== "move" && this.requestState !== "switch") { | |
return this.emitChoiceError(`Can't switch: You need a ${this.requestState} response`); | |
} | |
const index = this.getChoiceIndex(); | |
if (index >= this.active.length) { | |
if (this.requestState === "switch") { | |
return this.emitChoiceError(`Can't switch: You sent more switches than Pok\xE9mon that need to switch`); | |
} | |
return this.emitChoiceError(`Can't switch: You sent more choices than unfainted Pok\xE9mon`); | |
} | |
const pokemon = this.active[index]; | |
let slot; | |
if (!slotText) { | |
if (this.requestState !== "switch") { | |
return this.emitChoiceError(`Can't switch: You need to select a Pok\xE9mon to switch in`); | |
} | |
if (this.slotConditions[pokemon.position]["revivalblessing"]) { | |
slot = 0; | |
while (!this.pokemon[slot].fainted) | |
slot++; | |
} else { | |
if (!this.choice.forcedSwitchesLeft) | |
return this.choosePass(); | |
slot = this.active.length; | |
while (this.choice.switchIns.has(slot) || this.pokemon[slot].fainted) | |
slot++; | |
} | |
} else { | |
slot = parseInt(slotText) - 1; | |
} | |
if (isNaN(slot) || slot < 0) { | |
slot = -1; | |
for (const [i, mon] of this.pokemon.entries()) { | |
if (slotText.toLowerCase() === mon.name.toLowerCase() || (0, import_dex.toID)(slotText) === mon.species.id) { | |
slot = i; | |
break; | |
} | |
} | |
if (slot < 0) { | |
return this.emitChoiceError(`Can't switch: You do not have a Pok\xE9mon named "${slotText}" to switch to`); | |
} | |
} | |
if (slot >= this.pokemon.length) { | |
return this.emitChoiceError(`Can't switch: You do not have a Pok\xE9mon in slot ${slot + 1} to switch to`); | |
} else if (slot < this.active.length && !this.slotConditions[pokemon.position]["revivalblessing"]) { | |
return this.emitChoiceError(`Can't switch: You can't switch to an active Pok\xE9mon`); | |
} else if (this.choice.switchIns.has(slot)) { | |
return this.emitChoiceError(`Can't switch: The Pok\xE9mon in slot ${slot + 1} can only switch in once`); | |
} | |
const targetPokemon = this.pokemon[slot]; | |
if (this.slotConditions[pokemon.position]["revivalblessing"]) { | |
if (!targetPokemon.fainted) { | |
return this.emitChoiceError(`Can't switch: You have to pass to a fainted Pok\xE9mon`); | |
} | |
this.choice.forcedSwitchesLeft = this.battle.clampIntRange(this.choice.forcedSwitchesLeft - 1, 0); | |
pokemon.switchFlag = false; | |
this.choice.actions.push({ | |
choice: "revivalblessing", | |
pokemon, | |
target: targetPokemon | |
}); | |
return true; | |
} | |
if (targetPokemon.fainted) { | |
return this.emitChoiceError(`Can't switch: You can't switch to a fainted Pok\xE9mon`); | |
} | |
if (this.requestState === "move") { | |
if (pokemon.trapped) { | |
const includeRequest = this.updateRequestForPokemon(pokemon, (req) => { | |
let updated = false; | |
if (req.maybeTrapped) { | |
delete req.maybeTrapped; | |
updated = true; | |
} | |
if (!req.trapped) { | |
req.trapped = true; | |
updated = true; | |
} | |
return updated; | |
}); | |
const status = this.emitChoiceError(`Can't switch: The active Pok\xE9mon is trapped`, includeRequest); | |
if (includeRequest) | |
this.emitRequest(this.activeRequest); | |
return status; | |
} else if (pokemon.maybeTrapped) { | |
this.choice.cantUndo = this.choice.cantUndo || pokemon.isLastActive(); | |
} | |
} else if (this.requestState === "switch") { | |
if (!this.choice.forcedSwitchesLeft) { | |
throw new Error(`Player somehow switched too many Pokemon`); | |
} | |
this.choice.forcedSwitchesLeft--; | |
} | |
this.choice.switchIns.add(slot); | |
this.choice.actions.push({ | |
choice: this.requestState === "switch" ? "instaswitch" : "switch", | |
pokemon, | |
target: targetPokemon | |
}); | |
return true; | |
} | |
/** | |
* The number of pokemon you must choose in Team Preview. | |
* | |
* Note that PS doesn't support choosing fewer than this number of pokemon. | |
* In the games, it is sometimes possible to bring fewer than this, but | |
* since that's nearly always a mistake, we haven't gotten around to | |
* supporting it. | |
*/ | |
pickedTeamSize() { | |
return Math.min(this.pokemon.length, this.battle.ruleTable.pickedTeamSize || Infinity); | |
} | |
chooseTeam(data = "") { | |
if (this.requestState !== "teampreview") { | |
return this.emitChoiceError(`Can't choose for Team Preview: You're not in a Team Preview phase`); | |
} | |
const ruleTable = this.battle.ruleTable; | |
let positions = data.split(data.includes(",") ? "," : "").map((datum) => parseInt(datum) - 1); | |
const pickedTeamSize = this.pickedTeamSize(); | |
positions.splice(pickedTeamSize); | |
if (positions.length === 0) { | |
for (let i = 0; i < pickedTeamSize; i++) | |
positions.push(i); | |
} else if (positions.length < pickedTeamSize) { | |
for (let i = 0; i < pickedTeamSize; i++) { | |
if (!positions.includes(i)) | |
positions.push(i); | |
if (positions.length >= pickedTeamSize) | |
break; | |
} | |
} | |
for (const [index, pos] of positions.entries()) { | |
if (isNaN(pos) || pos < 0 || pos >= this.pokemon.length) { | |
return this.emitChoiceError(`Can't choose for Team Preview: You do not have a Pok\xE9mon in slot ${pos + 1}`); | |
} | |
if (positions.indexOf(pos) !== index) { | |
return this.emitChoiceError(`Can't choose for Team Preview: The Pok\xE9mon in slot ${pos + 1} can only switch in once`); | |
} | |
} | |
if (ruleTable.maxTotalLevel) { | |
let totalLevel = 0; | |
for (const pos of positions) | |
totalLevel += this.pokemon[pos].level; | |
if (totalLevel > ruleTable.maxTotalLevel) { | |
if (!data) { | |
positions = [...this.pokemon.keys()].sort((a, b) => this.pokemon[a].level - this.pokemon[b].level).slice(0, pickedTeamSize); | |
} else { | |
return this.emitChoiceError(`Your selected team has a total level of ${totalLevel}, but it can't be above ${ruleTable.maxTotalLevel}; please select a valid team of ${pickedTeamSize} Pok\xE9mon`); | |
} | |
} | |
} | |
if (ruleTable.valueRules.has("forceselect")) { | |
const species = this.battle.dex.species.get(ruleTable.valueRules.get("forceselect")); | |
if (!data) { | |
positions = [...this.pokemon.keys()].filter((pos) => this.pokemon[pos].species.name === species.name).concat([...this.pokemon.keys()].filter((pos) => this.pokemon[pos].species.name !== species.name)).slice(0, pickedTeamSize); | |
} else { | |
let hasSelection = false; | |
for (const pos of positions) { | |
if (this.pokemon[pos].species.name === species.name) { | |
hasSelection = true; | |
break; | |
} | |
} | |
if (!hasSelection) { | |
return this.emitChoiceError(`You must bring ${species.name} to the battle.`); | |
} | |
} | |
} | |
for (const [index, pos] of positions.entries()) { | |
this.choice.switchIns.add(pos); | |
this.choice.actions.push({ | |
choice: "team", | |
index, | |
pokemon: this.pokemon[pos], | |
priority: -index | |
}); | |
} | |
return true; | |
} | |
chooseShift() { | |
const index = this.getChoiceIndex(); | |
if (index >= this.active.length) { | |
return this.emitChoiceError(`Can't shift: You do not have a Pok\xE9mon in slot ${index + 1}`); | |
} else if (this.requestState !== "move") { | |
return this.emitChoiceError(`Can't shift: You can only shift during a move phase`); | |
} else if (this.battle.gameType !== "triples") { | |
return this.emitChoiceError(`Can't shift: You can only shift to the center in triples`); | |
} else if (index === 1) { | |
return this.emitChoiceError(`Can't shift: You can only shift from the edge to the center`); | |
} | |
const pokemon = this.active[index]; | |
this.choice.actions.push({ | |
choice: "shift", | |
pokemon | |
}); | |
return true; | |
} | |
clearChoice() { | |
let forcedSwitches = 0; | |
let forcedPasses = 0; | |
if (this.battle.requestState === "switch") { | |
const canSwitchOut = this.active.filter((pokemon) => pokemon?.switchFlag).length; | |
const canSwitchIn = this.pokemon.slice(this.active.length).filter((pokemon) => pokemon && !pokemon.fainted).length; | |
forcedSwitches = Math.min(canSwitchOut, canSwitchIn); | |
forcedPasses = canSwitchOut - forcedSwitches; | |
} | |
this.choice = { | |
cantUndo: false, | |
error: ``, | |
actions: [], | |
forcedSwitchesLeft: forcedSwitches, | |
forcedPassesLeft: forcedPasses, | |
switchIns: /* @__PURE__ */ new Set(), | |
zMove: false, | |
mega: false, | |
ultra: false, | |
dynamax: false, | |
terastallize: false | |
}; | |
} | |
choose(input) { | |
if (!this.requestState) { | |
return this.emitChoiceError( | |
this.battle.ended ? `Can't do anything: The game is over` : `Can't do anything: It's not your turn` | |
); | |
} | |
if (this.choice.cantUndo) { | |
return this.emitChoiceError(`Can't undo: A trapping/disabling effect would cause undo to leak information`); | |
} | |
this.clearChoice(); | |
const choiceStrings = input.startsWith("team ") ? [input] : input.split(","); | |
if (choiceStrings.length > this.active.length) { | |
return this.emitChoiceError( | |
`Can't make choices: You sent choices for ${choiceStrings.length} Pok\xE9mon, but this is a ${this.battle.gameType} game!` | |
); | |
} | |
for (const choiceString of choiceStrings) { | |
let [choiceType, data] = import_utils.Utils.splitFirst(choiceString.trim(), " "); | |
data = data.trim(); | |
switch (choiceType) { | |
case "move": | |
const original = data; | |
const error = () => this.emitChoiceError(`Conflicting arguments for "move": ${original}`); | |
let targetLoc; | |
let event = ""; | |
while (true) { | |
if (/\s(?:-|\+)?[1-3]$/.test(data) && (0, import_dex.toID)(data) !== "conversion2") { | |
if (targetLoc !== void 0) | |
return error(); | |
targetLoc = parseInt(data.slice(-2)); | |
data = data.slice(0, -2).trim(); | |
} else if (data.endsWith(" mega")) { | |
if (event) | |
return error(); | |
event = "mega"; | |
data = data.slice(0, -5); | |
} else if (data.endsWith(" megax")) { | |
if (event) | |
return error(); | |
event = "megax"; | |
data = data.slice(0, -6); | |
} else if (data.endsWith(" megay")) { | |
if (event) | |
return error(); | |
event = "megay"; | |
data = data.slice(0, -6); | |
} else if (data.endsWith(" zmove")) { | |
if (event) | |
return error(); | |
event = "zmove"; | |
data = data.slice(0, -6); | |
} else if (data.endsWith(" ultra")) { | |
if (event) | |
return error(); | |
event = "ultra"; | |
data = data.slice(0, -6); | |
} else if (data.endsWith(" dynamax")) { | |
if (event) | |
return error(); | |
event = "dynamax"; | |
data = data.slice(0, -8); | |
} else if (data.endsWith(" gigantamax")) { | |
if (event) | |
return error(); | |
event = "dynamax"; | |
data = data.slice(0, -11); | |
} else if (data.endsWith(" max")) { | |
if (event) | |
return error(); | |
event = "dynamax"; | |
data = data.slice(0, -4); | |
} else if (data.endsWith(" terastal")) { | |
if (event) | |
return error(); | |
event = "terastallize"; | |
data = data.slice(0, -9); | |
} else if (data.endsWith(" terastallize")) { | |
if (event) | |
return error(); | |
event = "terastallize"; | |
data = data.slice(0, -13); | |
} else { | |
break; | |
} | |
} | |
if (!this.chooseMove(data, targetLoc, event)) | |
return false; | |
break; | |
case "switch": | |
this.chooseSwitch(data); | |
break; | |
case "shift": | |
if (data) | |
return this.emitChoiceError(`Unrecognized data after "shift": ${data}`); | |
if (!this.chooseShift()) | |
return false; | |
break; | |
case "team": | |
if (!this.chooseTeam(data)) | |
return false; | |
break; | |
case "pass": | |
case "skip": | |
if (data) | |
return this.emitChoiceError(`Unrecognized data after "pass": ${data}`); | |
if (!this.choosePass()) | |
return false; | |
break; | |
case "auto": | |
case "default": | |
this.autoChoose(); | |
break; | |
default: | |
this.emitChoiceError(`Unrecognized choice: ${choiceString}`); | |
break; | |
} | |
} | |
return !this.choice.error; | |
} | |
getChoiceIndex(isPass) { | |
let index = this.choice.actions.length; | |
if (!isPass) { | |
switch (this.requestState) { | |
case "move": | |
while (index < this.active.length && (this.active[index].fainted || this.active[index].volatiles["commanding"])) { | |
this.choosePass(); | |
index++; | |
} | |
break; | |
case "switch": | |
while (index < this.active.length && !this.active[index].switchFlag) { | |
this.choosePass(); | |
index++; | |
} | |
break; | |
} | |
} | |
return index; | |
} | |
choosePass() { | |
const index = this.getChoiceIndex(true); | |
if (index >= this.active.length) | |
return false; | |
const pokemon = this.active[index]; | |
switch (this.requestState) { | |
case "switch": | |
if (pokemon.switchFlag) { | |
if (!this.choice.forcedPassesLeft) { | |
return this.emitChoiceError(`Can't pass: You need to switch in a Pok\xE9mon to replace ${pokemon.name}`); | |
} | |
this.choice.forcedPassesLeft--; | |
} | |
break; | |
case "move": | |
if (!pokemon.fainted && !pokemon.volatiles["commanding"]) { | |
return this.emitChoiceError(`Can't pass: Your ${pokemon.name} must make a move (or switch)`); | |
} | |
break; | |
default: | |
return this.emitChoiceError(`Can't pass: Not a move or switch request`); | |
} | |
this.choice.actions.push({ | |
choice: "pass" | |
}); | |
return true; | |
} | |
/** Automatically finish a choice if not currently complete. */ | |
autoChoose() { | |
if (this.requestState === "teampreview") { | |
if (!this.isChoiceDone()) | |
this.chooseTeam(); | |
} else if (this.requestState === "switch") { | |
let i = 0; | |
while (!this.isChoiceDone()) { | |
if (!this.chooseSwitch()) | |
throw new Error(`autoChoose switch crashed: ${this.choice.error}`); | |
i++; | |
if (i > 10) | |
throw new Error(`autoChoose failed: infinite looping`); | |
} | |
} else if (this.requestState === "move") { | |
let i = 0; | |
while (!this.isChoiceDone()) { | |
if (!this.chooseMove()) | |
throw new Error(`autoChoose crashed: ${this.choice.error}`); | |
i++; | |
if (i > 10) | |
throw new Error(`autoChoose failed: infinite looping`); | |
} | |
} | |
return true; | |
} | |
destroy() { | |
for (const pokemon of this.pokemon) { | |
if (pokemon) | |
pokemon.destroy(); | |
} | |
for (const action of this.choice.actions) { | |
delete action.side; | |
delete action.pokemon; | |
delete action.target; | |
} | |
this.choice.actions = []; | |
this.pokemon = []; | |
this.active = []; | |
this.foe = null; | |
this.battle = null; | |
} | |
} | |
//# sourceMappingURL=side.js.map | |