pokemon-showdown-client/play.pokemonshowdown.com/src/battle-text-parser.ts

1235 lines
45 KiB
TypeScript

/**
* Text parser
*
* No dependencies
* Optional dependency: BattleText
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license MIT
*/
import { Dex, toID, type ID } from "./battle-dex";
export type Args = [string, ...string[]];
export type KWArgs = { [kw: string]: string };
export type SideID = 'p1' | 'p2' | 'p3' | 'p4';
export class BattleTextParser {
/** escaped for string.replace */
p1 = "Player 1";
/** escaped for string.replace */
p2 = "Player 2";
/** escaped for string.replace */
p3 = "Player 3";
/** escaped for string.replace */
p4 = "Player 4";
perspective: SideID;
gen = 9;
turn = 0;
curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor' = 'break';
lowercaseRegExp: RegExp | null | undefined = undefined;
constructor(perspective: SideID = 'p1') {
this.perspective = perspective;
}
static parseLine(line: string, noDefault: true): Args | null;
static parseLine(line: string): Args;
static parseLine(line: string, noDefault?: boolean): Args | null {
if (!line.startsWith('|')) {
return ['', line];
}
if (line === '|') {
return ['done'];
}
const index = line.indexOf('|', 1);
const cmd = line.slice(1, index);
switch (cmd) {
case 'chatmsg': case 'chatmsg-raw': case 'raw': case 'error': case 'html':
case 'inactive': case 'inactiveoff': case 'warning':
case 'fieldhtml': case 'controlshtml': case 'pagehtml': case 'bigerror':
case 'debug': case 'tier': case 'challstr': case 'popup': case '':
return [cmd, line.slice(index + 1)];
case 'c': case 'chat': case 'uhtml': case 'uhtmlchange': case 'queryresponse': case 'showteam':
// three parts
const index2a = line.indexOf('|', index + 1);
return [cmd, line.slice(index + 1, index2a), line.slice(index2a + 1)];
case 'c:': case 'pm':
// four parts
const index2b = line.indexOf('|', index + 1);
const index3b = line.indexOf('|', index2b + 1);
return [cmd, line.slice(index + 1, index2b), line.slice(index2b + 1, index3b), line.slice(index3b + 1)];
}
if (noDefault) return null;
return line.slice(1).split('|') as [string, ...string[]];
}
static parseBattleLine(line: string): { args: Args, kwArgs: KWArgs } {
let args = this.parseLine(line, true);
if (args) return { args, kwArgs: {} };
args = line.slice(1).split('|') as [string, ...string[]];
const kwArgs: KWArgs = {};
while (args.length > 1) {
const lastArg = args[args.length - 1];
if (!lastArg.startsWith('[')) break;
const bracketPos = lastArg.indexOf(']');
if (bracketPos <= 0) break;
// default to '.' so it evaluates to boolean true
kwArgs[lastArg.slice(1, bracketPos)] = lastArg.slice(bracketPos + 1).trim() || '.';
args.pop();
}
return BattleTextParser.upgradeArgs({ args, kwArgs });
}
static parseNameParts(text: string) {
let group = '';
// names can't start with a symbol
if (!/[A-Za-z0-9]/.test(text.charAt(0))) {
group = text.charAt(0);
text = text.slice(1);
}
let name = text;
const atIndex = text.indexOf('@');
let status = '';
let away = false;
if (atIndex > 0) {
name = text.slice(0, atIndex);
status = text.slice(atIndex + 1);
if (status.startsWith('!')) {
away = true;
status = status.slice(1);
}
}
return { group, name, away, status };
}
/**
* Old replays may use syntax we no longer use, so this function upgrades
* them to modern versions. Used to keep battle.ts itself cleaner. Not
* guaranteed to mutate or not mutate its inputs.
*/
static upgradeArgs({ args, kwArgs }: { args: Args, kwArgs: KWArgs }): { args: Args, kwArgs: KWArgs } {
switch (args[0]) {
case '-activate': {
if (kwArgs.item || kwArgs.move || kwArgs.number || kwArgs.ability) return { args, kwArgs };
let [, pokemon, effect, arg3, arg4] = args;
let target = kwArgs.of;
const id = BattleTextParser.effectId(effect);
if (kwArgs.block) return { args: ['-fail', pokemon], kwArgs };
if (id === 'wonderguard') return { args: ['-immune', pokemon], kwArgs: { from: 'ability:Wonder Guard' } };
if (id === 'beatup' && kwArgs.of) return { args, kwArgs: { name: kwArgs.of } };
if ([
'ingrain', 'quickguard', 'wideguard', 'craftyshield', 'matblock', 'protect', 'mist', 'safeguard',
'electricterrain', 'mistyterrain', 'psychicterrain', 'telepathy', 'stickyhold', 'suctioncups', 'aromaveil',
'flowerveil', 'sweetveil', 'disguise', 'safetygoggles', 'protectivepads',
].includes(id)) {
if (target) {
kwArgs.of = pokemon;
return { args: ['-block', target, effect, arg3], kwArgs };
}
return { args: ['-block', pokemon, effect, arg3], kwArgs };
}
if (id === 'charge') {
return { args: ['-singlemove', pokemon, effect], kwArgs: { of: target } };
}
if ([
'bind', 'wrap', 'clamp', 'whirlpool', 'firespin', 'magmastorm', 'sandtomb', 'infestation', 'snaptrap', 'thundercage', 'trapped',
].includes(id)) {
return { args: ['-start', pokemon, effect], kwArgs: { of: target } };
}
if (id === 'fairylock') {
return { args: ['-fieldactivate', effect], kwArgs: {} };
}
if (id === 'symbiosis' || id === 'poltergeist') {
kwArgs.item = arg3;
} else if (id === 'magnitude') {
kwArgs.number = arg3;
} else if (id === 'skillswap' || id === 'mummy' || id === 'lingeringaroma' || id === 'wanderingspirit') {
kwArgs.ability = arg3;
kwArgs.ability2 = arg4;
} else if ([
'eeriespell', 'gmaxdepletion', 'spite', 'grudge', 'forewarn', 'sketch', 'leppaberry', 'mysteryberry',
].includes(id)) {
kwArgs.move = arg3;
kwArgs.number = arg4;
}
args = ['-activate', pokemon, effect, target || ''];
break;
}
case '-fail': {
if (kwArgs.from === 'ability: Flower Veil') {
return { args: ['-block', kwArgs.of, 'ability: Flower Veil'], kwArgs: { of: args[1] } };
}
break;
}
case '-start': {
if (kwArgs.from === 'Protean' || kwArgs.from === 'Color Change') kwArgs.from = 'ability:' + kwArgs.from;
break;
}
case 'move': {
if (kwArgs.from === 'Magic Bounce') kwArgs.from = 'ability:Magic Bounce';
break;
}
case 'cant': {
let [, pokemon, effect, move] = args;
if (['ability: Damp', 'ability: Dazzling', 'ability: Queenly Majesty', 'ability: Armor Tail'].includes(effect)) {
args[0] = '-block';
return { args: ['-block', pokemon, effect, move, kwArgs.of], kwArgs: {} };
}
break;
}
case '-heal': {
const id = BattleTextParser.effectId(kwArgs.from);
if (['dryskin', 'eartheater', 'voltabsorb', 'waterabsorb'].includes(id)) kwArgs.of = '';
break;
}
case '-restoreboost': {
args[0] = '-clearnegativeboost';
break;
}
case '-weather': {
if (args[1] === 'Snow') args[1] = 'Snowscape';
break;
}
case '-ability': {
if (args[3] && (args[3].startsWith('p1') || args[3].startsWith('p2') || args[3] === 'boost')) {
args[4] = args[3];
args[3] = '';
}
break;
}
case '-nothing':
// OLD: |-nothing
// NEW: |-activate||move:Splash
return { args: ['-activate', '', 'move:Splash'], kwArgs };
}
return { args, kwArgs };
}
extractMessage(buf: string) {
let out = '';
for (const line of buf.split('\n')) {
const { args, kwArgs } = BattleTextParser.parseBattleLine(line);
out += this.parseArgs(args, kwArgs) || '';
}
return out;
}
fixLowercase(input: string) {
if (this.lowercaseRegExp === undefined) {
const prefixes = ['pokemon', 'opposingPokemon', 'team', 'opposingTeam', 'party', 'opposingParty'].map(templateId => {
const template = BattleText.default[templateId];
if (template.startsWith(template.charAt(0).toUpperCase())) return '';
const bracketIndex = template.indexOf('[');
if (bracketIndex >= 0) return template.slice(0, bracketIndex);
return template;
}).filter(prefix => prefix);
if (prefixes.length) {
let buf = `((?:^|\n)(?: | \\(|\\[)?)(` +
prefixes.map(BattleTextParser.escapeRegExp).join('|') +
`)`;
this.lowercaseRegExp = new RegExp(buf, 'g');
} else {
this.lowercaseRegExp = null;
}
}
if (!this.lowercaseRegExp) return input;
return input.replace(this.lowercaseRegExp, (match, p1, p2) => (
p1 + p2.charAt(0).toUpperCase() + p2.slice(1)
));
}
static escapeRegExp(input: string) {
return input.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
static escapeReplace(input: string) {
return input.replace(/\$/g, '$$$$');
}
/** Returns a pokemon name escaped for passing into the second argument of string.replace */
pokemonName = (pokemon: string) => {
if (!pokemon) return '';
if (!pokemon.startsWith('p')) return `???pokemon:${pokemon}???`;
if (pokemon.charAt(3) === ':') return BattleTextParser.escapeReplace(pokemon.slice(4).trim());
else if (pokemon.charAt(2) === ':') return BattleTextParser.escapeReplace(pokemon.slice(3).trim());
return `???pokemon:${pokemon}???`;
};
/** Returns a string escaped for passing into the second argument of string.replace */
pokemon(pokemon: string) {
if (!pokemon) return '';
let side = pokemon.slice(0, 2);
if (!['p1', 'p2', 'p3', 'p4'].includes(side)) return `???pokemon:${pokemon}???`;
const name = this.pokemonName(pokemon);
const isNear = side === this.perspective || side === BattleTextParser.allyID(side as SideID);
const template = BattleText.default[isNear ? 'pokemon' : 'opposingPokemon'];
return template.replace('[NICKNAME]', name).replace(/\$/g, '$$$$');
}
/** Returns a string escaped for passing into the second argument of string.replace */
pokemonFull(pokemon: string, details: string): [string, string] {
const nickname = this.pokemonName(pokemon);
const species = details.split(',')[0];
if (nickname === species) return [pokemon.slice(0, 2), `**${species}**`];
return [pokemon.slice(0, 2), `${nickname} (**${species}**)`];
}
trainer(side: string) {
side = side.slice(0, 2);
if (side === 'p1') return this.p1;
if (side === 'p2') return this.p2;
if (side === 'p3') return this.p3;
if (side === 'p4') return this.p4;
return `???side:${side}???`;
}
static allyID(sideid: SideID): SideID | '' {
if (sideid === 'p1') return 'p3';
if (sideid === 'p2') return 'p4';
if (sideid === 'p3') return 'p1';
if (sideid === 'p4') return 'p2';
return '';
}
team(side: string, isFar = false) {
side = side.slice(0, 2);
if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) {
return !isFar ? BattleText.default.team : BattleText.default.opposingTeam;
}
return isFar ? BattleText.default.team : BattleText.default.opposingTeam;
}
own(side: string) {
side = side.slice(0, 2);
if (side === this.perspective) {
return 'OWN';
}
return '';
}
party(side: string) {
side = side.slice(0, 2);
if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) {
return BattleText.default.party;
}
return BattleText.default.opposingParty;
}
static effectId(effect?: string) {
if (!effect) return '';
if (effect.startsWith('item:') || effect.startsWith('move:')) {
effect = effect.slice(5);
} else if (effect.startsWith('ability:')) {
effect = effect.slice(8);
}
return toID(effect);
}
effect(effect?: string) {
if (!effect) return '';
if (effect.startsWith('item:') || effect.startsWith('move:')) {
effect = effect.slice(5);
} else if (effect.startsWith('ability:')) {
effect = effect.slice(8);
}
return effect.trim();
}
template(type: string, ...namespaces: (string | undefined)[]) {
for (const namespace of namespaces) {
if (!namespace) continue;
if (namespace === 'OWN') {
return BattleText.default[type + 'Own'] + '\n';
}
if (namespace === 'NODEFAULT') {
return '';
}
let id = BattleTextParser.effectId(namespace);
if (BattleText[id] && type in BattleText[id]) {
if (BattleText[id][type].charAt(1) === '.') type = BattleText[id][type].slice(2) as ID;
if (BattleText[id][type].startsWith('#')) id = BattleText[id][type].slice(1) as ID;
if (!BattleText[id][type]) return '';
let template = BattleText[id][type];
for (let i = Dex.gen - 1; i >= this.gen; i--) {
let curTemplate = BattleText[id][`${type}Gen${i}`];
if (curTemplate) template = curTemplate;
}
return template + '\n';
}
}
if (!BattleText.default[type]) return '';
let template = BattleText.default[type];
for (let i = Dex.gen - 1; i >= this.gen; i--) {
let curTemplate = BattleText.default[`${type}Gen${i}`];
if (curTemplate) template = curTemplate;
}
return template + '\n';
}
maybeAbility(effect: string | undefined, holder: string) {
if (!effect) return '';
if (!effect.startsWith('ability:')) return '';
return this.ability(effect.slice(8).trim(), holder);
}
ability(name: string | undefined, holder: string) {
if (!name) return '';
return BattleText.default.abilityActivation.replace('[POKEMON]', this.pokemon(holder)).replace('[ABILITY]', this.effect(name)) + '\n';
}
static stat(stat: string) {
const entry = BattleText[stat || "stats"];
if (!entry?.statName) return `???stat:${stat}???`;
return entry.statName;
}
lineSection(args: Args, kwArgs: KWArgs) {
if (kwArgs.premajor) return 'preMajor';
if (kwArgs.postmajor) return 'postMajor';
if (kwArgs.major) return 'major';
const cmd = args[0];
switch (cmd) {
case 'done' : case 'turn':
return 'break';
case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start':
case '-mega': case '-candynamax': case '-terastallize':
return 'major';
case 'switchout': case 'faint':
return 'preMajor';
case '-zpower':
return 'postMajor';
case '-damage': {
const id = BattleTextParser.effectId(kwArgs.from);
if (id === 'confusion') return 'major';
return 'postMajor';
}
case '-curestatus': {
const id = BattleTextParser.effectId(kwArgs.from);
if (id === 'naturalcure') return 'preMajor';
return 'postMajor';
}
case '-start': {
const id = BattleTextParser.effectId(kwArgs.from);
if (id === 'protean') return 'preMajor';
return 'postMajor';
}
case '-activate': {
const id = BattleTextParser.effectId(args[2]);
if (id === 'confusion' || id === 'attract') return 'preMajor';
return 'postMajor';
}
}
return (cmd.startsWith('-') ? 'postMajor' : '');
}
sectionBreak(args: Args, kwArgs: KWArgs) {
const prevSection = this.curLineSection;
const curSection = this.lineSection(args, kwArgs);
if (!curSection) return false;
this.curLineSection = curSection;
switch (curSection) {
case 'break':
if (prevSection !== 'break') return true;
return false;
case 'preMajor':
case 'major':
if (prevSection === 'postMajor' || prevSection === 'major') return true;
return false;
case 'postMajor':
return false;
}
}
parseArgs(args: Args, kwArgs: KWArgs, noSectionBreak?: boolean) {
let buf = !noSectionBreak && this.sectionBreak(args, kwArgs) ? '\n' : '';
return buf + this.fixLowercase(this.parseArgsInner(args, kwArgs) || '');
}
parseArgsInner(args: Args, kwArgs: KWArgs) {
let cmd = args[0];
switch (cmd) {
case 'player': {
const [, side, name] = args;
if (side === 'p1' && name) {
this.p1 = BattleTextParser.escapeReplace(name);
} else if (side === 'p2' && name) {
this.p2 = BattleTextParser.escapeReplace(name);
} else if (side === 'p3' && name) {
this.p3 = BattleTextParser.escapeReplace(name);
} else if (side === 'p4' && name) {
this.p4 = BattleTextParser.escapeReplace(name);
}
return '';
}
case 'gen': {
const [, num] = args;
this.gen = parseInt(num, 10);
return '';
}
case 'turn': {
const [, num] = args;
this.turn = Number.parseInt(num, 10);
return this.template('turn').replace('[NUMBER]', num) + '\n';
}
case 'start': {
return this.template('startBattle').replace('[TRAINER]', this.p1).replace('[TRAINER]', this.p2);
}
case 'win': case 'tie': {
const [, name] = args;
if (cmd === 'tie' || !name) {
return this.template('tieBattle').replace('[TRAINER]', this.p1).replace('[TRAINER]', this.p2);
}
return this.template('winBattle').replace('[TRAINER]', name);
}
case 'switch': {
const [, pokemon, details] = args;
const [side, fullname] = this.pokemonFull(pokemon, details);
const template = this.template('switchIn', this.own(side));
return template.replace('[TRAINER]', this.trainer(side)).replace('[FULLNAME]', fullname);
}
case 'drag': {
const [, pokemon, details] = args;
const [side, fullname] = this.pokemonFull(pokemon, details);
const template = this.template('drag');
return template.replace('[TRAINER]', this.trainer(side)).replace('[FULLNAME]', fullname);
}
case 'detailschange': case '-transform': case '-formechange': {
const [, pokemon, arg2, arg3] = args;
let newSpecies = '';
switch (cmd) {
case 'detailschange': newSpecies = arg2.split(',')[0].trim(); break;
case '-transform': newSpecies = arg3; break;
case '-formechange': newSpecies = arg2; break;
}
let newSpeciesId = toID(newSpecies);
let id = '';
let templateName = 'transform';
if (cmd !== '-transform') {
switch (newSpeciesId) {
case 'greninjaash': id = 'battlebond'; break;
case 'mimikyubusted': id = 'disguise'; break;
case 'zygardecomplete': id = 'powerconstruct'; break;
case 'necrozmaultra': id = 'ultranecroziumz'; break;
case 'darmanitanzen': id = 'zenmode'; break;
case 'darmanitan': id = 'zenmode'; templateName = 'transformEnd'; break;
case 'darmanitangalarzen': id = 'zenmode'; break;
case 'darmanitangalar': id = 'zenmode'; templateName = 'transformEnd'; break;
case 'aegislashblade': id = 'stancechange'; break;
case 'aegislash': id = 'stancechange'; templateName = 'transformEnd'; break;
case 'wishiwashischool': id = 'schooling'; break;
case 'wishiwashi': id = 'schooling'; templateName = 'transformEnd'; break;
case 'miniormeteor': id = 'shieldsdown'; break;
case 'minior': id = 'shieldsdown'; templateName = 'transformEnd'; break;
case 'eiscuenoice': id = 'iceface'; break;
case 'eiscue': id = 'iceface'; templateName = 'transformEnd'; break;
case 'terapagosterastal': id = 'terashift'; break;
}
} else if (newSpecies) {
id = 'transform';
}
const template = this.template(templateName, id, kwArgs.msg ? '' : 'NODEFAULT');
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SPECIES]', newSpecies);
}
case 'switchout': {
const [, pokemon] = args;
const side = pokemon.slice(0, 2);
const template = this.template('switchOut', kwArgs.from, this.own(side));
return template.replace('[TRAINER]', this.trainer(side)).replace('[NICKNAME]', this.pokemonName(pokemon)).replace('[POKEMON]', this.pokemon(pokemon));
}
case 'faint': {
const [, pokemon] = args;
const template = this.template('faint');
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
case 'swap': {
const [, pokemon, target] = args;
if (!target || !isNaN(Number(target))) {
const template = this.template('swapCenter');
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
const template = this.template('swap');
return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target));
}
case 'move': {
const [, pokemon, move] = args;
let line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (kwArgs.zeffect) {
line1 = this.template('zEffect').replace('[POKEMON]', this.pokemon(pokemon));
}
const template = this.template('move', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move);
}
case 'cant': {
let [, pokemon, effect, move] = args;
const template = this.template('cant', effect, 'NODEFAULT') ||
this.template(move ? 'cant' : 'cantNoMove');
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move);
}
case '-candynamax': {
let [, side] = args;
const own = this.own(side);
let template = '';
if (this.turn === 1) {
if (own) template = this.template('canDynamax', own);
} else {
template = this.template('canDynamax', own);
}
return template.replace('[TRAINER]', this.trainer(side));
}
case 'message': {
let [, message] = args;
return '' + message + '\n';
}
case '-start': {
let [, pokemon, effect, arg3] = args;
const line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let id = BattleTextParser.effectId(effect);
if (id === 'typechange') {
const template = this.template('typeChange', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3)
.replace('[SOURCE]', this.pokemon(kwArgs.of));
}
if (id === 'typeadd') {
const template = this.template('typeAdd', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TYPE]', arg3);
}
if (id.startsWith('stockpile')) {
const num = id.slice(9);
const template = this.template('start', 'stockpile');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', num);
}
if (id.startsWith('perish')) {
const num = id.slice(6);
const template = this.template('activate', 'perishsong');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', num);
}
if (id.startsWith('protosynthesis') || id.startsWith('quarkdrive')) {
const stat = id.slice(-3);
const template = this.template('start', id.slice(0, id.length - 3));
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat));
}
let templateId = 'start';
if (kwArgs.already) templateId = 'alreadyStarted';
if (kwArgs.fatigue) templateId = 'startFromFatigue';
if (kwArgs.zeffect) templateId = 'startFromZEffect';
if (kwArgs.damage) templateId = 'activate';
if (kwArgs.block) templateId = 'block';
if (kwArgs.upkeep) templateId = 'upkeep';
if (templateId === 'start' && kwArgs.from?.startsWith('item:')) {
templateId += 'FromItem';
}
const template = this.template(templateId, kwArgs.from, effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect))
.replace('[MOVE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(kwArgs.from));
}
case '-end': {
let [, pokemon, effect] = args;
const line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let id = BattleTextParser.effectId(effect);
if (id === 'doomdesire' || id === 'futuresight') {
const template = this.template('activate', effect);
return line1 + template.replace('[TARGET]', this.pokemon(pokemon));
}
let templateId = 'end';
let template = '';
if (kwArgs.from?.startsWith('item:')) {
template = this.template('endFromItem', effect);
}
if (!template) template = this.template(templateId, effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect))
.replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(kwArgs.from));
}
case '-ability': {
let [, pokemon, ability, oldAbility] = args;
let line1 = '';
if (oldAbility) line1 += this.ability(oldAbility, pokemon);
line1 += this.ability(ability, pokemon);
if (kwArgs.fail) {
const template = this.template('block', kwArgs.from);
return line1 + template;
}
if (kwArgs.from) {
if (!oldAbility) line1 = this.maybeAbility(kwArgs.from, pokemon) + line1;
const template = this.template('changeAbility', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ABILITY]', this.effect(ability))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
}
const id = BattleTextParser.effectId(ability);
if (id === 'unnerve') {
const template = this.template('start', ability);
return line1 + template.replace('[TEAM]', this.team(pokemon.slice(0, 2), true));
}
let templateId = 'start';
if (id === 'anticipation' || id === 'sturdy') templateId = 'activate';
const template = this.template(templateId, ability, 'NODEFAULT');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-endability': {
let [, pokemon, ability] = args;
if (ability) return this.ability(ability, pokemon);
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const template = this.template('start', 'Gastro Acid');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-item': {
const [, pokemon, item] = args;
const id = BattleTextParser.effectId(kwArgs.from);
let target = '';
if (['magician', 'pickpocket'].includes(id)) {
[target, kwArgs.of] = [kwArgs.of, ''];
}
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (['thief', 'covet', 'bestow', 'magician', 'pickpocket'].includes(id)) {
const template = this.template('takeItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[SOURCE]', this.pokemon(target || kwArgs.of));
}
if (id === 'frisk') {
const hasTarget = kwArgs.of && pokemon && kwArgs.of !== pokemon;
const template = this.template(hasTarget ? 'activate' : 'activateNoTarget', "Frisk");
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(item))
.replace('[TARGET]', this.pokemon(pokemon));
}
if (kwArgs.from) {
const template = this.template('addItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item));
}
const template = this.template('start', item, 'NODEFAULT');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-enditem': {
let [, pokemon, item] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (kwArgs.eat) {
const template = this.template('eatItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item));
}
const id = BattleTextParser.effectId(kwArgs.from);
if (id === 'gem') {
const template = this.template('useGem', item);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[MOVE]', kwArgs.move);
}
if (id === 'stealeat') {
const template = this.template('removeItem', "Bug Bite");
return line1 + template.replace('[SOURCE]', this.pokemon(kwArgs.of)).replace('[ITEM]', this.effect(item));
}
if (kwArgs.from) {
const template = this.template('removeItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
}
if (kwArgs.weaken) {
const template = this.template('activateWeaken');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item));
}
let template = this.template('end', item, 'NODEFAULT');
if (!template) template = this.template('activateItem').replace('[ITEM]', this.effect(item));
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(kwArgs.of));
}
case '-status': {
const [, pokemon, status] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (kwArgs.from?.startsWith('item:')) {
const template = this.template('startFromItem', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from));
}
if (BattleTextParser.effectId(kwArgs.from) === 'rest') {
const template = this.template('startFromRest', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
const template = this.template('start', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-curestatus': {
const [, pokemon, status] = args;
if (BattleTextParser.effectId(kwArgs.from) === 'naturalcure') {
const template = this.template('activate', kwArgs.from);
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (kwArgs.from?.startsWith('item:')) {
const template = this.template('endFromItem', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from));
}
if (kwArgs.thaw) {
const template = this.template('endFromMove', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', this.effect(kwArgs.from));
}
let template = this.template('end', status, 'NODEFAULT');
if (!template) template = this.template('end').replace('[EFFECT]', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-cureteam': {
return this.template('activate', kwArgs.from);
}
case '-singleturn': case '-singlemove': {
const [, pokemon, effect] = args;
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon) ||
this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let id = BattleTextParser.effectId(effect);
if (id === 'instruct') {
const template = this.template('activate', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[TARGET]', this.pokemon(pokemon));
}
let template = this.template('start', effect, 'NODEFAULT');
if (!template) template = this.template('start').replace('[EFFECT]', this.effect(effect));
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of))
.replace('[TEAM]', this.team(pokemon.slice(0, 2)));
}
case '-sidestart': {
let [, side, effect] = args;
let template = this.template('start', effect, 'NODEFAULT');
if (!template) template = this.template('startTeamEffect').replace('[EFFECT]', this.effect(effect));
return template.replace('[TEAM]', this.team(side)).replace('[PARTY]', this.party(side));
}
case '-sideend': {
let [, side, effect] = args;
let template = this.template('end', effect, 'NODEFAULT');
if (!template) template = this.template('endTeamEffect').replace('[EFFECT]', this.effect(effect));
return template.replace('[TEAM]', this.team(side)).replace('[PARTY]', this.party(side));
}
case '-weather': {
const [, weather] = args;
if (!weather || weather === 'none') {
const template = this.template('end', kwArgs.from, 'NODEFAULT');
if (!template) return this.template('endFieldEffect').replace('[EFFECT]', this.effect(weather));
return template;
}
if (kwArgs.upkeep) {
return this.template('upkeep', weather, 'NODEFAULT');
}
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of);
let template = this.template('start', weather, 'NODEFAULT');
if (!template) template = this.template('startFieldEffect').replace('[EFFECT]', this.effect(weather));
return line1 + template;
}
case '-fieldstart': case '-fieldactivate': {
const [, effect] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of);
if (BattleTextParser.effectId(kwArgs.from) === 'hadronengine') {
return line1 + this.template('start', 'hadronengine').replace('[POKEMON]', this.pokemon(kwArgs.of));
}
let templateId = cmd.slice(6);
if (BattleTextParser.effectId(effect) === 'perishsong') templateId = 'start';
let template = this.template(templateId, effect, 'NODEFAULT');
if (!template) template = this.template('startFieldEffect').replace('[EFFECT]', this.effect(effect));
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of));
}
case '-fieldend': {
let [, effect] = args;
let template = this.template('end', effect, 'NODEFAULT');
if (!template) template = this.template('endFieldEffect').replace('[EFFECT]', this.effect(effect));
return template;
}
case '-sethp': {
let effect = kwArgs.from;
return this.template('activate', effect);
}
case '-message': {
let [, message] = args;
return ' ' + message + '\n';
}
case '-hint': {
let [, message] = args;
return ' (' + message + ')\n';
}
case '-activate': {
let [, pokemon, effect, target] = args;
let id = BattleTextParser.effectId(effect);
if (id === 'celebrate') {
return this.template('activate', 'celebrate').replace('[TRAINER]', this.trainer(pokemon.slice(0, 2)));
}
if (!target &&
['hyperdrill', 'hyperspacefury', 'hyperspacehole', 'phantomforce', 'shadowforce', 'feint'].includes(id)) {
[pokemon, target] = [kwArgs.of, pokemon];
if (!pokemon) pokemon = target;
}
if (!target) target = kwArgs.of || pokemon;
let line1 = this.maybeAbility(effect, pokemon);
if (id === 'lockon' || id === 'mindreader') {
const template = this.template('start', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of)).replace('[SOURCE]', this.pokemon(pokemon));
}
if ((id === 'mummy' || id === 'lingeringaroma') && kwArgs.ability) {
line1 += this.ability(kwArgs.ability, target);
line1 += this.ability(id === 'mummy' ? 'Mummy' : 'Lingering Aroma', target);
const template = this.template('changeAbility', id);
return line1 + template.replace('[TARGET]', this.pokemon(target));
}
if (id === 'commander') {
// Commander didn't have a message prior to v1.2.0 of SV
// so this is for backwards compatibility
if (target === pokemon) return line1;
const template = this.template('activate', id);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace(/\[TARGET\]/g, this.pokemon(target));
}
let templateId = 'activate';
if (id === 'forewarn' && pokemon === target) {
templateId = 'activateNoTarget';
}
if ((id === 'protosynthesis' || id === 'quarkdrive') && kwArgs.fromitem) {
templateId = 'activateFromItem';
}
if (id === 'orichalcumpulse' && kwArgs.source) {
templateId = 'start';
}
let template = this.template(templateId, effect, 'NODEFAULT');
if (!template) {
if (line1) return line1; // Abilities don't have a default template
template = this.template('activate');
return line1 + template.replace('[EFFECT]', this.effect(effect));
}
if (id === 'brickbreak') {
template = template.replace('[TEAM]', this.team(target.slice(0, 2)));
}
if (kwArgs.ability) {
line1 += this.ability(kwArgs.ability, pokemon);
}
if (kwArgs.ability2) {
line1 += this.ability(kwArgs.ability2, target);
}
if (kwArgs.move || kwArgs.number || kwArgs.item || kwArgs.name) {
template = template.replace('[MOVE]', kwArgs.move).replace('[NUMBER]', kwArgs.number)
.replace('[ITEM]', kwArgs.item).replace('[NAME]', kwArgs.name);
}
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
}
case '-prepare': {
const [, pokemon, effect, target] = args;
const template = this.template('prepare', effect);
return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target));
}
case '-damage': {
let [, pokemon, , percentage] = args;
let template = this.template('damage', kwArgs.from, 'NODEFAULT');
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const id = BattleTextParser.effectId(kwArgs.from);
if (template) {
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
if (!kwArgs.from) {
template = this.template(percentage ? 'damagePercentage' : 'damage');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[PERCENTAGE]', percentage);
}
if (kwArgs.from.startsWith('item:')) {
template = this.template(kwArgs.of ? 'damageFromPokemon' : 'damageFromItem');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from))
.replace('[SOURCE]', this.pokemon(kwArgs.of));
}
if (kwArgs.partiallytrapped || id === 'bind' || id === 'wrap') {
template = this.template('damageFromPartialTrapping');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', this.effect(kwArgs.from));
}
template = this.template('damage');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-heal': {
let [, pokemon] = args;
let template = this.template('heal', kwArgs.from, 'NODEFAULT');
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (template) {
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of))
.replace('[NICKNAME]', kwArgs.wisher);
}
if (kwArgs.from && !kwArgs.from.startsWith('ability:')) {
template = this.template('healFromEffect');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(kwArgs.from));
}
template = this.template('heal');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-boost': case '-unboost': {
let [, pokemon, stat, num] = args;
if (stat === 'spa' && this.gen === 1) stat = 'spc';
const amount = parseInt(num, 10);
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let templateId = cmd.slice(1);
if (amount >= 3) templateId += '3';
else if (amount >= 2) templateId += '2';
else if (amount === 0) templateId += '0';
if (amount && kwArgs.zeffect) {
templateId += (kwArgs.multiple ? 'MultipleFromZEffect' : 'FromZEffect');
} else if (amount && kwArgs.from?.startsWith('item:')) {
const template = this.template(templateId + 'FromItem', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat))
.replace('[ITEM]', this.effect(kwArgs.from));
}
const template = this.template(templateId, kwArgs.from);
return line1 + template.replace(/\[POKEMON\]/g, this.pokemon(pokemon)).replace('[STAT]', BattleTextParser.stat(stat));
}
case '-setboost': {
const [, pokemon] = args;
const effect = kwArgs.from;
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
const template = this.template('boost', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-swapboost': {
const [, pokemon, target] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const id = BattleTextParser.effectId(kwArgs.from);
let templateId = 'swapBoost';
if (id === 'guardswap') templateId = 'swapDefensiveBoost';
if (id === 'powerswap') templateId = 'swapOffensiveBoost';
const template = this.template(templateId, kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target));
}
case '-copyboost': {
const [, pokemon, target] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const template = this.template('copyBoost', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target));
}
case '-clearboost': case '-clearpositiveboost': case '-clearnegativeboost': {
const [, pokemon, source] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let templateId = 'clearBoost';
if (kwArgs.zeffect) templateId = 'clearBoostFromZEffect';
const template = this.template(templateId, kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(source));
}
case '-invertboost': {
const [, pokemon] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const template = this.template('invertBoost', kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-clearallboost': {
return this.template('clearAllBoost', kwArgs.from);
}
case '-crit': case '-supereffective': case '-resisted': {
const [, pokemon] = args;
let templateId = cmd.slice(1);
if (templateId === 'supereffective') templateId = 'superEffective';
if (kwArgs.spread) templateId += 'Spread';
const template = this.template(templateId);
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-block': {
let [, pokemon, effect, move, attacker] = args;
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
const template = this.template('block', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon))
.replace('[SOURCE]', this.pokemon(attacker || kwArgs.of)).replace('[MOVE]', move);
}
case '-fail': {
let [, pokemon, effect, stat] = args;
let id = BattleTextParser.effectId(effect);
let blocker = BattleTextParser.effectId(kwArgs.from);
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let templateId = 'block';
if (['desolateland', 'primordialsea'].includes(blocker) &&
!['sunnyday', 'raindance', 'sandstorm', 'hail', 'snowscape', 'chillyreception'].includes(id)) {
templateId = 'blockMove';
} else if (blocker === 'uproar' && kwArgs.msg) {
templateId = 'blockSelf';
}
let template = this.template(templateId, kwArgs.from);
if (template) {
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
if (id === 'unboost') {
template = this.template(stat ? 'failSingular' : 'fail', 'unboost');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', stat);
}
templateId = 'fail';
if (['brn', 'frz', 'par', 'psn', 'slp', 'substitute', 'shedtail'].includes(id)) {
templateId = 'alreadyStarted';
}
if (kwArgs.heavy) templateId = 'failTooHeavy';
if (kwArgs.weak) templateId = 'fail';
if (kwArgs.forme) templateId = 'failWrongForme';
template = this.template(templateId, id);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-immune': {
const [, pokemon] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let template = this.template('block', kwArgs.from);
if (!template) {
const templateId = kwArgs.ohko ? 'immuneOHKO' : 'immune';
template = this.template(pokemon ? templateId : 'immuneNoPokemon', kwArgs.from);
}
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-miss': {
const [, source, pokemon] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (!pokemon) {
const template = this.template('missNoPokemon');
return line1 + template.replace('[SOURCE]', this.pokemon(source));
}
const template = this.template('miss');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-center': case '-ohko': case '-combine': {
return this.template(cmd.slice(1));
}
case '-notarget': {
return this.template('noTarget');
}
case '-mega': case '-primal': {
const [, pokemon, species, item] = args;
let id = '';
let templateId = cmd.slice(1);
if (species === 'Rayquaza') {
id = 'dragonascent';
templateId = 'megaNoItem';
}
if (!item && cmd === '-mega') templateId = 'megaNoItem';
let template = this.template(templateId, id);
const side = pokemon.slice(0, 2);
const pokemonName = this.pokemon(pokemon);
if (cmd === '-mega') {
const template2 = this.template('transformMega');
template += template2.replace('[POKEMON]', pokemonName).replace('[SPECIES]', species);
}
return template.replace('[POKEMON]', pokemonName).replace('[ITEM]', item).replace('[TRAINER]', this.trainer(side));
}
case '-terastallize': {
const [, pokemon, type] = args;
let id = '';
let templateId = cmd.slice(1);
let template = this.template(templateId, id);
const pokemonName = this.pokemon(pokemon);
return template.replace('[POKEMON]', pokemonName).replace('[TYPE]', type);
}
case '-zpower': {
const [, pokemon] = args;
const template = this.template('zPower');
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-burst': {
const [, pokemon] = args;
const template = this.template('activate', "Ultranecrozium Z");
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-zbroken': {
const [, pokemon] = args;
const template = this.template('zBroken');
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-hitcount': {
const [, , num] = args;
if (num === '1') {
return this.template('hitCountSingular');
}
return this.template('hitCount').replace('[NUMBER]', num);
}
case '-waiting': {
const [, pokemon, target] = args;
const template = this.template('activate', "Water Pledge");
return template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(target));
}
case '-anim': {
return '';
}
default: {
return null;
}
}
}
}
declare const require: any;
declare const global: any;
if (typeof require === 'function') {
// in Node
global.BattleTextParser = BattleTextParser;
}