Implement new battle-text-parser

All battle text messages have been moved out of `src/battle.ts` and
into its own file `data/text.js`.

Code for handling this is in the new files `src/battle-log.ts` and
`src/battle-text-parser.ts`.

`data/text.js` is now extremely self-contained, and nearly ready for
translation support!

This is a significant modernization of battle.ts. In addition to moving
messages out:

Functions for getting names (`pokemon.getLowerName()` etc) have been
removed.

`battle.minorQueue` has been removed. Minor lines are now processed
directly on the main queue, with a new `battle.waitForAnimations`
flag to decide whether or not the main queue should wait for animations
to finish before moving on to the next line.

`battle.waitForResult()` and `battle.endPrevAction()` have been
removed. These confusingly-named functions closed the messagebar (and
flush the minor queue). They've been replaced with
`scene.maybeCloseMessagebar()`.

`pokemon.markMove()` and `pokemon.markAbility()` have been renamed
`pokemon.rememberMove()` and `pokemon.rememberAbility()`.
This commit is contained in:
Guangcong Luo 2018-11-14 16:05:05 -06:00
parent bf002f9450
commit fd89a66510
14 changed files with 3935 additions and 3187 deletions

View File

@ -5,6 +5,7 @@ node_modules/
/js/battle.js
/js/battledata.js
/js/battle-log.js
/js/battle-text-parser.js
/js/battle-dex.js
/js/battle-dex-data.js
/js/battle-animations-moves.js

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ package-lock.json
/js/battle.js
/js/battledata.js
/js/battle-log.js
/js/battle-text-parser.js
/js/battle-dex.js
/js/battle-dex-data.js
/js/battle-animations-moves.js

View File

@ -60,7 +60,9 @@ if (!ignoreGraphics) {
fs.writeFileSync(
'data/graphics.js',
fs.readFileSync('js/battle-animations.js') + '\n\n' +
fs.readFileSync('js/battle-animations-moves.js')
fs.readFileSync('js/battle-animations-moves.js') + '\n\n' +
fs.readFileSync('data/text.js') + '\n\n' +
fs.readFileSync('js/battle-text-parser.js')
);
}

