(function ($) {
var BattleRoom = this.BattleRoom = ConsoleRoom.extend({
type: 'battle',
title: '',
minWidth: 320,
minMainWidth: 956,
maxWidth: 1180,
initialize: function (data) {
this.choice = undefined;
/** are move/switch/team-preview controls currently being shown? */
this.controlsShown = false;
this.battlePaused = false;
this.autoTimerActivated = false;
this.isSideRoom = Dex.prefs('rightpanelbattles');
this.$el.addClass('ps-room-opaque').html('
Battle is here
Connecting...
');
this.$battle = this.$el.find('.battle');
this.$controls = this.$el.find('.battle-controls');
this.$chatFrame = this.$el.find('.battle-log');
this.$chatAdd = this.$el.find('.battle-log-add');
this.$foeHint = this.$el.find('.foehint');
BattleSound.setMute(Dex.prefs('mute'));
this.battle = new Battle({
id: this.id,
$frame: this.$battle,
$logFrame: this.$chatFrame
});
this.battle.roomid = this.id;
this.battle.joinButtons = true;
this.tooltips = this.battle.scene.tooltips;
this.tooltips.listen(this.$controls);
var self = this;
this.battle.subscribe(function () { self.updateControls(); });
this.users = {};
this.userCount = { users: 0 };
this.$userList = this.$('.userlist');
this.userList = new UserList({
el: this.$userList,
room: this
});
this.userList.construct();
this.$chat = this.$chatFrame.find('.inner');
this.$options = this.battle.scene.$options.html('');
},
events: {
'click .replayDownloadButton': 'clickReplayDownloadButton',
'change input[name=megaevox]': 'uncheckMegaEvoY',
'change input[name=megaevoy]': 'uncheckMegaEvoX',
'change input[name=zmove]': 'updateZMove',
'change input[name=dynamax]': 'updateMaxMove'
},
battleEnded: false,
join: function () {
app.send('/join ' + this.id);
},
showChat: function () {
this.$('.battle-chat-toggle').attr('name', 'hideChat').html('Battle ');
this.$el.addClass('showing-chat');
},
hideChat: function () {
this.$('.battle-chat-toggle').attr('name', 'showChat').html(' Chat');
this.$el.removeClass('showing-chat');
},
leave: function () {
if (!this.expired) app.send('/noreply /leave ' + this.id);
if (this.battle) this.battle.destroy();
},
requestLeave: function (e) {
if ((this.side || this.requireForfeit) && this.battle && !this.battleEnded && !this.expired && !this.battle.forfeitPending) {
app.addPopup(ForfeitPopup, { room: this, sourceEl: e && e.currentTarget, gameType: 'battle' });
return false;
}
return true;
},
updateLayout: function () {
var width = this.$el.width();
if (width < 950 || this.battle.hardcoreMode) {
this.battle.messageShownTime = 500;
} else {
this.battle.messageShownTime = 1;
}
if (width && width < 640) {
var scale = (width / 640);
this.$battle.css('transform', 'scale(' + scale + ')');
this.$foeHint.css('transform', 'scale(' + scale + ')');
this.$controls.css('top', 360 * scale + 10);
} else {
this.$battle.css('transform', 'none');
this.$foeHint.css('transform', 'none');
this.$controls.css('top', 370);
}
this.$el.toggleClass('small-layout', width < 830);
this.$el.toggleClass('tiny-layout', width < 640);
if (this.$chat) this.$chatFrame.scrollTop(this.$chat.height());
},
show: function () {
Room.prototype.show.apply(this, arguments);
this.updateLayout();
},
receive: function (data) {
this.add(data);
},
focus: function (e) {
this.tooltips.hideTooltip();
if (this.battle.paused && !this.battlePaused) {
if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
this.battle.play();
}
ConsoleRoom.prototype.focus.call(this, e);
},
blur: function () {
this.battle.pause();
},
init: function (data) {
var log = data.split('\n');
if (data.substr(0, 6) === '|init|') log.shift();
if (log.length && log[0].substr(0, 7) === '|title|') {
this.title = log[0].substr(7);
log.shift();
app.roomTitleChanged(this);
}
if (this.battle.stepQueue.length) return;
this.battle.stepQueue = log;
this.battle.seekTurn(Infinity, true);
if (this.battle.ended) this.battleEnded = true;
this.updateLayout();
this.updateControls();
},
add: function (data) {
if (!data) return;
if (data.substr(0, 6) === '|init|') {
return this.init(data);
}
if (data.substr(0, 11) === '|cantleave|') {
this.requireForfeit = true;
return;
}
if (data.substr(0, 12) === '|allowleave|') {
this.requireForfeit = false;
return;
}
if (data.substr(0, 9) === '|request|') {
data = data.slice(9);
var requestData = null;
var choiceText = null;
var nlIndex = data.indexOf('\n');
if (/[0-9]/.test(data.charAt(0)) && data.charAt(1) === '|') {
// message format:
// |request|CHOICEINDEX|CHOICEDATA
// REQUEST
// This is backwards compatibility with old code that violates the
// expectation that server messages can be streamed line-by-line.
// Please do NOT EVER push protocol changes without a pull request.
// https://github.com/Zarel/Pokemon-Showdown/commit/e3c6cbe4b91740f3edc8c31a1158b506f5786d72#commitcomment-21278523
choiceText = '?';
data = data.slice(2, nlIndex);
} else if (nlIndex >= 0) {
// message format:
// |request|REQUEST
// |sentchoice|CHOICE
if (data.slice(nlIndex + 1, nlIndex + 13) === '|sentchoice|') {
choiceText = data.slice(nlIndex + 13);
}
data = data.slice(0, nlIndex);
}
try {
requestData = JSON.parse(data);
} catch (err) {}
return this.receiveRequest(requestData, choiceText);
}
var log = data.split('\n');
for (var i = 0; i < log.length; i++) {
var logLine = log[i];
if (logLine === '|') {
this.callbackWaiting = false;
this.controlsShown = false;
this.$controls.html('');
}
if (logLine.substr(0, 10) === '|callback|') {
// TODO: Maybe a more sophisticated UI for this.
// In singles, this isn't really necessary because some elements of the UI will be
// immediately disabled. However, in doubles/triples it might not be obvious why
// the player is being asked to make a new decision without the following messages.
var args = logLine.substr(10).split('|');
var pokemon = isNaN(Number(args[1])) ? this.battle.getPokemon(args[1]) : this.battle.nearSide.active[args[1]];
var requestData = this.request.active[pokemon ? pokemon.slot : 0];
this.choice = undefined;
switch (args[0]) {
case 'trapped':
requestData.trapped = true;
var pokeName = pokemon.side.n === 0 ? BattleLog.escapeHTML(pokemon.name) : "The opposing " + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name));
this.battle.stepQueue.push('|message|' + pokeName + ' is trapped and cannot switch!');
break;
case 'cant':
for (var i = 0; i < requestData.moves.length; i++) {
if (requestData.moves[i].id === args[3]) {
requestData.moves[i].disabled = true;
}
}
args.splice(1, 1, pokemon.getIdent());
this.battle.stepQueue.push('|' + args.join('|'));
break;
}
} else if (logLine.substr(0, 7) === '|title|') {
// empty
} else if (logLine.substr(0, 5) === '|win|' || logLine === '|tie') {
this.battleEnded = true;
this.battle.stepQueue.push(logLine);
} else if (logLine.substr(0, 6) === '|chat|' || logLine.substr(0, 3) === '|c|' || logLine.substr(0, 4) === '|c:|' || logLine.substr(0, 9) === '|chatmsg|' || logLine.substr(0, 10) === '|inactive|') {
this.battle.instantAdd(logLine);
} else {
this.battle.stepQueue.push(logLine);
}
}
this.battle.add();
if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
this.updateControls();
},
toggleMessages: function (user) {
var $messages = $('.chatmessage-' + user + '.revealed');
var $button = $messages.find('button');
if (!$messages.is(':hidden')) {
$messages.hide();
$button.html('(' + ($messages.length) + ' line' + ($messages.length > 1 ? 's' : '') + 'from ' + user + ')');
$button.parent().show();
} else {
$button.html('(Hide ' + ($messages.length) + ' line' + ($messages.length > 1 ? 's' : '') + ' from ' + user + ')');
$button.parent().removeClass('revealed');
$messages.show();
}
},
setHardcoreMode: function (mode) {
this.battle.setHardcoreMode(mode);
var id = '#' + this.el.id + ' ';
this.$('.hcmode-style').remove();
this.updateLayout(); // set animation delay
if (mode) this.$el.prepend('');
if (this.choice && this.choice.waiting) {
this.updateControlsForPlayer();
}
},
/*********************************************************
* Battle stuff
*********************************************************/
updateControls: function () {
if (this.battle.scene.customControls) return;
var controlsShown = this.controlsShown;
var switchViewpointButton = '';
this.controlsShown = false;
if (this.battle.seeking !== null) {
// battle is seeking
this.$controls.html('');
return;
} else if (!this.battle.atQueueEnd) {
// battle is playing or paused
if (!this.side || this.battleEnded) {
// spectator
if (this.battle.paused) {
// paused
this.$controls.html(
' ' +
'
' +
switchViewpointButton
);
} else {
// playing
this.$controls.html(
' ' +
'
' +
switchViewpointButton
);
}
} else {
// is a player
this.$controls.html('' + this.getTimerHTML() + '
');
}
return;
}
if (this.battle.ended) {
var replayDownloadButton = ' Download replay
';
// battle has ended
if (this.side) {
// was a player
this.closeNotification('choice');
this.$controls.html('' + replayDownloadButton + '
');
} else {
this.$controls.html('' + replayDownloadButton + '
' + switchViewpointButton + '
');
}
} else if (this.side) {
// player
this.controlsShown = true;
if (!controlsShown || this.choice === undefined || this.choice && this.choice.waiting) {
// don't update controls (and, therefore, side) if `this.choice === null`: causes damage miscalculations
this.updateControlsForPlayer();
} else {
this.updateTimer();
}
} else if (!this.battle.nearSide.name || !this.battle.farSide.name) {
// empty battle
this.$controls.html('Waiting for players...
');
} else {
// full battle
if (this.battle.paused) {
// paused
this.$controls.html(
' ' +
'
' +
switchViewpointButton + 'Waiting for players...
'
);
} else {
// playing
this.$controls.html(
' ' +
'
' +
switchViewpointButton + 'Waiting for players...
'
);
}
}
// This intentionally doesn't happen if the battle is still playing,
// since those early-return.
app.topbar.updateTabbar();
},
updateControlsForPlayer: function () {
this.callbackWaiting = true;
var act = '';
var switchables = [];
if (this.request) {
// TODO: investigate when to do this
this.updateSide();
if (this.request.ally) {
this.addAlly(this.request.ally);
}
act = this.request.requestType;
if (this.request.side) {
switchables = this.battle.myPokemon;
}
if (!this.finalDecision) this.finalDecision = !!this.request.noCancel;
}
if (this.choice && this.choice.waiting) {
act = '';
}
var type = this.choice ? this.choice.type : '';
// The choice object:
// !this.choice = nothing has been chosen
// this.choice.choices = array of choice strings
// this.choice.switchFlags = dict of pokemon indexes that have a switch pending
// this.choice.switchOutFlags = ???
// this.choice.freedomDegrees = in a switch request: number of empty slots that can't be replaced
// this.choice.type = determines what the current choice screen to be displayed is
// this.choice.waiting = true if the choice has been sent and we're just waiting for the next turn
switch (act) {
case 'move':
if (!this.choice) {
this.choice = {
choices: [],
switchFlags: {},
switchOutFlags: {}
};
}
this.updateMoveControls(type);
break;
case 'switch':
if (!this.choice) {
this.choice = {
choices: [],
switchFlags: {},
switchOutFlags: {},
freedomDegrees: 0,
canSwitch: 0
};
if (this.request.forceSwitch !== true) {
var faintedLength = _.filter(this.request.forceSwitch, function (fainted) { return fainted; }).length;
var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.pokemonControlled), function (mon) { return !mon.fainted; }).length;
this.choice.freedomDegrees = Math.max(freedomDegrees, 0);
this.choice.canSwitch = faintedLength - this.choice.freedomDegrees;
}
}
this.updateSwitchControls(type);
break;
case 'team':
if (this.battle.mySide.pokemon && !this.battle.mySide.pokemon.length) {
// too early, we can't determine `this.choice.count` yet
// TODO: send teamPreviewCount in the request object
this.controlsShown = false;
return;
}
if (!this.choice) {
this.choice = {
choices: null,
teamPreview: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24].slice(0, switchables.length),
done: 0,
count: 1
};
if (this.battle.gameType === 'multi') {
this.choice.count = 1;
}
if (this.battle.gameType === 'doubles') {
this.choice.count = 2;
}
if (this.battle.gameType === 'triples' || this.battle.gameType === 'rotation') {
this.choice.count = 3;
}
// Request full team order if one of our Pokémon has Illusion
for (var i = 0; i < switchables.length && i < 6; i++) {
if (toID(switchables[i].baseAbility) === 'illusion') {
this.choice.count = this.battle.myPokemon.length;
}
}
if (this.battle.teamPreviewCount) {
var requestCount = parseInt(this.battle.teamPreviewCount, 10);
if (requestCount > 0 && requestCount <= switchables.length) {
this.choice.count = requestCount;
}
}
this.choice.choices = new Array(this.choice.count);
}
this.updateTeamControls(type);
break;
default:
this.updateWaitControls();
break;
}
},
timerInterval: 0,
getTimerHTML: function (nextTick) {
var time = 'Timer';
var timerTicking = (this.battle.kickingInactive && this.request && !this.request.wait && !(this.choice && this.choice.waiting)) ? ' timerbutton-on' : '';
if (!nextTick) {
var self = this;
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = 0;
}
if (timerTicking) this.timerInterval = setInterval(function () {
var $timerButton = self.$('.timerbutton');
if ($timerButton.length) {
$timerButton.replaceWith(self.getTimerHTML(true));
} else {
clearInterval(self.timerInterval);
self.timerInterval = 0;
}
}, 1000);
} else if (this.battle.kickingInactive > 1) {
this.battle.kickingInactive--;
if (this.battle.graceTimeLeft) this.battle.graceTimeLeft--;
else if (this.battle.totalTimeLeft) this.battle.totalTimeLeft--;
}
if (this.battle.kickingInactive) {
var secondsLeft = this.battle.kickingInactive;
if (secondsLeft !== true) {
if (secondsLeft <= 10 && timerTicking) {
timerTicking = ' timerbutton-critical';
}
var minutesLeft = Math.floor(secondsLeft / 60);
secondsLeft -= minutesLeft * 60;
time = '' + minutesLeft + ':' + (secondsLeft < 10 ? '0' : '') + secondsLeft;
secondsLeft = this.battle.totalTimeLeft;
if (secondsLeft) {
minutesLeft = Math.floor(secondsLeft / 60);
secondsLeft -= minutesLeft * 60;
time += ' | ' + minutesLeft + ':' + (secondsLeft < 10 ? '0' : '') + secondsLeft + ' total';
}
} else {
time = '-:--';
}
}
return '';
},
uncheckMegaEvoX: function () {
this.$('input[name=megaevox]').prop('checked', false);
},
uncheckMegaEvoY: function () {
this.$('input[name=megaevoy]').prop('checked', false);
},
updateMaxMove: function () {
var dynaChecked = this.$('input[name=dynamax]')[0].checked;
if (dynaChecked) {
this.$('.movebuttons-nomax').hide();
this.$('.movebuttons-max').show();
} else {
this.$('.movebuttons-nomax').show();
this.$('.movebuttons-max').hide();
}
},
updateZMove: function () {
var zChecked = this.$('input[name=zmove]')[0].checked;
if (zChecked) {
this.$('.movebuttons-noz').hide();
this.$('.movebuttons-z').show();
} else {
this.$('.movebuttons-noz').show();
this.$('.movebuttons-z').hide();
}
},
updateTimer: function () {
this.$('.timerbutton').replaceWith(this.getTimerHTML());
},
openTimer: function () {
app.addPopup(TimerPopup, { room: this });
},
updateMoveControls: function (type) {
var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
if (type !== 'movetarget') {
while (
switchables[this.choice.choices.length] &&
(switchables[this.choice.choices.length].fainted || switchables[this.choice.choices.length].commanding) &&
this.choice.choices.length + 1 < this.battle.nearSide.active.length
) {
this.choice.choices.push('pass');
}
}
var moveTarget = this.choice ? this.choice.moveTarget : '';
var pos = this.choice.choices.length;
if (type === 'movetarget') pos--;
var hpRatio = switchables[pos].hp / switchables[pos].maxhp;
var curActive = this.request && this.request.active && this.request.active[pos];
if (!curActive) return;
var trapped = curActive.trapped;
var canMegaEvo = curActive.canMegaEvo || switchables[pos].canMegaEvo;
var canMegaEvoX = curActive.canMegaEvoX || switchables[pos].canMegaEvoX;
var canMegaEvoY = curActive.canMegaEvoY || switchables[pos].canMegaEvoY;
var canZMove = curActive.canZMove || switchables[pos].canZMove;
var canUltraBurst = curActive.canUltraBurst || switchables[pos].canUltraBurst;
var canDynamax = curActive.canDynamax || switchables[pos].canDynamax;
var maxMoves = curActive.maxMoves || switchables[pos].maxMoves;
var gigantamax = curActive.gigantamax;
var canTerastallize = curActive.canTerastallize || switchables[pos].canTerastallize;
if (canZMove && typeof canZMove[0] === 'string') {
canZMove = _.map(canZMove, function (move) {
return { move: move, target: Dex.moves.get(move).target };
});
}
if (gigantamax) gigantamax = Dex.moves.get(gigantamax);
this.finalDecisionMove = curActive.maybeDisabled || false;
this.finalDecisionSwitch = curActive.maybeTrapped || false;
for (var i = pos + 1; i < this.battle.nearSide.active.length; ++i) {
var p = this.battle.nearSide.active[i];
if (p && !p.fainted) {
this.finalDecisionMove = this.finalDecisionSwitch = false;
break;
}
}
var requestTitle = '';
if (type === 'move2' || type === 'movetarget') {
requestTitle += ' ';
}
// Target selector
if (type === 'movetarget') {
requestTitle += 'At who? ';
var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
var targetMenus = ['', ''];
var nearActive = this.battle.nearSide.active;
var farActive = this.battle.farSide.active;
var farSlot = farActive.length - 1 - activePos;
if ((moveTarget === 'adjacentAlly' || moveTarget === 'adjacentFoe') && this.battle.gameType === 'freeforall') {
moveTarget = 'normal';
}
for (var i = farActive.length - 1; i >= 0; i--) {
var pokemon = farActive[i];
var tooltipArgs = 'activepokemon|1|' + i;
var disabled = false;
if (moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
disabled = true;
} else if (moveTarget === 'normal' || moveTarget === 'adjacentFoe') {
if (Math.abs(farSlot - i) > 1) disabled = true;
}
if (disabled) {
targetMenus[0] += ' ';
} else if (!pokemon || pokemon.fainted) {
targetMenus[0] += ' ';
} else {
targetMenus[0] += ' ';
}
}
for (var i = 0; i < nearActive.length; i++) {
var pokemon = nearActive[i];
var tooltipArgs = 'activepokemon|0|' + i;
var disabled = false;
if (moveTarget === 'adjacentFoe') {
disabled = true;
} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
if (Math.abs(activePos - i) > 1) disabled = true;
}
if (moveTarget !== 'adjacentAllyOrSelf' && activePos === i) disabled = true;
if (disabled) {
targetMenus[1] += ' ';
} else if (!pokemon || pokemon.fainted) {
targetMenus[1] += ' ';
} else {
targetMenus[1] += ' ';
}
}
this.$controls.html(
'' +
'
' + requestTitle + this.getTimerHTML() + '
' +
'' +
'' +
'
'
);
} else {
// Move chooser
var hpBar = 'HP ' + switchables[pos].hp + '/' + switchables[pos].maxhp + '';
requestTitle += ' What will ' + BattleLog.escapeHTML(switchables[pos].name) + ' do? ' + hpBar;
var hasMoves = false;
var moveMenu = '';
var movebuttons = '';
var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
var typeValueTracker = new ModifiableValue(this.battle, this.battle.nearSide.active[activePos], this.battle.myPokemon[pos]);
var currentlyDynamaxed = (!canDynamax && maxMoves);
for (var i = 0; i < curActive.moves.length; i++) {
var moveData = curActive.moves[i];
var move = this.battle.dex.moves.get(moveData.move);
var name = move.name;
var pp = moveData.pp + '/' + moveData.maxpp;
if (!moveData.maxpp) pp = '–';
if (move.id === 'Struggle' || move.id === 'Recharge') pp = '–';
if (move.id === 'Recharge') move.type = '–';
if (name.substr(0, 12) === 'Hidden Power') name = 'Hidden Power';
var moveType = this.tooltips.getMoveType(move, typeValueTracker)[0];
var tooltipArgs = 'move|' + moveData.move + '|' + pos;
if (moveData.disabled) {
movebuttons += '