1100
data/text.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -406,7 +406,13 @@ var BattleTooltips = (function () {
var gender = pokemon.gender;
if (gender) gender = ' <img src="' + Tools.resourcePrefix + 'fx/gender-' + gender.toLowerCase() + '.png" alt="' + gender + '" />';
text = '<div class="tooltipinner"><div class="tooltip">';
text += '<h2>' + pokemon.getFullName() + gender + (pokemon.level !== 100 ? ' <small>L' + pokemon.level + '</small>' : '') + '<br />';
var name = BattleLog.escapeHTML(pokemon.name);
if (pokemon.species !== pokemon.name) {
name += ' <small>(' + BattleLog.escapeHTML(pokemon.species) + ')</small>';
}
text += '<h2>' + name + gender + (pokemon.level !== 100 ? ' <small>L' + pokemon.level + '</small>' : '') + '<br />';
var template = Tools.getTemplate(pokemon.getSpecies ? pokemon.getSpecies() : pokemon.species);
if (pokemon.volatiles && pokemon.volatiles.formechange) {

View File

@ -955,7 +955,6 @@
pokemonData.getFormattedRange = Pokemon.prototype.getFormattedRange;
pokemonData.getHPColorClass = Pokemon.prototype.getHPColorClass;
pokemonData.getHPColor = Pokemon.prototype.getHPColor;
pokemonData.getFullName = Pokemon.prototype.getFullName;
}
},

View File

@ -924,6 +924,86 @@
app.addPopup(UserPopup, {name: target});
});
}
}, {
parseChatMessage: function (message, name, timestamp, isHighlighted, $chatElem) {
var showMe = !((Tools.prefs('chatformatting') || {}).hideme);
var group = ' ';
if (!/[A-Za-z0-9]/.test(name.charAt(0))) {
// Backwards compatibility
group = name.charAt(0);
name = name.substr(1);
}
var color = BattleLog.hashColor(toId(name));
var clickableName = '<small>' + BattleLog.escapeHTML(group) + '</small><span class="username" data-name="' + BattleLog.escapeHTML(name) + '">' + BattleLog.escapeHTML(name) + '</span>';
var hlClass = isHighlighted ? ' highlighted' : '';
var mineClass = (window.app && app.user && app.user.get('name') === name ? ' mine' : '');
var cmd = '';
var target = '';
if (message.charAt(0) === '/') {
if (message.charAt(1) === '/') {
message = message.slice(1);
} else {
var spaceIndex = message.indexOf(' ');
cmd = (spaceIndex >= 0 ? message.slice(1, spaceIndex) : message.slice(1));
if (spaceIndex >= 0) target = message.slice(spaceIndex + 1);
}
}
switch (cmd) {
case 'me':
if (!showMe) return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + BattleLog.parseMessage(' ' + target) + '</em></div>';
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + BattleLog.parseMessage(' ' + target) + '</i></em></div>';
case 'mee':
if (!showMe) return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + BattleLog.parseMessage(' ' + target).slice(1) + '</em></div>';
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + BattleLog.parseMessage(' ' + target).slice(1) + '</i></em></div>';
case 'invite':
var roomid = toRoomid(target);
return [
'<div class="chat">' + timestamp + '<em>' + clickableName + ' invited you to join the room "' + roomid + '"</em></div>',
'<div class="notice"><button name="joinRoom" value="' + roomid + '">Join ' + roomid + '</button></div>'
];
case 'announce':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <span class="message-announce">' + BattleLog.parseMessage(target) + '</span></div>';
case 'log':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<span class="message-log">' + BattleLog.parseMessage(target) + '</span></div>';
case 'data-pokemon':
case 'data-item':
case 'data-ability':
case 'data-move':
return '[outdated message type not supported]';
case 'text':
return '<div class="chat">' + BattleLog.parseMessage(target) + '</div>';
case 'error':
return '<div class="chat message-error">' + BattleLog.escapeHTML(target) + '</div>';
case 'html':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.sanitizeHTML(target) + '</em></div>';
case 'uhtml':
case 'uhtmlchange':
var parts = target.split(',');
var $elements = $chatElem.find('div.uhtml-' + toId(parts[0]));
var html = parts.slice(1).join(',');
if (!html) {
$elements.remove();
} else if (!$elements.length) {
$chatElem.append('<div class="chat uhtml-' + toId(parts[0]) + '">' + BattleLog.sanitizeHTML(html) + '</div>');
} else if (cmd === 'uhtmlchange') {
$elements.html(BattleLog.sanitizeHTML(html));
} else {
$elements.remove();
$chatElem.append('<div class="chat uhtml-' + toId(parts[0]) + '">' + BattleLog.sanitizeHTML(html) + '</div>');
}
return '';
case 'raw':
return '<div class="chat">' + BattleLog.sanitizeHTML(target) + '</div>';
default:
// Not a command or unsupported. Parsed as a normal chat message.
if (!name) {
return '<div class="chat' + hlClass + '">' + timestamp + '<em>' + BattleLog.parseMessage(message) + '</em></div>';
}
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.parseMessage(message) + '</em></div>';
}
}
});
var FormatPopup = this.FormatPopup = this.Popup.extend({

View File

@ -3366,9 +3366,6 @@ const BattleMoveAnims: AnimTable = {
}, 'accel');
},
prepareAnim: BattleOtherAnims.chargestatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' is absorbing power!';
}
},
magnetrise: {
anim(scene, [attacker]) {
@ -3991,9 +3988,6 @@ const BattleMoveAnims: AnimTable = {
attacker.anim({opacity: 0, time: 50}, 'linear');
scene.wait(200);
},
prepareMessage(pokemon) {
return pokemon.getName() + ' vanished instantly!';
}
},
bounce: {
anim(scene, [attacker, defender]) {
@ -4074,9 +4068,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' sprang up!';
}
},
dig: {
anim(scene, [attacker, defender]) {
@ -4157,9 +4148,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' burrowed its way under the ground!';
}
},
dive: {
anim(scene, [attacker, defender]) {
@ -4238,9 +4226,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'swing');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' hid underwater!';
}
},
fly: {
anim: BattleOtherAnims.flight.anim,
@ -4251,9 +4236,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' flew up high!';
}
},
skydrop: {
anim: BattleOtherAnims.contactattack.anim,
@ -4269,9 +4251,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon, pokemon2) {
return pokemon.getName() + ' took ' + pokemon2.getLowerName() + ' into the sky!';
}
},
skullbash: {
anim: BattleOtherAnims.contactattack.anim,
@ -4281,9 +4260,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' tucked in its head!';
}
},
skyattack: {
anim: BattleOtherAnims.flight.anim,
@ -4293,9 +4269,6 @@ const BattleMoveAnims: AnimTable = {
time: 300
}, 'linear');
},
prepareMessage(pokemon) {
return pokemon.getName() + ' became cloaked in a harsh light!';
}
},
hiddenpower: {
anim(scene, [attacker, defender]) {
@ -9171,9 +9144,6 @@ const BattleMoveAnims: AnimTable = {
}, 'linear', 'explode');
},
prepareAnim: BattleOtherAnims.chargestatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' set a shell trap!';
}
},
flamecharge: {
anim(scene, [attacker, defender]) {
@ -18806,9 +18776,6 @@ const BattleMoveAnims: AnimTable = {
}, 'linear', 'explode');
},
prepareAnim: BattleOtherAnims.chargestatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' absorbed light!';
}
},
solarblade: {
anim(scene, [attacker, defender]) {
@ -18932,9 +18899,6 @@ const BattleMoveAnims: AnimTable = {
}, 'swing');
},
prepareAnim: BattleOtherAnims.chargestatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' absorbed light!';
}
},
lightofruin: {
anim(scene, [attacker, defender]) {
@ -19294,9 +19258,6 @@ const BattleMoveAnims: AnimTable = {
}, 'linear');
},
prepareAnim: BattleOtherAnims.selfstatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' became cloaked in a freezing light!';
}
},
iceburn: {
anim(scene, [attacker, defender]) {
@ -19416,16 +19377,10 @@ const BattleMoveAnims: AnimTable = {
}, 'linear');
},
prepareAnim: BattleOtherAnims.selfstatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' became cloaked in freezing air!';
}
},
razorwind: {
anim: null!,
prepareAnim: BattleOtherAnims.selfstatus.anim,
prepareMessage(pokemon) {
return pokemon.getName() + ' whipped up a whirlwind!';
}
},
overheat: {
anim(scene, [attacker, defender]) {
@ -30711,7 +30666,7 @@ BattleMoveAnims['flatter'] = {anim:BattleMoveAnims['attract'].anim};
BattleMoveAnims['armthrust'] = {anim:BattleMoveAnims['smellingsalts'].anim, multihit:true};
BattleMoveAnims['phantomforce'] = {anim:BattleMoveAnims['shadowforce'].anim, prepareAnim:BattleMoveAnims['shadowforce'].prepareAnim, prepareMessage:BattleMoveAnims['shadowforce'].prepareMessage};
BattleMoveAnims['phantomforce'] = {anim:BattleMoveAnims['shadowforce'].anim, prepareAnim:BattleMoveAnims['shadowforce'].prepareAnim};
BattleMoveAnims['shadowstrike'] = {anim:BattleMoveAnims['shadowforce'].anim};
BattleMoveAnims['smackdown'] = {anim:BattleMoveAnims['rockblast'].anim};

View File

@ -115,27 +115,25 @@ class BattleScene {
activeCount = 1;
numericId = 0;
$frame: JQuery<HTMLElement>;
$battle: JQuery<HTMLElement> = null!;
$logFrame: JQuery<HTMLElement>;
$options: JQuery<HTMLElement> = null!;
$logPreempt: JQuery<HTMLElement> = null!;
$log: JQuery<HTMLElement> = null!;
$terrain: JQuery<HTMLElement> = null!;
$weather: JQuery<HTMLElement> = null!;
$bgEffect: JQuery<HTMLElement> = null!;
$bg: JQuery<HTMLElement> = null!;
$sprite: JQuery<HTMLElement> = null!;
$sprites: [JQuery<HTMLElement>, JQuery<HTMLElement>] = [null!, null!];
$spritesFront: [JQuery<HTMLElement>, JQuery<HTMLElement>] = [null!, null!];
$stat: JQuery<HTMLElement> = null!;
$fx: JQuery<HTMLElement> = null!;
$leftbar: JQuery<HTMLElement> = null!;
$rightbar: JQuery<HTMLElement> = null!;
$turn: JQuery<HTMLElement> = null!;
$messagebar: JQuery<HTMLElement> = null!;
$delay: JQuery<HTMLElement> = null!;
$hiddenMessage: JQuery<HTMLElement> = null!;
$frame: JQuery;
$battle: JQuery = null!;
$options: JQuery = null!;
log: BattleLog;
$terrain: JQuery = null!;
$weather: JQuery = null!;
$bgEffect: JQuery = null!;
$bg: JQuery = null!;
$sprite: JQuery = null!;
$sprites: [JQuery, JQuery] = [null!, null!];
$spritesFront: [JQuery, JQuery] = [null!, null!];
$stat: JQuery = null!;
$fx: JQuery = null!;
$leftbar: JQuery = null!;
$rightbar: JQuery = null!;
$turn: JQuery = null!;
$messagebar: JQuery = null!;
$delay: JQuery = null!;
$hiddenMessage: JQuery = null!;
sideConditions = [{}, {}] as [{[id: string]: Sprite[]}, {[id: string]: Sprite[]}];
@ -146,7 +144,6 @@ class BattleScene {
bgmNum = 0;
preloadCache = {} as {[url: string]: HTMLImageElement};
autoScrollOnResume = false;
messagebarOpen = false;
interruptionCount = 1;
curWeather = '';
@ -161,11 +158,11 @@ class BattleScene {
/** jQuery objects that need to finish animating */
activeAnimations = $();
constructor(battle: Battle, $frame: JQuery<HTMLElement>, $logFrame: JQuery<HTMLElement>) {
constructor(battle: Battle, $frame: JQuery, $logFrame: JQuery) {
this.battle = battle;
$frame.addClass('battle');
this.$frame = $frame;
this.$logFrame = $logFrame;
this.log = new BattleLog($logFrame[0] as HTMLDivElement, this);
let numericId = 0;
if (battle.id) {
@ -187,16 +184,10 @@ class BattleScene {
/////////////
if (this.$options) {
this.$log.empty();
this.$logPreempt.empty();
this.log.reset();
} else {
this.$logFrame.empty();
this.$options = $('<div class="battle-options"></div>');
this.$log = $('<div class="inner" role="log"></div>');
this.$logPreempt = $('<div class="inner-preempt"></div>');
this.$logFrame.append(this.$options);
this.$logFrame.append(this.$log);
this.$logFrame.append(this.$logPreempt);
$(this.log.elem).prepend(this.$options);
}
// Battle frame
@ -244,7 +235,7 @@ class BattleScene {
this.$battle.append(this.$hiddenMessage);
if (this.battle.ignoreNicks) {
this.$log.addClass('hidenicks');
this.log.setHideNicks(true);
this.$messagebar.addClass('hidenicks');
this.$hiddenMessage.addClass('hidenicks');
}
@ -258,6 +249,8 @@ class BattleScene {
this.pokemonTimeOffset = 0;
this.curTerrain = '';
this.curWeather = '';
this.log.battleParser!.perspective = this.battle.sidesSwitched ? 1 : 0;
}
animationOff() {
@ -265,7 +258,6 @@ class BattleScene {
this.stopAnimation();
this.animating = false;
this.autoScrollOnResume = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!);
this.$messagebar.empty().css({
opacity: 0,
height: 0
@ -289,9 +281,6 @@ class BattleScene {
}
this.updateWeather(true);
this.resetTurn();
if (this.autoScrollOnResume) {
this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!);
}
this.resetSideConditions();
}
pause() {
@ -474,7 +463,7 @@ class BattleScene {
} as JQuery.PlainObject;
}
waitFor(elem: JQuery<HTMLElement>) {
waitFor(elem: JQuery) {
this.activeAnimations = this.activeAnimations.add(elem);
}
@ -497,24 +486,12 @@ class BattleScene {
// Messagebar and log
/////////////////////////////////////////////////////////////////////
log(html: string, preempt?: boolean) {
let willScroll = false;
if (this.animating) willScroll = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!);
if (preempt) {
this.$logPreempt.append(html);
} else {
this.$log.append(html);
}
if (willScroll) {
this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!);
}
}
preemptCatchup() {
this.$log.append(this.$logPreempt.children().first());
this.log.preemptCatchup();
}
message(message: string, hiddenMessage?: string) {
if (!this.messagebarOpen) {
this.log('<div class="spacer battle-history"></div>');
this.log.addSpacer();
if (this.animating) {
this.$messagebar.empty();
this.$messagebar.css({
@ -552,7 +529,14 @@ class BattleScene {
this.waitFor($message);
}
this.messagebarOpen = true;
this.log('<div class="battle-history">' + message + (hiddenMessage ? hiddenMessage : '') + '</div>');
}
maybeCloseMessagebar(args: Args, kwArgs: KWArgs) {
if (this.log.battleParser!.sectionBreak(args, kwArgs)) {
if (!this.messagebarOpen) return false;
this.closeMessagebar();
return true;
}
return false;
}
closeMessagebar() {
if (this.messagebarOpen) {
@ -563,17 +547,9 @@ class BattleScene {
}, this.battle.messageFadeTime / this.acceleration);
this.waitFor(this.$messagebar);
}
return true;
}
}
unlink(userid: string, showRevealButton = false) {
if (Tools.prefs('nounlink')) return;
let $messages = $('.chatmessage-' + userid);
if (!$messages.length) return;
$messages.find('a').contents().unwrap();
if (window.BattleRoom && showRevealButton) {
$messages.hide().addClass('revealed').find('button').parent().remove();
this.log('<div class="chatmessage-' + userid + '"><button name="toggleMessages" value="' + userid + '" class="subtle"><small>(' + $messages.length + ' line' + ($messages.length > 1 ? 's' : '') + ' from ' + userid + ' hidden)</small></button></div>');
}
return false;
}
// General updating
@ -608,7 +584,6 @@ class BattleScene {
const moveAnim = BattleMoveAnims[moveid];
if (!moveAnim.prepareAnim) return;
moveAnim.prepareAnim(this, [attacker.sprite, defender.sprite]);
this.message('<small>' + moveAnim.prepareMessage!(attacker, defender) + '</small>');
}
updateGen() {
@ -630,6 +605,31 @@ class BattleScene {
}
}
getDetailsText(pokemon: Pokemon) {
let name = pokemon.side && pokemon.side.n && (this.battle.ignoreOpponent || this.battle.ignoreNicks) ? pokemon.species : pokemon.name;
if (name !== pokemon.species) {
name += ' (' + pokemon.species + ')';
}
if (pokemon === pokemon.side.active[0]) {
name += ' (active)';
} else if (pokemon.fainted) {
name += ' (fainted)';
} else {
let statustext = '';
if (pokemon.hp !== pokemon.maxhp) {
statustext += pokemon.hpDisplay();
}
if (pokemon.status) {
if (statustext) statustext += '|';
statustext += pokemon.status;
}
if (statustext) {
name += ' (' + statustext + ')';
}
}
return BattleLog.escapeHTML(name);
}
updateSidebar(side: Side) {
if (!this.animating) return;
let pokemonhtml = '';
@ -648,9 +648,11 @@ class BattleScene {
} else if (!poke) {
pokemonhtml += '<span class="picon" style="' + Tools.getPokemonIcon('pokeball') + '" title="Not revealed" aria-label="Not revealed"></span>';
} else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) {
pokemonhtml += '<span class="picon" style="' + Tools.getPokemonIcon(poke, !side.n) + ';opacity:0.6" title="' + poke.getFullName(true) + '" aria-label="' + poke.getFullName(true) + '"></span>';
const details = this.getDetailsText(poke);
pokemonhtml += '<span class="picon" style="' + Tools.getPokemonIcon(poke, !side.n) + ';opacity:0.6" title="' + details + '" aria-label="' + details + '"></span>';
} else {
pokemonhtml += '<span class="picon" style="' + Tools.getPokemonIcon(poke, !side.n) + '" title="' + poke.getFullName(true) + '" aria-label="' + poke.getFullName(true) + '"></span>';
const details = this.getDetailsText(poke);
pokemonhtml += '<span class="picon" style="' + Tools.getPokemonIcon(poke, !side.n) + '" title="' + details + '" aria-label="' + details + '"></span>';
}
if (i % 3 === 2) pokemonhtml += '</div><div class="teamicons">';
}
@ -729,7 +731,9 @@ class BattleScene {
}
side.totalPokemon = side.pokemon.length;
if (textBuf) {
this.log('<div class="chat battle-history"><strong>' + BattleLog.escapeHTML(side.name) + '\'s team:</strong> <em style="color:#445566;display:block;">' + BattleLog.escapeHTML(textBuf) + '</em></div>');
this.log.addDiv('chat battle-history',
'<strong>' + BattleLog.escapeHTML(side.name) + '\'s team:</strong> <em style="color:#445566;display:block;">' + BattleLog.escapeHTML(textBuf) + '</em>'
);
}
this.$sprites[siden].html(buf + buf2);
@ -1395,7 +1399,7 @@ class BattleScene {
BattleSound.pauseBgm();
}
destroy() {
if (this.$logFrame) this.$logFrame.empty();
this.log.destroy();
if (this.$frame) this.$frame.empty();
this.soundStop();
this.battle = null!;
@ -1427,7 +1431,7 @@ interface InitScenePos {
class Sprite {
scene: BattleScene;
$el: JQuery<HTMLElement> = null!;
$el: JQuery = null!;
sp: SpriteData;
x: number;
y: number;
@ -1493,10 +1497,10 @@ class PokemonSprite extends Sprite {
cryurl: string | undefined = undefined;
subsp: SpriteData | null = null;
$sub: JQuery<HTMLElement> | null = null;
$sub: JQuery | null = null;
isSubActive = false;
$statbar: JQuery<HTMLElement> | null = null;
$statbar: JQuery | null = null;
isBackSprite: boolean;
isMissedPokemon = false;
/**

View File

@ -141,7 +141,7 @@ const BattleStatNames = { // proper style
spe: 'Spe'
};
const BattleStats = {
atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed', accuracy: 'accuracy', evasion: 'evasiveness', spc: 'Special'
hp: 'HP', atk: 'Attack', def: 'Defense', spa: 'Special Attack', spd: 'Special Defense', spe: 'Speed', accuracy: 'accuracy', evasion: 'evasiveness', spc: 'Special'
};
const baseSpeciesChart = [

View File

@ -1,11 +1,296 @@
/**
* Battle log
*
*
* An exercise in minimalism! This is a dependency of the client, which
* requires IE9+ and uses Preact, and the replay player, which requires
* IE7+ and uses jQuery. Therefore, this has to be compatible with IE7+
* and use the DOM directly!
*
* Special thanks to PPK for QuirksMode.org, one of the few resources
* available for how to do web development in these conditions.
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license MIT
*/
class BattleLog {
elem: HTMLDivElement;
innerElem: HTMLDivElement;
scene: BattleScene | null = null;
preemptElem: HTMLDivElement = null!;
atBottom = true;
className: string;
battleParser: BattleTextParser | null = null;
/**
* -1 = spectator: "Red sent out Pikachu!" "Blue's Eevee used Tackle!"
* 0 = player 1: "Go! Pikachu!" "The opposing Eevee used Tackle!"
* 1 = player 2: "Red sent out Pikachu!" "Eevee used Tackle!"
*/
perspective: -1 | 0 | 1 = -1;
constructor(elem: HTMLDivElement, scene?: BattleScene) {
this.elem = elem;
elem.setAttribute('role', 'log');
elem.innerHTML = '';
const innerElem = document.createElement('div');
innerElem.className = 'inner';
elem.appendChild(innerElem);
this.innerElem = innerElem;
this.className = elem.className;
if (scene) {
this.scene = scene;
const preemptElem = document.createElement('div');
preemptElem.className = 'inner-preempt';
elem.appendChild(preemptElem);
this.preemptElem = preemptElem;
this.battleParser = new BattleTextParser();
}
elem.onscroll = this.onScroll;
}
onScroll = () => {
const distanceFromBottom = this.elem.scrollHeight - this.elem.scrollTop - this.elem.clientHeight;
this.atBottom = (distanceFromBottom < 30);
};
reset() {
this.innerElem.innerHTML = '';
this.atBottom = true;
}
destroy() {
this.elem.onscroll = null;
}
setHideNicks(hideNicks: boolean) {
this.elem.className = this.className + (hideNicks ? ' hidenicks' : '');
}
add(args: Args, kwArgs?: KWArgs, preempt?: boolean) {
if (kwArgs && kwArgs.silent) return;
let divClass = 'chat';
let divHTML = '';
switch (args[0]) {
case 'chat': case 'c': case 'c:':
let name, message;
let battle = this.scene && this.scene.battle;
if (args[0] === 'c:') {
name = args[2];
message = args[3];
} else {
name = args[1];
message = args[2];
}
let rank = name.charAt(0);
if (battle && battle.ignoreSpects && (rank === ' ' || rank === '+')) return;
if (battle && battle.ignoreOpponent && (rank === '\u2605' || rank === '\u2606') && toUserid(name) !== app.user.get('userid')) return;
if (window.app && app.ignore && app.ignore[toUserid(name)] && (rank === ' ' || rank === '+' || rank === '\u2605' || rank === '\u2606')) return;
let isHighlighted = window.app && app.rooms && app.rooms[battle!.roomid].getHighlight(message);
[divClass, divHTML] = this.parseChatMessage(message, name, '', isHighlighted);
if (isHighlighted) {
let notifyTitle = "Mentioned by " + name + " in " + battle!.roomid;
app.rooms[battle!.roomid].notifyOnce(notifyTitle, "\"" + message + "\"", 'highlight');
}
break;
case 'join': case 'j':
divHTML = '<small>' + BattleLog.escapeHTML(args[1]) + ' joined.</small>';
break;
case 'leave': case 'l':
divHTML = '<small>' + BattleLog.escapeHTML(args[1]) + ' left.</small>';
break;
case 'chatmsg': case '':
divHTML = BattleLog.escapeHTML(args[1]);
break;
case 'chatmsg-raw': case 'raw': case 'html':
divHTML = BattleLog.sanitizeHTML(args[1]);
break;
case 'error': case 'inactive': case 'inactiveoff':
divClass = 'chat message-error';
divHTML = BattleLog.escapeHTML(args[1]);
break;
case 'bigerror':
this.message('<div class="broadcast-red">' + BattleLog.escapeHTML(args[1]).replace(/\|/g, '<br />') + '</div>');
return;
case 'pm':
divHTML = '<strong>' + BattleLog.escapeHTML(args[1]) + ':</strong> <span class="message-pm"><i style="cursor:pointer" onclick="selectTab(\'lobby\');rooms.lobby.popupOpen(\'' + BattleLog.escapeHTML(args[2], true) + '\')">(Private to ' + BattleLog.escapeHTML(args[3]) + ')</i> ' + BattleLog.parseMessage(args[4]) + '</span>';
break;
case 'askreg':
this.addDiv('chat', '<div class="broadcast-blue"><b>Register an account to protect your ladder rating!</b><br /><button name="register" value="' + BattleLog.escapeHTML(args[1]) + '"><b>Register</b></button></div>');
return;
case 'unlink':
this.hideChatFrom(toId(args[2] || args[1]));
return;
case 'debug':
divClass = 'debug';
divHTML = '<div class="chat"><small style="color:#999">[DEBUG] ' + BattleLog.escapeHTML(args[1]) + '.</small></div>';
break;
case 'seed': case 'choice': case ':': case 'timer':
case 'J': case 'L': case 'N': case 'n': case 'spectator': case 'spectatorleave':
return;
default:
this.addBattleMessage(args, kwArgs);
return;
}
if (divHTML) this.addDiv(divClass, divHTML, preempt);
}
addBattleMessage(args: Args, kwArgs?: KWArgs) {
switch (args[0]) {
case 'warning':
this.message('<strong>Warning:</strong> ' + BattleLog.escapeHTML(args[1]));
this.message(`Bug? Report it to <a href="http://www.smogon.com/forums/showthread.php?t=3453192">the replay viewer's Smogon thread</a>`);
if (this.scene) this.scene.wait(1000);
return;
case 'variation':
this.addDiv('', '<small>Variation: <em>' + BattleLog.escapeHTML(args[1]) + '</em></small>');
break;
case 'rule':
const ruleArgs = args[1].split(': ');
this.addDiv('', '<small><em>' + BattleLog.escapeHTML(ruleArgs[0]) + (ruleArgs[1] ? ':' : '') + '</em> ' + BattleLog.escapeHTML(ruleArgs[1] || '') + '</small>');
break;
case 'rated':
this.addDiv('rated', '<strong>' + (BattleLog.escapeHTML(args[1]) || 'Rated battle') + '</strong>');
break;
case 'tier':
this.addDiv('', '<small>Format:</small> <br /><strong>' + BattleLog.escapeHTML(args[1]) + '</strong>');
break;
case 'turn':
const h2elem = document.createElement('h2');
h2elem.className = 'battle-history';
let turnMessage = this.battleParser!.parseLine(args, {}).trim();
if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) {
throw new Error("Turn message must be a heading.");
}
turnMessage = turnMessage.slice(2, -2).trim();
this.battleParser!.curLineSection = 'break';
h2elem.innerHTML = BattleLog.escapeHTML(turnMessage);
this.addNode(h2elem);
break;
default:
let line = null;
if (this.battleParser) {
line = this.battleParser.parseLine(args, kwArgs || {}, true);
}
if (line === null) {
this.addDiv('chat message-error', 'Unrecognized: |' + BattleLog.escapeHTML(args.join('|')));
return;
}
if (!line) return;
this.message(...this.parseLogMessage(line));
break;
}
}
/**
* To avoid trolling with nicknames, we can't just run this through
* parseMessage
*/
parseLogMessage(message: string): [string, string] {
const messages = message.split('\n').map(line => {
line = BattleLog.escapeHTML(line);
line = line.replace(/\*\*(.*)\*\*/, '<strong>$1</strong>');
line = line.replace(/\|\|([^\|]*)\|\|([^\|]*)\|\|/, '<abbr title="$1">$2</abbr>');
if (line.startsWith(' ')) line = '<small>' + line.trim() + '</small>';
return line;
});
return [
messages.join('<br />'),
messages.filter(message => !message.startsWith('<small>[')).join('<br />'),
];
}
message(message: string, sceneMessage = message) {
if (this.scene) this.scene.message(sceneMessage);
this.addDiv('battle-history', message);
}
addNode(node: HTMLElement, preempt?: boolean) {
(preempt ? this.preemptElem : this.innerElem).appendChild(node);
if (this.atBottom) {
this.elem.scrollTop = this.elem.scrollHeight;
}
}
addDiv(className: string, innerHTML: string, preempt?: boolean) {
const el = document.createElement('div');
el.className = className;
el.innerHTML = innerHTML;
this.addNode(el);
}
addSpacer() {
this.addDiv('spacer battle-history', '');
}
changeUhtml(id: string, html: string, forceAdd?: boolean) {
id = toId(id);
const classContains = ' uhtml-' + id + ' ';
let elements = [] as HTMLDivElement[];
for (const node of this.innerElem.childNodes as any) {
if (node.className && (' ' + node.className + ' ').includes(classContains)) {
elements.push(node);
}
}
for (const node of this.preemptElem.childNodes as any) {
if (node.className && (' ' + node.className + ' ').includes(classContains)) {
elements.push(node);
}
}
if (html && elements.length && !forceAdd) {
for (const element of elements) {
element.innerHTML = BattleLog.sanitizeHTML(html);
}
return;
}
for (const element of elements) {
element.parentElement!.removeChild(element);
}
if (html) {
this.addDiv('notice uhtml-' + id, BattleLog.sanitizeHTML(html));
}
}
hideChatFrom(userid: ID, showRevealButton = true) {
const classStart = 'chat chatmessage-' + userid + ' ';
let lastNode;
let count = 0;
for (const node of this.innerElem.childNodes as any) {
if (node.className && (node.className + ' ').startsWith(classStart)) {
node.style.display = 'none';
node.className = 'revealed ' + node.className;
count++;
}
lastNode = node;
}
for (const node of this.preemptElem.childNodes as any) {
if (node.className && (node.className + ' ').startsWith(classStart)) {
node.style.display = 'none';
node.className = 'revealed ' + node.className;
count++;
}
lastNode = node;
}
if (!count || !showRevealButton) return;
const button = document.createElement('button');
button.name = 'toggleMessages';
button.value = userid;
button.className = 'subtle';
button.innerHTML = '<small>(' + count + ' line' + (count > 1 ? 's' : '') + ' from ' + userid + ' hidden)</small>';
lastNode.appendChild(document.createTextNode(' '));
lastNode.appendChild(button);
}
preemptCatchup() {
if (!this.preemptElem.firstChild) return;
this.innerElem.appendChild(this.preemptElem.firstChild);
}
static escapeFormat(formatid: string): string {
let atIndex = formatid.indexOf('@@@');
if (atIndex >= 0) {
@ -77,16 +362,24 @@ class BattleLog {
return this.colorCache[name];
}
static parseChatMessage(message: string, name: string, timestamp: string, isHighlighted?: boolean, $chatElem?: any) {
let showMe = !((Tools.prefs('chatformatting') || {}).hideme);
static prefs(name: string) {
// @ts-ignore
if (window.Storage && Storage.prefs) return Storage.prefs(name);
// @ts-ignore
if (window.PS) return PS.prefs[name];
return undefined;
}
parseChatMessage(message: string, name: string, timestamp: string, isHighlighted?: boolean) {
let showMe = !(BattleLog.prefs('chatformatting') || {}).hideme;
let group = ' ';
if (!/[A-Za-z0-9]/.test(name.charAt(0))) {
// Backwards compatibility
group = name.charAt(0);
name = name.substr(1);
}
let color = this.hashColor(toId(name));
let clickableName = '<small>' + this.escapeHTML(group) + '</small><span class="username" data-name="' + this.escapeHTML(name) + '">' + this.escapeHTML(name) + '</span>';
let color = BattleLog.hashColor(toId(name));
let clickableName = '<small>' + BattleLog.escapeHTML(group) + '</small><span class="username" data-name="' + BattleLog.escapeHTML(name) + '">' + BattleLog.escapeHTML(name) + '</span>';
let hlClass = isHighlighted ? ' highlighted' : '';
let mineClass = (window.app && app.user && app.user.get('name') === name ? ' mine' : '');
@ -104,99 +397,44 @@ class BattleLog {
switch (cmd) {
case 'me':
if (!showMe) return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + this.parseMessage(' ' + target) + '</em></div>';
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + this.parseMessage(' ' + target) + '</i></em></div>';
if (!showMe) return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + BattleLog.parseMessage(' ' + target) + '</em>'];
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + BattleLog.parseMessage(' ' + target) + '</i></em>'];
case 'mee':
if (!showMe) return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + this.parseMessage(' ' + target).slice(1) + '</em></div>';
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + this.parseMessage(' ' + target).slice(1) + '</i></em></div>';
if (!showMe) return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>/me' + BattleLog.parseMessage(' ' + target).slice(1) + '</em>'];
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">&bull;</strong> <em>' + clickableName + '<i>' + BattleLog.parseMessage(' ' + target).slice(1) + '</i></em>'];
case 'invite':
let roomid = toRoomid(target);
return [
'<div class="chat">' + timestamp + '<em>' + clickableName + ' invited you to join the room "' + roomid + '"</em></div>',
'<div class="notice"><button name="joinRoom" value="' + roomid + '">Join ' + roomid + '</button></div>'
];
return ['chat', timestamp + '<em>' + clickableName + ' invited you to join the room "' + roomid + '"</em>' +
'<div class="notice"><button name="joinRoom" value="' + roomid + '">Join ' + roomid + '</button></div>'];
case 'announce':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <span class="message-announce">' + this.parseMessage(target) + '</span></div>';
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <span class="message-announce">' + BattleLog.parseMessage(target) + '</span>'];
case 'log':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<span class="message-log">' + this.parseMessage(target) + '</span></div>';
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<span class="message-log">' + BattleLog.parseMessage(target) + '</span>'];
case 'data-pokemon':
let buf = '<li class="result">';
let template = Tools.getTemplate(target);
if (!template.abilities || !template.baseStats) return '[not supported in replays]';
buf += '<span class="col numcol">' + (template.tier || Tools.getTemplate(template.baseSpecies).tier) + '</span> ';
buf += '<span class="col iconcol"><span style="' + Tools.getPokemonIcon(template) + '"></span></span> ';
buf += '<span class="col pokemonnamecol" style="white-space:nowrap"><a href="https://pokemonshowdown.com/dex/pokemon/' + template.id + '" target="_blank">' + template.species + '</a></span> ';
buf += '<span class="col typecol">';
if (template.types) for (let i = 0; i < template.types.length; i++) {
buf += Tools.getTypeIcon(template.types[i]);
}
buf += '</span> ';
buf += '<span style="float:left;min-height:26px">';
if (template.abilities['1']) {
buf += '<span class="col twoabilitycol">' + template.abilities['0'] + '<br />' + template.abilities['1'] + '</span>';
} else {
buf += '<span class="col abilitycol">' + template.abilities['0'] + '</span>';
}
if (template.abilities['S']) {
buf += '<span class="col twoabilitycol' + (template.unreleasedHidden ? ' unreleasedhacol' : '') + '"><em>' + template.abilities['H'] + '<br />' + template.abilities['S'] + '</em></span>';
} else if (template.abilities['H']) {
buf += '<span class="col abilitycol' + (template.unreleasedHidden ? ' unreleasedhacol' : '') + '"><em>' + template.abilities['H'] + '</em></span>';
} else {
buf += '<span class="col abilitycol"></span>';
}
buf += '</span>';
buf += '<span style="float:left;min-height:26px">';
buf += '<span class="col statcol"><em>HP</em><br />' + template.baseStats.hp + '</span> ';
buf += '<span class="col statcol"><em>Atk</em><br />' + template.baseStats.atk + '</span> ';
buf += '<span class="col statcol"><em>Def</em><br />' + template.baseStats.def + '</span> ';
buf += '<span class="col statcol"><em>SpA</em><br />' + template.baseStats.spa + '</span> ';
buf += '<span class="col statcol"><em>SpD</em><br />' + template.baseStats.spd + '</span> ';
buf += '<span class="col statcol"><em>Spe</em><br />' + template.baseStats.spe + '</span> ';
let bst = 0;
for (const i in template.baseStats) bst += template.baseStats[i as StatName];
buf += '<span class="col bstcol"><em>BST<br />' + bst + '</em></span> ';
buf += '</span>';
buf += '</li>';
return '<div class="message"><ul class="utilichart">' + buf + '<li style=\"clear:both\"></li></ul></div>';
case 'data-item':
if (!window.BattleSearch) return '[not supported in replays]';
return '<div class="message"><ul class="utilichart">' + BattleSearch.renderItemRow(Tools.getItem(target), 0, 0) + '<li style=\"clear:both\"></li></ul></div>';
case 'data-ability':
if (!window.BattleSearch) return '[not supported in replays]';
return '<div class="message"><ul class="utilichart">' + BattleSearch.renderAbilityRow(Tools.getAbility(target), 0, 0) + '<li style=\"clear:both\"></li></ul></div>';
case 'data-move':
if (!window.BattleSearch) return '[not supported in replays]';
return '<div class="message"><ul class="utilichart">' + BattleSearch.renderMoveRow(Tools.getMove(target), 0, 0) + '<li style=\"clear:both\"></li></ul></div>';
return ['chat message-error', '[outdated code no longer supported]'];
case 'text':
return '<div class="chat">' + this.parseMessage(target) + '</div>';
return ['chat', BattleLog.parseMessage(target)];
case 'error':
return '<div class="chat message-error">' + this.escapeHTML(target) + '</div>';
return ['chat message-error', BattleLog.escapeHTML(target)];
case 'html':
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.sanitizeHTML(target) + '</em></div>';
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.sanitizeHTML(target) + '</em>'];
case 'uhtml':
case 'uhtmlchange':
let parts = target.split(',');
let $elements = $chatElem.find('div.uhtml-' + toId(parts[0]));
let html = parts.slice(1).join(',');
if (!html) {
$elements.remove();
} else if (!$elements.length) {
$chatElem.append('<div class="chat uhtml-' + toId(parts[0]) + '">' + BattleLog.sanitizeHTML(html) + '</div>');
} else if (cmd === 'uhtmlchange') {
$elements.html(BattleLog.sanitizeHTML(html));
} else {
$elements.remove();
$chatElem.append('<div class="chat uhtml-' + toId(parts[0]) + '">' + BattleLog.sanitizeHTML(html) + '</div>');
}
return '';
let html = parts.slice(1).join(',').trim();
this.changeUhtml(parts[0], html, cmd === 'uhtml');
return ['', ''];
case 'raw':
return '<div class="chat">' + BattleLog.sanitizeHTML(target) + '</div>';
return ['chat', BattleLog.sanitizeHTML(target)];
default:
// Not a command or unsupported. Parsed as a normal chat message.
if (!name) {
return '<div class="chat' + hlClass + '">' + timestamp + '<em>' + this.parseMessage(message) + '</em></div>';
return ['chat' + hlClass, timestamp + '<em>' + BattleLog.parseMessage(message) + '</em>'];
}
return '<div class="chat chatmessage-' + toId(name) + hlClass + mineClass + '">' + timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + this.parseMessage(message) + '</em></div>';
return ['chat chatmessage-' + toId(name) + hlClass + mineClass, timestamp + '<strong style="' + color + '">' + clickableName + ':</strong> <em>' + BattleLog.parseMessage(message) + '</em>'];
}
}
@ -207,7 +445,7 @@ class BattleLog {
if (str.substr(0, 3) === '<< ') return this.escapeHTML(str);
str = formatText(str);
let options = Tools.prefs('chatformatting') || {};
let options = BattleLog.prefs('chatformatting') || {};
if (options.hidelinks) {
str = str.replace(/<a[^>]*>/g, '<u>').replace(/<\/a>/g, '</u>');
@ -484,3 +722,5 @@ class BattleLog {
return 'data:text/plain;base64,' + encodeURIComponent(btoa(unescape(encodeURIComponent(this.createReplayFile(room)))));
}
}
exports.BattleLog = BattleLog;

View File

@ -7,12 +7,14 @@ class BattleSceneStub {
timeOffset: number = NaN;
interruptionCount: number = NaN;
messagebarOpen: boolean = false;
log = {add: (args: Args, kwargs?: KWArgs) => {}} as BattleLog;
abilityActivateAnim(pokemon: Pokemon, result: string): void { }
addPokemonSprite(pokemon: Pokemon) { return null!; }
addSideCondition(siden: number, id: ID, instant?: boolean | undefined): void { }
animationOff(): void { }
animationOn(): void { }
maybeCloseMessagebar(args: Args, kwArgs: KWArgs): boolean { return false; }
closeMessagebar(): void { }
damageAnim(pokemon: Pokemon, damage: string | number): void { }
destroy(): void { }
@ -20,7 +22,6 @@ class BattleSceneStub {
healAnim(pokemon: Pokemon, damage: string | number): void { }
hideJoinButtons(): void { }
incrementTurn(): void { }
log(html: string, preempt?: boolean | undefined): void { }
message(message: string, hiddenMessage?: string | undefined): void { }
pause(): void { }
preemptCatchup(): void { }

902
src/battle-text-parser.ts Normal file
View File

@ -0,0 +1,902 @@
declare const BattleText: {[id: string]: {[templateName: string]: string}};
type Args = [string, ...string[]];
type KWArgs = {[kw: string]: string};
class BattleTextParser {
p1 = "Player 1";
p2 = "Player 2";
perspective: 0 | 1;
gen = 7;
curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor' = 'break';
lowercaseRegExp: RegExp | null | undefined = undefined;
constructor(perspective: 0 | 1 = 0) {
this.perspective = perspective;
}
fixLowercase(input: string) {
if (this.lowercaseRegExp === undefined) {
const prefixes = ['pokemon', 'opposingPokemon', 'team', 'opposingTeam'].map(templateId => {
const template = BattleText.default[templateId];
if (template.charAt(0) === 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, '\\$&');
}
pokemon(pokemon: string) {
if (!pokemon) return '';
let side;
if (pokemon.startsWith('p1')) side = 0;
else if (pokemon.startsWith('p2')) side = 1;
else return '???pokemon:' + pokemon + '???';
if (pokemon.charAt(3) === ':') pokemon = pokemon.slice(4).trim();
else if (pokemon.charAt(2) === ':') pokemon = pokemon.slice(3).trim();
else return '???pokemon:' + pokemon + '???';
let template = BattleText.default[side === this.perspective ? 'pokemon' : 'opposingPokemon'];
return template.replace('[NICKNAME]', pokemon);
}
pokemonName(pokemon: string) {
if (!pokemon) return '';
if (!pokemon.startsWith('p1') && !pokemon.startsWith('p2')) return '???pokemon:' + pokemon + '???';
if (pokemon.charAt(3) === ':') pokemon = pokemon.slice(4).trim();
else if (pokemon.charAt(2) === ':') pokemon = pokemon.slice(3).trim();
else return '???pokemon:' + pokemon + '???';
return pokemon;
}
pokemonFull(pokemon: string, details: string) {
if (pokemon.startsWith('p1')) {}
else if (pokemon.startsWith('p2')) {}
else return '???pokemon:' + pokemon + '???';
let nickname;
if (pokemon.charAt(3) === ':') nickname = pokemon.slice(4).trim();
else if (pokemon.charAt(2) === ':') nickname = pokemon.slice(3).trim();
else return '???pokemon:' + pokemon + '???';
let species = details.split(',')[0];
if (nickname === species) return [pokemon.slice(0, 2), `**${species}**`];
return [pokemon.slice(0, 2), `${nickname} (**${species}**)`];
}
trainer(side: string) {
if (side === 'p1') return this.p1;
if (side === 'p2') return this.p2;
return '???side:' + side + '???';
}
team(side: string) {
if ((side === 'p1' && this.perspective === 0) ||
(side === 'p2' && this.perspective === 1)) {
return BattleText.default.team;
}
return BattleText.default.opposingTeam;
}
own(side: string) {
if (side === 'p1' && this.perspective === 0) return 'OWN';
if (side === 'p2' && this.perspective === 1) return 'OWN';
return '';
}
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 = this.effectId(namespace);
if (BattleText[id] && BattleText[id][type]) {
if (BattleText[id][type].charAt(1) === '.') type = BattleText[id][type].slice(2) as ID;
if (BattleText[id][type].charAt(0) === '#') id = BattleText[id][type].slice(1) as ID;
return BattleText[id][type] + '\n';
}
}
if (!BattleText.default[type]) return '';
return BattleText.default[type] + '\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';
}
lineSection(args: Args, kwArgs: KWArgs) {
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':
return 'major';
case 'switchout': case 'faint':
return 'preMajor';
case '-zpower':
return 'postMajor';
case '-damage': {
const id = this.effectId(kwArgs.from);
if (id === 'confusion') return 'major';
}
case '-curestatus': {
const id = this.effectId(kwArgs.from);
if (id === 'naturalcure') return 'preMajor';
}
case '-start': {
const id = this.effectId(kwArgs.from);
if (id === 'protean') return 'preMajor';
}
case '-activate': {
const id = this.effectId(kwArgs.from);
if (id === 'confusion' || id === 'attract') return 'preMajor';
}
}
return (cmd.charAt(0) === '-' ? '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;
}
}
parseLine(args: Args, kwArgs: KWArgs, noSectionBreak?: boolean) {
let buf = !noSectionBreak && this.sectionBreak(args, kwArgs) ? '\n' : '';
return buf + this.fixLowercase(this.parseLineInner(args, kwArgs) || '');
}
parseLineInner(args: Args, kwArgs: KWArgs) {
let cmd = args[0];
switch (cmd) {
case 'player': {
const [, side, name] = args;
if (side === 'p1' && name) {
this.p1 = name;
} else if (side === 'p2' && name) {
this.p2 = name;
}
return '';
}
case 'gen': {
const [, number] = args;
this.gen = parseInt(number, 10);
return '';
}
case 'turn': {
const [, number] = args;
return this.template('turn').replace('[NUMBER]', number) + '\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 '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;
}
} 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));
}
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 = '';
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;
let id = this.effectId(effect);
switch (id) {
case 'damp': case 'dazzling': case 'queenlymajesty':
// thanks Marty
[pokemon, kwArgs.of] = [kwArgs.of, pokemon];
break;
}
const template = this.template('cant', effect);
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move);
}
case 'faint': {
const [, pokemon] = args;
const template = this.template('faint');
return template.replace('[POKEMON]', this.pokemon(pokemon));
}
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 = this.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 number = id.slice(9);
const template = this.template('start', 'stockpile');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', number);
}
if (id.startsWith('perish')) {
const number = id.slice(6);
const template = this.template('activate', 'perishsong');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[NUMBER]', number);
}
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 = 'activate';
let template = '';
if (templateId === 'start' && kwArgs.from && kwArgs.from.startsWith('item:')) {
template = this.template('startFromItem', effect);
}
if (!template) template = this.template(templateId, effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(kwArgs.from)).replace('[MOVE]', arg3).replace('[SOURCE]', this.pokemon(kwArgs.of));
}
case '-end': {
let [, pokemon, effect] = args;
const line1 = this.maybeAbility(effect, pokemon) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let id = this.effectId(effect);
if (id === 'doomdesire' || id === 'futuresight') {
const template = this.template('activate', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[TARGET]', this.pokemon(kwArgs.of));
}
let templateId = 'end';
let template = '';
if (kwArgs.from && 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(kwArgs.from)).replace('[SOURCE]', this.pokemon(kwArgs.of));
}
case '-ability': {
let [, pokemon, ability, oldAbility, arg4] = args;
let line1 = '';
if (oldAbility && (oldAbility.startsWith('p1') || oldAbility.startsWith('p2') || oldAbility === 'boost')) {
arg4 = oldAbility;
oldAbility = '';
}
if (oldAbility) line1 += this.ability(oldAbility, pokemon);
line1 += this.ability(ability, pokemon);
if (kwArgs.fail) {
const template = this.template('block', ability);
return line1 + template;
}
if (kwArgs.from) {
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 = this.effectId(ability);
if (id === 'unnerve') {
const template = this.template('start', ability);
return line1 + template.replace('[TEAM]', this.team(arg4 && arg4.slice(0, 2)));
}
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': {
let [, pokemon, item] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const id = this.effectId(kwArgs.from);
if (['thief', 'covet', 'bestow'].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(kwArgs.of));
}
if (id === 'frisk') {
const template = this.template(kwArgs.of && pokemon && kwArgs.of !== pokemon ? 'activate' : 'activateNoTarget', "Frisk");
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(item)).replace('[TARGET]', this.pokemon(kwArgs.of));
}
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, arg3] = 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 = this.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));
}
const template = this.template('end', item, 'NODEFAULT');
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);
const template = this.template('start', status);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-curestatus': {
const [, pokemon, status] = args;
if (this.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 && 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));
}
const template = this.template('end', 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 = this.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));
}
const template = this.template('start', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of));
}
case '-sidestart': {
let [, side, effect] = args;
const template = this.template('start', effect);
return template.replace('[TEAM]', this.team(side));
}
case '-sideend': {
let [, side, effect] = args;
const template = this.template('end', effect);
return template.replace('[TEAM]', this.team(side));
}
case '-weather': {
const [, weather] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of);
if (!weather || weather === 'none') {
return this.template('end', kwArgs.from);
}
if (kwArgs.upkeep) {
return this.template('activate', weather);
}
if (!kwArgs.of) {
return this.template('start', weather);
}
const template = this.template('startFromAbility', weather);
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of));
}
case '-fieldstart': case '-fieldactivate': {
const [, effect] = args;
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of);
const templateId = cmd.slice(6);
const template = this.template(templateId, effect, 'NODEFAULT');
return line1 + template.replace('[POKEMON]', this.pokemon(kwArgs.of));
}
case '-fieldend': {
let [, effect] = args;
return this.template('end', effect, 'NODEFAULT');
}
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, arg4, arg5] = args;
let id = this.effectId(effect);
if (id === 'celebrate') {
return this.template('activate', 'celebrate').replace('[TRAINER]', this.trainer(pokemon.slice(0, 2)));
}
if (!arg5 && (id === 'spite' || id === 'skillswap')) {
[target, arg4, arg5] = [pokemon, target, arg4];
} else if (!arg4 && ['grudge', 'forewarn', 'magnitude', 'sketch', 'persistent', 'symbiosis', 'safetygoggles', 'matblock', 'safetygoggles'].includes(id)) {
[target, arg4] = [pokemon, target];
} else if (!target && ['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) || this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
if (id === 'wonderguard') {
return line1 + this.template('immune');
}
if (['bind', 'wrap', 'clamp', 'whirlpool', 'firespin', 'magmastorm', 'sandtomb', 'infestation', 'charge', 'fairylock', 'trapped'].includes(id)) {
const template = this.template('start', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[SOURCE]', this.pokemon(kwArgs.of));
}
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 (kwArgs.block) {
return this.template('fail');
}
if (['ingrain', 'quickguard', 'wideguard', 'craftyshield', 'matblock', 'protect', 'mist', 'safeguard', 'electricterrain', 'mistyterrain', 'psychicterrain', 'telepathy', 'stickyhold', 'suctioncups', 'aromaveil', 'flowerveil', 'sweetveil', 'disguise', 'safetygoggles', 'protectivepads'].includes(id)) {
const template = this.template('block', effect);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', arg4);
}
let templateId = 'activate';
if (id === 'forewarn' && pokemon === target) {
templateId = 'activateNoTarget';
}
let template = this.template(templateId, effect, 'NODEFAULT');
if (!template) {
template = this.template('activate');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[EFFECT]', this.effect(effect));
}
if (id === 'brickbreak') {
template = template.replace('[TEAM]', this.team(target.slice(0, 2)));
}
if (id === 'spite' || id === 'grudge' || id === 'forewarn' || id === 'sketch') {
template = template.replace('[MOVE]', arg4).replace('[NUMBER]', arg5);
}
if (id === 'magnitude') {
template = template.replace('[NUMBER]', arg4);
}
if (id === 'symbiosis') {
template = template.replace('[ITEM]', arg4);
}
if (id === 'skillswap') {
line1 += this.ability(arg4, pokemon);
line1 += this.ability(arg5, target);
}
if (id === 'mummy') {
line1 += this.ability(arg4, target);
line1 += this.ability("Mummy", target);
template = this.template('changeAbility', "Mummy");
}
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, newHealth, percentage] = args;
let template = this.template('damage', kwArgs.from, 'NODEFAULT');
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
const id = this.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('damageFromItem');
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[ITEM]', this.effect(kwArgs.from));
}
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));
}
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': {
const [, pokemon, stat, number] = args;
const statName = BattleStats[stat as StatName] || "stats";
const amount = parseInt(number);
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 && kwArgs.from.startsWith('item:')) {
templateId += 'FromItem';
}
const template = this.template(templateId, kwArgs.from);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', statName);
}
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 = this.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 '-fail': {
let [, pokemon, effect, stat] = args;
let id = this.effectId(effect);
let blocker = this.effectId(kwArgs.from);
const line1 = this.maybeAbility(kwArgs.from, kwArgs.of || pokemon);
let templateId = 'block';
if (['desolateland', 'primordialsea'].includes(blocker) &&
!['sunnyday', 'raindance', 'sandstorm', 'hail'].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');
if (this.effectId(kwArgs.from) === 'flowerveil') {
template = this.template('block', kwArgs.from);
pokemon = kwArgs.of;
}
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[STAT]', stat);
}
templateId = 'fail';
if (['brn', 'frz', 'par', 'psn', 'slp', 'substitute'].includes(id)) {
templateId = 'alreadyStarted';
}
if (kwArgs.heavy) templateId = 'failTooHeavy';
if (kwArgs.weak) templateId = 'fail';
if (kwArgs.forme) templateId = 'failWrongForme';
template = this.template(templateId);
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon));
}
case '-immune': {
let [, pokemon, effect] = args;
const line1 = this.maybeAbility(effect, kwArgs.of || pokemon);
let template = this.template('block', effect);
if (!template) {
const templateId = kwArgs.ohko ? 'immuneOHKO' : 'immune';
template = this.template(pokemon ? templateId : 'immuneNoPokemon', effect);
}
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 '-nothing': {
return this.template('activate', 'splash');
}
case '-notarget': {
return this.template('noTarget');
}
case '-mega': case '-primal': {
const [, pokemon, species, item] = args;
let id = '';
if (species === 'Rayquaza') id = 'dragonascent';
if (!id && cmd === '-mega' && this.gen < 7) cmd = '-megaGen6';
let template = this.template(cmd.slice(1));
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);
}
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 [, number] = args;
if (number === '1') {
return this.template('hitCountSingular');
}
return this.template('hitCount').replace('[NUMBER]', number);
}
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;
}
}
}
}
exports.BattleTextParser = BattleTextParser;

File diff suppressed because it is too large Load Diff