PageSourceSearch

https://fnf-showdown-client.herokuapp.com/js/client-battle.js

js fnf-showdown-client.herokuapp.com collected 2026-10-03 14:48:54 UTC 66,190 bytes, 1,558 lines download raw bytes

1(function ($) {
2
3	var BattleRoom = this.BattleRoom = ConsoleRoom.extend({
4		type: 'battle',
5		title: '',
6		minWidth: 320,
7		minMainWidth: 956,
8		maxWidth: 1180,
9		initialize: function (data) {
10			this.choice = undefined;
11			/** are move/switch/team-preview controls currently being shown? */
12			this.controlsShown = false;
13
14			this.battlePaused = false;
15			this.autoTimerActivated = false;
16
17			this.isSideRoom = Dex.prefs('rightpanelbattles');
18
19			this.$el.addClass('ps-room-opaque').html('<div class="battle">Battle is here</div><div class="foehint"></div><div class="battle-log" aria-label="Battle Log" role="complementary"></div><div class="battle-log-add">Connecting...</div><ul class="battle-userlist userlist userlist-minimized"></ul><div class="battle-controls" role="complementary" aria-label="Battle Controls"></div><button class="battle-chat-toggle button" name="showChat"><i class="fa fa-caret-left"></i> Chat</button>');
20
21			this.$battle = this.$el.find('.battle');
22			this.$controls = this.$el.find('.battle-controls');
23			this.$chatFrame = this.$el.find('.battle-log');
24			this.$chatAdd = this.$el.find('.battle-log-add');
25			this.$foeHint = this.$el.find('.foehint');
26
27			BattleSound.setMute(Dex.prefs('mute'));
28			this.battle = new Battle({
29				id: this.id,
30				$frame: this.$battle,
31				$logFrame: this.$chatFrame
32			});
33			this.battle.roomid = this.id;
34			this.battle.joinButtons = true;
35			this.tooltips = this.battle.scene.tooltips;
36			this.tooltips.listen(this.$controls);
37
38			var self = this;
39			this.battle.subscribe(function () { self.updateControls(); });
40
41			this.users = {};
42			this.userCount = {users: 0};
43			this.$userList = this.$('.userlist');
44			this.userList = new UserList({
45				el: this.$userList,
46				room: this
47			});
48			this.userList.construct();
49
50			this.$chat = this.$chatFrame.find('.inner');
51
52			this.$options = this.battle.scene.$options.html('<div style="padding-top: 3px; padding-right: 3px; text-align: right"><button class="icon button" name="openBattleOptions" title="Options">Battle Options</button></div>');
53		},
54		events: {
55			'click .replayDownloadButton': 'clickReplayDownloadButton',
56			'change input[name=zmove]': 'updateZMove',
57			'change input[name=dynamax]': 'updateMaxMove'
58		},
59		battleEnded: false,
60		join: function () {
61			app.send('/join ' + this.id);
62		},
63		showChat: function () {
64			this.$('.battle-chat-toggle').attr('name', 'hideChat').html('Battle <i class="fa fa-caret-right"></i>');
65			this.$el.addClass('showing-chat');
66		},
67		hideChat: function () {
68			this.$('.battle-chat-toggle').attr('name', 'showChat').html('<i class="fa fa-caret-left"></i> Chat');
69			this.$el.removeClass('showing-chat');
70		},
71		leave: function () {
72			if (!this.expired) app.send('/noreply /leave ' + this.id);
73			if (this.battle) this.battle.destroy();
74		},
75		requestLeave: function (e) {
76			if (this.side && this.battle && !this.battleEnded && !this.expired && !this.battle.forfeitPending) {
77				app.addPopup(ForfeitPopup, {room: this, sourceEl: e && e.currentTarget, gameType: 'battle'});
78				return false;
79			}
80			return true;
81		},
82		updateLayout: function () {
83			var width = this.$el.width();
84			if (width < 950 || this.battle.hardcoreMode) {
85				this.battle.messageShownTime = 500;
86			} else {
87				this.battle.messageShownTime = 1;
88			}
89			if (width && width < 640) {
90				var scale = (width / 640);
91				this.$battle.css('transform', 'scale(' + scale + ')');
92				this.$foeHint.css('transform', 'scale(' + scale + ')');
93				this.$controls.css('top', 360 * scale + 10);
94			} else {
95				this.$battle.css('transform', 'none');
96				this.$foeHint.css('transform', 'none');
97				this.$controls.css('top', 370);
98			}
99			this.$el.toggleClass('small-layout', width < 830);
100			this.$el.toggleClass('tiny-layout', width < 640);
101			if (this.$chat) this.$chatFrame.scrollTop(this.$chat.height());
102		},
103		show: function () {
104			Room.prototype.show.apply(this, arguments);
105			this.updateLayout();
106		},
107		receive: function (data) {
108			this.add(data);
109		},
110		focus: function (e) {
111			this.tooltips.hideTooltip();
112			if (this.battle.paused && !this.battlePaused) {
113				if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
114				this.battle.play();
115			}
116			ConsoleRoom.prototype.focus.call(this, e);
117		},
118		blur: function () {
119			this.battle.pause();
120		},
121		init: function (data) {
122			var log = data.split('\n');
123			if (data.substr(0, 6) === '|init|') log.shift();
124			if (log.length && log[0].substr(0, 7) === '|title|') {
125				this.title = log[0].substr(7);
126				log.shift();
127				app.roomTitleChanged(this);
128			}
129			if (this.battle.stepQueue.length) return;
130			this.battle.stepQueue = log;
131			this.battle.seekTurn(Infinity, true);
132			if (this.battle.ended) this.battleEnded = true;
133			this.updateLayout();
134			this.updateControls();
135		},
136		add: function (data) {
137			if (!data) return;
138			if (data.substr(0, 6) === '|init|') {
139				return this.init(data);
140			}
141			if (data.substr(0, 9) === '|request|') {
142				data = data.slice(9);
143
144				var requestData = null;
145				var choiceText = null;
146
147				var nlIndex = data.indexOf('\n');
148				if (/[0-9]/.test(data.charAt(0)) && data.charAt(1) === '|') {
149					// message format:
150					//   |request|CHOICEINDEX|CHOICEDATA
151					//   REQUEST
152
153					// This is backwards compatibility with old code that violates the
154					// expectation that server messages can be streamed line-by-line.
155					// Please do NOT EVER push protocol changes without a pull request.
156					// https://github.com/Zarel/Pokemon-Showdown/commit/e3c6cbe4b91740f3edc8c31a1158b506f5786d72#commitcomment-21278523
157					choiceText = '?';
158					data = data.slice(2, nlIndex);
159				} else if (nlIndex >= 0) {
160					// message format:
161					//   |request|REQUEST
162					//   |sentchoice|CHOICE
163					if (data.slice(nlIndex + 1, nlIndex + 13) === '|sentchoice|') {
164						choiceText = data.slice(nlIndex + 13);
165					}
166					data = data.slice(0, nlIndex);
167				}
168
169				try {
170					requestData = JSON.parse(data);
171				} catch (err) {}
172				return this.receiveRequest(requestData, choiceText);
173			}
174
175			var log = data.split('\n');
176			for (var i = 0; i < log.length; i++) {
177				var logLine = log[i];
178
179				if (logLine === '|') {
180					this.callbackWaiting = false;
181					this.controlsShown = false;
182					this.$controls.html('');
183				}
184
185				if (logLine.substr(0, 10) === '|callback|') {
186					// TODO: Maybe a more sophisticated UI for this.
187					// In singles, this isn't really necessary because some elements of the UI will be
188					// immediately disabled. However, in doubles/triples it might not be obvious why
189					// the player is being asked to make a new decision without the following messages.
190					var args = logLine.substr(10).split('|');
191					var pokemon = isNaN(Number(args[1])) ? this.battle.getPokemon(args[1]) : this.battle.nearSide.active[args[1]];
192					var requestData = this.request.active[pokemon ? pokemon.slot : 0];
193					this.choice = undefined;
194					switch (args[0]) {
195					case 'trapped':
196						requestData.trapped = true;
197						var pokeName = pokemon.side.n === 0 ? BattleLog.escapeHTML(pokemon.name) : "The opposing " + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name));
198						this.battle.stepQueue.push('|message|' + pokeName + ' is trapped and cannot switch!');
199						break;
200					case 'cant':
201						for (var i = 0; i < requestData.moves.length; i++) {
202							if (requestData.moves[i].id === args[3]) {
203								requestData.moves[i].disabled = true;
204							}
205						}
206						args.splice(1, 1, pokemon.getIdent());
207						this.battle.stepQueue.push('|' + args.join('|'));
208						break;
209					}
210				} else if (logLine.substr(0, 7) === '|title|') { // eslint-disable-line no-empty
211				} else if (logLine.substr(0, 5) === '|win|' || logLine === '|tie') {
212					this.battleEnded = true;
213					this.battle.stepQueue.push(logLine);
214				} 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|') {
215					this.battle.instantAdd(logLine);
216				} else {
217					this.battle.stepQueue.push(logLine);
218				}
219			}
220			this.battle.add();
221			if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
222			this.updateControls();
223		},
224		toggleMessages: function (user) {
225			var $messages = $('.chatmessage-' + user + '.revealed');
226			var $button = $messages.find('button');
227			if (!$messages.is(':hidden')) {
228				$messages.hide();
229				$button.html('<small>(' + ($messages.length) + ' line' + ($messages.length > 1 ? 's' : '') + 'from ' + user + ')</small>');
230				$button.parent().show();
231			} else {
232				$button.html('<small>(Hide ' + ($messages.length) + ' line' + ($messages.length > 1 ? 's' : '') + ' from ' + user + ')</small>');
233				$button.parent().removeClass('revealed');
234				$messages.show();
235			}
236		},
237		setHardcoreMode: function (mode) {
238			this.battle.setHardcoreMode(mode);
239			var id = '#' + this.el.id + ' ';
240			this.$('.hcmode-style').remove();
241			this.updateLayout(); // set animation delay
242			if (mode) this.$el.prepend('<style class="hcmode-style">' + id + '.battle .turn,' + id + '.battle-history{display:none !important;}</style>');
243			if (this.choice && this.choice.waiting) {
244				this.updateControlsForPlayer();
245			}
246		},
247
248		/*********************************************************
249		 * Battle stuff
250		 *********************************************************/
251
252		updateControls: function () {
253			if (this.battle.scene.customControls) return;
254			var controlsShown = this.controlsShown;
255			var switchSidesButton = '<p><button class="button" name="switchSides"><i class="fa fa-random"></i> Switch sides</button></p>';
256			this.controlsShown = false;
257
258			if (this.battle.seeking !== null) {
259
260				// battle is seeking
261				this.$controls.html('');
262				return;
263
264			} else if (!this.battle.atQueueEnd) {
265
266				// battle is playing or paused
267				if (!this.side || this.battleEnded) {
268					// spectator
269					if (this.battle.paused) {
270						// paused
271						this.$controls.html('<p><button class="button" name="resume"><i class="fa fa-play"></i><br />Play</button> <button class="button" name="rewindTurn"><i class="fa fa-step-backward"></i><br />Last turn</button><button class="button" name="skipTurn"><i class="fa fa-step-forward"></i><br />Skip turn</button> <button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />First turn</button><button class="button" name="goToEnd"><i class="fa fa-fast-forward"></i><br />Skip to end</button></p>' + switchSidesButton);
272					} else {
273						// playing
274						this.$controls.html('<p><button class="button" name="pause"><i class="fa fa-pause"></i><br />Pause</button> <button class="button" name="rewindTurn"><i class="fa fa-step-backward"></i><br />Last turn</button><button class="button" name="skipTurn"><i class="fa fa-step-forward"></i><br />Skip turn</button> <button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />First turn</button><button class="button" name="goToEnd"><i class="fa fa-fast-forward"></i><br />Skip to end</button></p>' + switchSidesButton);
275					}
276				} else {
277					// is a player
278					this.$controls.html('<p>' + this.getTimerHTML() + '<button class="button" name="skipTurn"><i class="fa fa-step-forward"></i><br />Skip turn</button> <button class="button" name="goToEnd"><i class="fa fa-fast-forward"></i><br />Skip to end</button></p>');
279				}
280				return;
281
282			}
283
284			if (this.battle.ended) {
285
286				var replayDownloadButton = '<span style="float:right;"><a href="//' + Config.routes.replays + '/" class="button replayDownloadButton"><i class="fa fa-download"></i> Download replay</a><br /><br /><button class="button" name="saveReplay"><i class="fa fa-upload"></i> Upload and share replay</button></span>';
287
288				// battle has ended
289				if (this.side) {
290					// was a player
291					this.closeNotification('choice');
292					this.$controls.html('<div class="controls"><p>' + replayDownloadButton + '<button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />Instant replay</button></p><p><button class="button" name="closeAndMainMenu"><strong>Main menu</strong><br /><small>(closes this battle)</small></button> <button class="button" name="closeAndRematch"><strong>Rematch</strong><br /><small>(closes this battle)</small></button></p></div>');
293				} else {
294					this.$controls.html('<div class="controls"><p>' + replayDownloadButton + '<button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />Instant replay</button></p>' + switchSidesButton + '</div>');
295				}
296
297			} else if (this.side) {
298
299				// player
300				this.controlsShown = true;
301				if (!controlsShown || this.choice === undefined || this.choice && this.choice.waiting) {
302					// don't update controls (and, therefore, side) if `this.choice === null`: causes damage miscalculations
303					this.updateControlsForPlayer();
304				} else {
305					this.updateTimer();
306				}
307
308			} else if (!this.battle.nearSide.name || !this.battle.farSide.name) {
309
310				// empty battle
311				this.$controls.html('<p><em>Waiting for players...</em></p>');
312
313			} else {
314
315				// full battle
316				if (this.battle.paused) {
317					// paused
318					this.$controls.html('<p><button class="button" name="resume"><i class="fa fa-play"></i><br />Play</button> <button class="button" name="rewindTurn"><i class="fa fa-step-backward"></i><br />Last turn</button><button class="button disabled" disabled><i class="fa fa-step-forward"></i><br />Skip turn</button> <button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />First turn</button><button class="button disabled" disabled><i class="fa fa-fast-forward"></i><br />Skip to end</button></p>' + switchSidesButton + '<p><em>Waiting for players...</em></p>');
319				} else {
320					// playing
321					this.$controls.html('<p><button class="button" name="pause"><i class="fa fa-pause"></i><br />Pause</button> <button class="button" name="rewindTurn"><i class="fa fa-step-backward"></i><br />Last turn</button><button class="button disabled" disabled><i class="fa fa-step-forward"></i><br />Skip turn</button> <button class="button" name="instantReplay"><i class="fa fa-undo"></i><br />First turn</button><button class="button disabled" disabled><i class="fa fa-fast-forward"></i><br />Skip to end</button></p>' + switchSidesButton + '<p><em>Waiting for players...</em></p>');
322				}
323
324			}
325
326			// This intentionally doesn't happen if the battle is still playing,
327			// since those early-return.
328			app.topbar.updateTabbar();
329		},
330		updateControlsForPlayer: function () {
331			this.callbackWaiting = true;
332
333			var act = '';
334			var switchables = [];
335			if (this.request) {
336				// TODO: investigate when to do this
337				this.updateSide();
338				if (this.request.ally) {
339					this.addAlly(this.request.ally);
340				}
341
342				act = this.request.requestType;
343				if (this.request.side) {
344					switchables = this.battle.myPokemon;
345				}
346				if (!this.finalDecision) this.finalDecision = !!this.request.noCancel;
347			}
348
349			if (this.choice && this.choice.waiting) {
350				act = '';
351			}
352
353			var type = this.choice ? this.choice.type : '';
354
355			// The choice object:
356			// !this.choice = nothing has been chosen
357			// this.choice.choices = array of choice strings
358			// this.choice.switchFlags = dict of pokemon indexes that have a switch pending
359			// this.choice.switchOutFlags = ???
360			// this.choice.freedomDegrees = in a switch request: number of empty slots that can't be replaced
361			// this.choice.type = determines what the current choice screen to be displayed is
362			// this.choice.waiting = true if the choice has been sent and we're just waiting for the next turn
363
364			switch (act) {
365			case 'move':
366				if (!this.choice) {
367					this.choice = {
368						choices: [],
369						switchFlags: {},
370						switchOutFlags: {}
371					};
372				}
373				this.updateMoveControls(type);
374				break;
375
376			case 'switch':
377				if (!this.choice) {
378					this.choice = {
379						choices: [],
380						switchFlags: {},
381						switchOutFlags: {},
382						freedomDegrees: 0,
383						canSwitch: 0
384					};
385
386					if (this.request.forceSwitch !== true) {
387						var faintedLength = _.filter(this.request.forceSwitch, function (fainted) {return fainted;}).length;
388						var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.pokemonControlled), function (mon) {return !mon.fainted;}).length;
389						this.choice.freedomDegrees = Math.max(freedomDegrees, 0);
390						this.choice.canSwitch = faintedLength - this.choice.freedomDegrees;
391					}
392				}
393				this.updateSwitchControls(type);
394				break;
395
396			case 'team':
397				if (this.battle.mySide.pokemon && !this.battle.mySide.pokemon.length) {
398					// too early, we can't determine `this.choice.count` yet
399					// TODO: send teamPreviewCount in the request object
400					this.controlsShown = false;
401					return;
402				}
403				if (!this.choice) {
404					this.choice = {
405						choices: null,
406						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),
407						done: 0,
408						count: 1
409					};
410					if (this.battle.gameType === 'multi') {
411						this.choice.count = 1;
412					}
413					if (this.battle.gameType === 'doubles') {
414						this.choice.count = 2;
415					}
416					if (this.battle.gameType === 'triples' || this.battle.gameType === 'rotation') {
417						this.choice.count = 3;
418					}
419					// Request full team order if one of our Pokémon has Illusion
420					for (var i = 0; i < switchables.length && i < 6; i++) {
421						if (toID(switchables[i].baseAbility) === 'illusion') {
422							this.choice.count = this.battle.myPokemon.length;
423						}
424					}
425					if (this.battle.teamPreviewCount) {
426						var requestCount = parseInt(this.battle.teamPreviewCount, 10);
427						if (requestCount > 0 && requestCount <= switchables.length) {
428							this.choice.count = requestCount;
429						}
430					}
431					this.choice.choices = new Array(this.choice.count);
432				}
433				this.updateTeamControls(type);
434				break;
435
436			default:
437				this.updateWaitControls();
438				break;
439			}
440		},
441		timerInterval: 0,
442		getTimerHTML: function (nextTick) {
443			var time = 'Timer';
444			var timerTicking = (this.battle.kickingInactive && this.request && !this.request.wait && !(this.choice && this.choice.waiting)) ? ' timerbutton-on' : '';
445
446			if (!nextTick) {
447				var self = this;
448				if (this.timerInterval) {
449					clearInterval(this.timerInterval);
450					this.timerInterval = 0;
451				}
452				if (timerTicking) this.timerInterval = setInterval(function () {
453					var $timerButton = self.$('.timerbutton');
454					if ($timerButton.length) {
455						$timerButton.replaceWith(self.getTimerHTML(true));
456					} else {
457						clearInterval(self.timerInterval);
458						self.timerInterval = 0;
459					}
460				}, 1000);
461			} else if (this.battle.kickingInactive > 1) {
462				this.battle.kickingInactive--;
463				if (this.battle.graceTimeLeft) this.battle.graceTimeLeft--;
464				else if (this.battle.totalTimeLeft) this.battle.totalTimeLeft--;
465			}
466
467			if (this.battle.kickingInactive) {
468				var secondsLeft = this.battle.kickingInactive;
469				if (secondsLeft !== true) {
470					if (secondsLeft <= 10 && timerTicking) {
471						timerTicking = ' timerbutton-critical';
472					}
473					var minutesLeft = Math.floor(secondsLeft / 60);
474					secondsLeft -= minutesLeft * 60;
475					time = '' + minutesLeft + ':' + (secondsLeft < 10 ? '0' : '') + secondsLeft;
476
477					secondsLeft = this.battle.totalTimeLeft;
478					if (secondsLeft) {
479						minutesLeft = Math.floor(secondsLeft / 60);
480						secondsLeft -= minutesLeft * 60;
481						time += ' | ' + minutesLeft + ':' + (secondsLeft < 10 ? '0' : '') + secondsLeft + ' total';
482					}
483				} else {
484					time = '-:--';
485				}
486			}
487			return '<button name="openTimer" class="button timerbutton' + timerTicking + '"><i class="fa fa-hourglass-start"></i> ' + time + '</button>';
488		},
489		updateMaxMove: function () {
490			var dynaChecked = this.$('input[name=dynamax]')[0].checked;
491			if (dynaChecked) {
492				this.$('.movebuttons-nomax').hide();
493				this.$('.movebuttons-max').show();
494			} else {
495				this.$('.movebuttons-nomax').show();
496				this.$('.movebuttons-max').hide();
497			}
498		},
499		updateZMove: function () {
500			var zChecked = this.$('input[name=zmove]')[0].checked;
501			if (zChecked) {
502				this.$('.movebuttons-noz').hide();
503				this.$('.movebuttons-z').show();
504			} else {
505				this.$('.movebuttons-noz').show();
506				this.$('.movebuttons-z').hide();
507			}
508		},
509		updateTimer: function () {
510			this.$('.timerbutton').replaceWith(this.getTimerHTML());
511		},
512		openTimer: function () {
513			app.addPopup(TimerPopup, {room: this});
514		},
515		updateMoveControls: function (type) {
516			var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
517
518			if (type !== 'movetarget') {
519				while (switchables[this.choice.choices.length] && switchables[this.choice.choices.length].fainted && this.choice.choices.length + 1 < this.battle.nearSide.active.length) {
520					this.choice.choices.push('pass');
521				}
522			}
523
524			var moveTarget = this.choice ? this.choice.moveTarget : '';
525			var pos = this.choice.choices.length;
526			if (type === 'movetarget') pos--;
527
528			var hpRatio = switchables[pos].hp / switchables[pos].maxhp;
529
530			var curActive = this.request && this.request.active && this.request.active[pos];
531			if (!curActive) return;
532			var trapped = curActive.trapped;
533			var canMegaEvo = curActive.canMegaEvo || switchables[pos].canMegaEvo;
534			var canZMove = curActive.canZMove || switchables[pos].canZMove;
535			var canUltraBurst = curActive.canUltraBurst || switchables[pos].canUltraBurst;
536			var canDynamax = curActive.canDynamax || switchables[pos].canDynamax;
537			var maxMoves = curActive.maxMoves || switchables[pos].maxMoves;
538			var gigantamax = curActive.gigantamax;
539			if (canZMove && typeof canZMove[0] === 'string') {
540				canZMove = _.map(canZMove, function (move) {
541					return {move: move, target: Dex.moves.get(move).target};
542				});
543			}
544			if (gigantamax) gigantamax = Dex.moves.get(gigantamax);
545
546			this.finalDecisionMove = curActive.maybeDisabled || false;
547			this.finalDecisionSwitch = curActive.maybeTrapped || false;
548			for (var i = pos + 1; i < this.battle.nearSide.active.length; ++i) {
549				var p = this.battle.nearSide.active[i];
550				if (p && !p.fainted) {
551					this.finalDecisionMove = this.finalDecisionSwitch = false;
552					break;
553				}
554			}
555
556			var requestTitle = '';
557			if (type === 'move2' || type === 'movetarget') {
558				requestTitle += '<button name="clearChoice">Back</button> ';
559			}
560
561			// Target selector
562			if (type === 'movetarget') {
563				requestTitle += 'At who? ';
564
565				var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
566
567				var targetMenus = ['', ''];
568				var nearActive = this.battle.nearSide.active;
569				var farActive = this.battle.farSide.active;
570				var farSlot = farActive.length - 1 - activePos;
571
572				if ((moveTarget === 'adjacentAlly' || moveTarget === 'adjacentFoe') && this.battle.gameType === 'freeforall') {
573					moveTarget = 'normal';
574				}
575
576				for (var i = farActive.length - 1; i >= 0; i--) {
577					var pokemon = farActive[i];
578					var tooltipArgs = 'activepokemon|1|' + i;
579
580					var disabled = false;
581					if (moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
582						disabled = true;
583					} else if (moveTarget === 'normal' || moveTarget === 'adjacentFoe') {
584						if (Math.abs(farSlot - i) > 1) disabled = true;
585					}
586
587					if (disabled) {
588						targetMenus[0] += '<button disabled="disabled"></button> ';
589					} else if (!pokemon || pokemon.fainted) {
590						targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
591					} else {
592						targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name)) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
593					}
594				}
595				for (var i = 0; i < nearActive.length; i++) {
596					var pokemon = nearActive[i];
597					var tooltipArgs = 'activepokemon|0|' + i;
598
599					var disabled = false;
600					if (moveTarget === 'adjacentFoe') {
601						disabled = true;
602					} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
603						if (Math.abs(activePos - i) > 1) disabled = true;
604					}
605					if (moveTarget !== 'adjacentAllyOrSelf' && activePos == i) disabled = true;
606
607					if (disabled) {
608						targetMenus[1] += '<button disabled="disabled" style="visibility:hidden"></button> ';
609					} else if (!pokemon || pokemon.fainted) {
610						targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
611					} else {
612						targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
613					}
614				}
615
616				this.$controls.html(
617					'<div class="controls">' +
618					'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
619					'<div class="switchmenu" style="display:block">' + targetMenus[0] + '<div style="clear:both"></div> </div>' +
620					'<div class="switchmenu" style="display:block">' + targetMenus[1] + '</div>' +
621					'</div>'
622				);
623			} else {
624				// Move chooser
625				var hpBar = '<small class="' + (hpRatio < 0.2 ? 'critical' : hpRatio < 0.5 ? 'weak' : 'healthy') + '">HP ' + switchables[pos].hp + '/' + switchables[pos].maxhp + '</small>';
626				requestTitle += ' What will <strong>' + BattleLog.escapeHTML(switchables[pos].name) + '</strong> do? ' + hpBar;
627
628				var hasMoves = false;
629				var moveMenu = '';
630				var movebuttons = '';
631				var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
632				var typeValueTracker = new ModifiableValue(this.battle, this.battle.nearSide.active[activePos], this.battle.myPokemon[pos]);
633				var currentlyDynamaxed = (!canDynamax && maxMoves);
634				for (var i = 0; i < curActive.moves.length; i++) {
635					var moveData = curActive.moves[i];
636					var move = this.battle.dex.moves.get(moveData.move);
637					var name = move.name;
638					var pp = moveData.pp + '/' + moveData.maxpp;
639					if (!moveData.maxpp) pp = '&ndash;';
640					if (move.id === 'Struggle' || move.id === 'Recharge') pp = '&ndash;';
641					if (move.id === 'Recharge') move.type = '&ndash;';
642					if (name.substr(0, 12) === 'Hidden Power') name = 'Hidden Power';
643					var moveType = this.tooltips.getMoveType(move, typeValueTracker)[0];
644					var tooltipArgs = 'move|' + moveData.move + '|' + pos;
645					if (moveData.disabled) {
646						movebuttons += '<button disabled="disabled" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
647					} else {
648						movebuttons += '<button class="type-' + moveType + ' has-tooltip" name="chooseMove" value="' + (i + 1) + '" data-move="' + BattleLog.escapeHTML(moveData.move) + '" data-target="' + BattleLog.escapeHTML(moveData.target) + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
649						hasMoves = true;
650					}
651					movebuttons += name + '<br /><small class="type">' + (moveType ? Dex.types.get(moveType).name : "Unknown") + '</small> <small class="pp">' + pp + '</small>&nbsp;</button> ';
652				}
653				if (!hasMoves) {
654					moveMenu += '<button class="movebutton" name="chooseMove" value="
6540" data-move="Struggle" data-target="randomNormal">Struggle<br /><small class="type">Normal</small> <small class="pp">&ndash;</small>&nbsp;</button> ';
655				} else {
656					if (canZMove || canDynamax || currentlyDynamaxed) {
657						var classType = canZMove ? 'z' : 'max';
658						if (currentlyDynamaxed) {
659							movebuttons = '';
660						} else {
661							movebuttons = '<div class="movebuttons-no' + classType + '">' + movebuttons + '</div><div class="movebuttons-' + classType + '" style="display:none">';
662						}
663						var specialMoves = canZMove ? canZMove : maxMoves.maxMoves;
664						for (var i = 0; i < curActive.moves.length; i++) {
665							if (specialMoves[i]) {
666								// when possible, use Z move to decide type, for cases like Z-Hidden Power
667								var baseMove = this.battle.dex.moves.get(curActive.moves[i].move);
668								// might not exist, such as for Z status moves - fall back on base move to determine type then
669								var specialMove = gigantamax || this.battle.dex.moves.get(specialMoves[i].move);
670								var moveType = this.tooltips.getMoveType(specialMove.exists && !specialMove.isMax ? specialMove : baseMove, typeValueTracker, specialMove.isMax ? gigantamax || switchables[pos].gigantamax || true : undefined)[0];
671								if (specialMove.isMax && specialMove.name !== 'Max Guard' && !specialMove.id.startsWith('gmax')) {
672									specialMove = this.tooltips.getMaxMoveFromType(moveType);
673								}
674								var tooltipArgs = classType + 'move|' + baseMove.id + '|' + pos;
675								if (specialMove.id.startsWith('gmax')) tooltipArgs += '|' + specialMove.id;
676								var isDisabled = specialMoves[i].disabled ? 'disabled="disabled"' : '';
677								movebuttons += '<button ' + isDisabled + ' class="type-' + moveType + ' has-tooltip" name="chooseMove" value="' + (i + 1) + '" data-move="' + BattleLog.escapeHTML(specialMoves[i].move) + '" data-target="' + BattleLog.escapeHTML(specialMoves[i].target) + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
678								var pp = curActive.moves[i].pp + '/' + curActive.moves[i].maxpp;
679								if (canZMove) {
680									pp = '1/1';
681								} else if (!curActive.moves[i].maxpp) {
682									pp = '&ndash;';
683								}
684								movebuttons += specialMove.name + '<br /><small class="type">' + (moveType ? Dex.types.get(moveType).name : "Unknown") + '</small> <small class="pp">' + pp + '</small>&nbsp;</button> ';
685							} else {
686								movebuttons += '<button disabled="disabled">&nbsp;</button>';
687							}
688						}
689						if (!currentlyDynamaxed) movebuttons += '</div>';
690					}
691					moveMenu += movebuttons;
692				}
693				if (canMegaEvo) {
694					moveMenu += '<br /><label class="megaevo"><input type="checkbox" name="megaevo" />&nbsp;Mega&nbsp;Evolution</label>';
695				} else if (canZMove) {
696					moveMenu += '<br /><label class="megaevo"><input type="checkbox" name="zmove" />&nbsp;Z-Power</label>';
697				} else if (canUltraBurst) {
698					moveMenu += '<br /><label class="megaevo"><input type="checkbox" name="ultraburst" />&nbsp;Ultra Burst</label>';
699				} else if (canDynamax) {
700					moveMenu += '<br /><label class="megaevo"><input type="checkbox" name="dynamax" />&nbsp;Dynamax</label>';
701				}
702				if (this.finalDecisionMove) {
703					moveMenu += '<em style="display:block;clear:both">You <strong>might</strong> have some moves disabled, so you won\'t be able to cancel an attack!</em><br/>';
704				}
705				moveMenu += '<div style="clear:left"></div>';
706
707				var moveControls = (
708					'<div class="movecontrols">' +
709					'<div class="moveselect">
709<button name="selectMove">Attack</button></div>' +
710					'<div class="movemenu">' + moveMenu + '</div>' +
711					'</div>'
712				);
713
714				var shiftControls = '';
715				if (this.battle.gameType === 'triples' && pos !== 1) {
716					shiftControls += '<div class="shiftselect"><button name="chooseShift">Shift</button></div>';
717				}
718
719				var switchMenu = '';
720				if (trapped) {
721					switchMenu += '<em>You are trapped and cannot switch!</em><br />';
722					switchMenu += this.displayParty(switchables, trapped);
723				} else {
724					switchMenu += this.displayParty(switchables, trapped);
725					if (this.finalDecisionSwitch && this.battle.gen > 2) {
726						switchMenu += '<em style="display:block;clear:both">You <strong>might</strong> be trapped, so you won\'t be able to cancel a switch!</em><br/>';
727					}
728				}
729				var switchControls = (
730					'<div class="switchcontrols">' +
731					'<div class="switchselect"><button name="selectSwitch">Switch</button></div>' +
732					'<div class="switchmenu">' + switchMenu + '</div>' +
733					'</div>'
734				);
735				this.$controls.html(
736					'<div class="controls">' +
737					'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
738					moveControls + shiftControls + switchControls +
739					'</div>'
740				);
741			}
742		},
743		displayParty: function (switchables, trapped) {
744			var party = '';
745			for (var i = 0; i < switchables.length; i++) {
746				var pokemon = switchables[i];
747				pokemon.name = pokemon.ident.substr(4);
748				var tooltipArgs = 'switchpokemon|' + i;
749				if (pokemon.fainted || i < this.battle.pokemonControlled || this.choice.switchFlags[i] || trapped) {
750					party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : trapped ? ',trapped' : i < this.battle.nearSide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
751				} else {
752					party += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
753				}
754			}
755			if (this.battle.mySide.ally) party += this.displayAllyParty();
756			return party;
757		},
758		displayAllyParty: function () {
759			var party = '';
760			if (!this.battle.myAllyPokemon) return '';
761			var allyParty = this.battle.myAllyPokemon;
762			for (var i = 0; i < allyParty.length; i++) {
763				var pokemon = allyParty[i];
764				pokemon.name = pokemon.ident.substr(4);
765				var tooltipArgs = 'allypokemon|' + i;
766				party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + ',notMine' + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
767			}
768			return party;
769		},
770		updateSwitchControls: function (type) {
771			var pos = this.choice.choices.length;
772
773			if (type !== 'switchposition' && this.request.forceSwitch !== true && !this.choice.freedomDegrees) {
774				while (!this.request.forceSwitch[pos] && pos < 6) {
775					pos = this.choice.choices.push('pass');
776				}
777			}
778
779			var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
780			var nearActive = this.battle.nearSide.active;
781
782			var requestTitle = '';
783			if (type === 'switch2' || type === 'switchposition') {
784				requestTitle += '<button name="clearChoice">Back</button> ';
785			}
786
787			// Place selector
788			if (type === 'switchposition') {
789				// TODO? hpbar
790				requestTitle += "Which Pokémon will it switch in for?";
791				var controls = '<div class="switchmenu" style="display:block">';
792				for (var i = 0; i < this.battle.pokemonControlled; i++) {
793					var pokemon = this.battle.myPokemon[i];
794					var tooltipArgs = 'switchpokemon|' + i;
795					if (pokemon && !pokemon.fainted || this.choice.switchOutFlags[i]) {
796						controls += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
797					} else if (!pokemon) {
798						controls += '<button disabled></button> ';
799					} else {
800						controls += '<button name="chooseSwitchTarget" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
801					}
802				}
803				controls += '</div>';
804				this.$controls.html(
805					'<div class="controls">' +
806					'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
807					controls +
808					'</div>'
809				);
810			} else {
811				if (this.choice.freedomDegrees >= 1) {
812					requestTitle += "Choose a Pokémon to send to battle!";
813				} else {
814					requestTitle += "Switch <strong>" + BattleLog.escapeHTML(switchables[pos].name) + "</strong> to:";
815				}
816
817				var switchMenu = '';
818				for (var i = 0; i < switchables.length; i++) {
819					var pokemon = switchables[i];
820					var tooltipArgs = 'switchpokemon|' + i;
821					if (pokemon.fainted || i < this.battle.pokemonControlled || this.choice.switchFlags[i]) {
822						switchMenu += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : i < this.battle.pokemonControlled ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
823					} else {
824						switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
825					}
826					switchMenu += '<span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
827				}
828
829				var controls = (
830					'<div class="switchcontrols">' +
831					'<div class="switchselect"><button name="selectSwitch">Switch</button></div>' +
832					'<div class="switchmenu">' + switchMenu + '</div>' +
833					'</div>'
834				);
835				this.$controls.html(
836					'<div class="controls">' +
837					'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
838					controls +
839					'</div>'
840				);
841				this.selectSwitch();
842			}
843		},
844		updateTeamControls: function (type) {
845			var switchables = this.request && this.request.side ? this.battle.myPokemon : [];
846			var maxIndex = Math.min(switchables.length, 24);
847
848			var requestTitle = "";
849			if (this.choice.done) {
850				requestTitle = '<button name="clearChoice">Back</button> ' + "What about the rest of your team?";
851			} else {
852				requestTitle = "How will you start the battle?";
853			}
854
855			var switchMenu = '';
856			for (var i = 0; i < maxIndex; i++) {
857				var oIndex = this.choice.teamPreview[i] - 1;
858				var pokemon = switchables[oIndex];
859				var tooltipArgs = 'switchpokemon|' + oIndex;
860				if (i < this.choice.done) {
861					switchMenu += '<button disabled="disabled" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
862				} else {
863					switchMenu += '<button name="chooseTeamPreview" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '</button> ';
864				}
865			}
866
867			var controls = (
868				'<div class="switchcontrols">' +
869				'<div class="switchselect">
869<button name="selectSwitch">' + (this.choice.done ? '' + "Choose a Pokémon for slot " + (this.choice.done + 1) : "Choose Lead") + '</button></div>' +
870				'<div class="switchmenu">' + switchMenu + '</div>' +
871				'</div>'
872			);
873			this.$controls.html(
874				'<div class="controls">' +
875				'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
876				controls +
877				'</div>'
878			);
879			this.selectSwitch();
880		},
881		updateWaitControls: function () {
882			var buf = '<div class="controls">';
883			buf += this.getPlayerChoicesHTML();
884			if (!this.battle.nearSide.name || !this.battle.farSide.name || !this.request) {
885				if (this.battle.kickingInactive) {
886					buf += '<p><button class="button" name="setTimer" value="off">Stop timer</button> <small>&larr; Your opponent has disconnected. This will give them more time to reconnect.</small></p>';
887				} else {
888					buf += '<p><button class="button" name="setTimer" value="on">Claim victory</button> <small>&larr; Your opponent has disconnected. Click this if they don\'t reconnect.</small></p>';
889				}
890			}
891			this.$controls.html(buf + '</div>');
892		},
893
894		getPlayerChoicesHTML: function () {
895			var buf = '<p>' + this.getTimerHTML();
896			if (!this.choice || !this.choice.waiting) {
897				return buf + '<em>Waiting for opponent...</em></p>';
898			}
899			buf += '<small>';
900
901			if (this.choice.teamPreview) {
902				var myPokemon = this.battle.mySide.pokemon;
903				var leads = [];
904				for (var i = 0; i < this.choice.count; i++) {
905					leads.push(myPokemon[this.choice.teamPreview[i] - 1].speciesForme);
906				}
907				buf += leads.join(', ') + ' will be sent out first.<br />';
908			} else if (this.choice.choices && this.request && this.battle.myPokemon) {
909				var myPokemon = this.battle.myPokemon;
910				for (var i = 0; i < this.choice.choices.length; i++) {
911					var parts = this.choice.choices[i].split(' ');
912					switch (parts[0]) {
913					case 'move':
914						var move;
915						if (this.request.active[i].maxMoves && !this.request.active[i].canDynamax) { // it's a max move
916							move = this.request.active[i].maxMoves.maxMoves[parseInt(parts[1], 10) - 1].move;
917						} else { // it's a normal move
918							move = this.request.active[i].moves[parseInt(parts[1], 10) - 1].move;
919						}
920						var target = '';
921						buf += myPokemon[i].speciesForme + ' will ';
922						if (parts.length > 2) {
923							var targetPos = parts[2];
924							if (targetPos === 'mega') {
925								buf += 'mega evolve, then ';
926								targetPos = parts[3];
927							}
928							if (targetPos === 'zmove') {
929								move = this.request.active[i].canZMove[parseInt(parts[1], 10) - 1].move;
930								targetPos = parts[3];
931							}
932							if (targetPos === 'dynamax') {
933								move = this.request.active[i].maxMoves.maxMoves[parseInt(parts[1], 10) - 1].move;
934								buf += 'Dynamax, then ';
935								targetPos = parts[3];
936							}
937							if (targetPos) {
938								var targetActive = this.battle.farSide.active;
939								if (targetPos < 0) {
940									// Targeting your own side in doubles / triples
941									targetActive = this.battle.nearSide.active;
942									targetPos = -targetPos;
943									if (this.battle.gameType !== 'freeforall') {
944										target += 'your ';
945									}
946								}
947								if (targetActive[targetPos - 1]) {
948									target += targetActive[targetPos - 1].speciesForme;
949								} else {
950									target += 'slot ' + targetPos; // targeting an empty slot
951								}
952							}
953						}
954						buf += 'use ' + Dex.moves.get(move).name + (target ? ' at ' + target : '') + '.<br />';
955						break;
956					case 'switch':
957						buf += '' + myPokemon[parts[1] - 1].speciesForme + ' will switch in';
958						if (myPokemon[i]) {
959							buf += ', replacing ' + myPokemon[i].speciesForme;
960						}
961						buf += '.<br />';
962						break;
963					case 'shift':
964						buf += myPokemon[i].speciesForme + ' will shift position.<br />';
965						break;
966					}
967				}
968			}
969			buf += '</small></p>';
970			if (!this.finalDecision && !this.battle.hardcoreMode) {
971				buf += '<p><small><em>Waiting for opponent...</em></small> <button class="button" name="undoChoice">Cancel</button></p>';
972			}
973			return buf;
974		},
975
976		/**
977		 * Sends a decision; pass it an array of choices like ['move 1', 'switch 2']
978		 * and it'll send `/choose move 1,switch 2|3`
979		 * (where 3 is the rqid).
980		 *
981		 * (The rqid helps verify that the decision is sent in response to the
982		 * correct request.)
983		 */
984		sendDecision: function (message) {
985			if (!$.isArray(message)) return this.send('/' + message + '|' + this.request.rqid);
986			var buf = '/choose ';
987			for (var i = 0; i < message.length; i++) {
988				if (message[i]) buf += message[i] + ',';
989			}
990			this.send(buf.substr(0, buf.length - 1) + '|' + this.request.rqid);
991		},
992		request: null,
993		receiveRequest: function (request, choiceText) {
994			if (!request) {
995				this.side = '';
996				return;
997			}
998
999			if (!this.autoTimerActivated && Storage.prefs('autotimer')) {
1000				this.setTimer('on');
1001				this.autoTimerActivated = true;
1002			}
1003
1004			request.requestType = 'move';
1005			if (request.forceSwitch) {
1006				request.requestType = 'switch';
1007			} else if (request.teamPreview) {
1008				request.requestType = 'team';
1009			} else if (request.wait) {
1010				request.requestType = 'wait';
1011			}
1012
1013			this.choice = choiceText ? {waiting: true} : null;
1014			this.finalDecision = this.finalDecisionMove = this.finalDecisionSwitch = false;
1015			this.request = request;
1016			if (request.side) {
1017				this.updateSideLocation(request.side);
1018			}
1019			this.notifyRequest();
1020			this.controlsShown = false;
1021			this.updateControls();
1022		},
1023		notifyRequest: function () {
1024			var oName = this.battle.farSide.name;
1025			if (oName) oName = " against " + oName;
1026			switch (this.request.requestType) {
1027			case 'move':
1028				this.notify("Your move!", "Move in your battle" + oName, 'choice');
1029				break;
1030			case 'switch':
1031				this.notify("Your switch!", "Switch in your battle" + oName, 'choice');
1032				break;
1033			case 'team':
1034				this.notify("Team preview!", "Choose your team order in your battle" + oName, 'choice');
1035				break;
1036			}
1037		},
1038		updateSideLocation: function (sideData) {
1039			if (!sideData.id) return;
1040			this.side = sideData.id;
1041			if (this.battle.mySide.sideid !== this.side) {
1042				this.battle.setPerspective(this.side);
1043				this.$chat = this.$chatFrame.find('.inner');
1044			}
1045		},
1046		updateSide: function () {
1047			var sideData = this.request.side;
1048			this.battle.myPokemon = sideData.pokemon;
1049			this.battle.setPerspective(sideData.id);
1050			for (var i = 0; i < sideData.pokemon.length; i++) {
1051				var pokemonData = sideData.pokemon[i];
1052				if (this.request.active && this.request.active[i]) pokemonData.canGmax = this.request.active[i].gigantamax || false;
1053				this.battle.parseDetails(pokemonData.ident.substr(4), pokemonData.ident, pokemonData.details, pokemonData);
1054				this.battle.parseHealth(pokemonData.condition, pokemonData);
1055				pokemonData.hpDisplay = Pokemon.prototype.hpDisplay;
1056				pokemonData.getPixelRange = Pokemon.prototype.getPixelRange;
1057				pokemonData.getFormattedRange = Pokemon.prototype.getFormattedRange;
1058				pokemonData.getHPColorClass = Pokemon.prototype.getHPColorClass;
1059				pokemonData.getHPColor = Pokemon.prototype.getHPColor;
1060			}
1061		},
1062		addAlly: function (allyData) {
1063			this.battle.myAllyPokemon = allyData.pokemon;
1064			for (var i = 0; i < allyData.pokemon.length; i++) {
1065				var pokemonData = allyData.pokemon[i];
1066				this.battle.parseDetails(pokemonData.ident.substr(4), pokemonData.ident, pokemonData.details, pokemonData);
1067				this.battle.parseHealth(pokemonData.condition, pokemonData);
1068				pokemonData.hpDisplay = Pokemon.prototype.hpDisplay;
1069				pokemonData.getPixelRange = Pokemon.prototype.getPixelRange;
1070				pokemonData.getFormattedRange = Pokemon.prototype.getFormattedRange;
1071				pokemonData.getHPColorClass = Pokemon.prototype.getHPColorClass;
1072				pokemonData.getHPColor = Pokemon.prototype.getHPColor;
1073				pokemonData.side = this.battle.mySide.ally;
1074			}
1075		},
1076
1077		// buttons
1078		joinBattle: function () {
1079			this.send('/joinbattle');
1080		},
1081		setTimer: function (setting) {
1082			this.send('/timer ' + setting);
1083		},
1084		forfeit: function () {
1085			this.send('/forfeit');
1086		},
1087		saveReplay: function () {
1088			this.send('/savereplay');
1089		},
1090		openBattleOptions: function () {
1091			app.addPopup(BattleOptionsPopup, {battle: this.battle, room: this});
1092		},
1093		clickReplayDownloadButton: function (e) {
1094			var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, '');
1095
1096			// ladies and gentlemen, JavaScript dates
1097			var date = new Date();
1098			filename += '-' + date.getFullYear();
1099			filename += (date.getMonth() >= 9 ? '-' : '-0') + (date.getMonth() + 1);
1100			filename += (date.getDate() >= 10 ? '-' : '-0') + date.getDate();
1101
1102			filename += '-' + toID(this.battle.p1.name);
1103			filename += '-' + toID(this.battle.p2.name);
1104
1105			e.currentTarget.href = BattleLog.createReplayFileHref(this);
1106			e.currentTarget.download = filename + '.html';
1107
1108			e.stopPropagation();
1109		},
1110		switchSides: function () {
1111			this.battle.switchSides();
1112		},
1113		pause: function () {
1114			this.tooltips.hideTooltip();
1115			this.battlePaused = true;
1116			this.battle.pause();
1117			this.updateControls();
1118		},
1119		resume: function () {
1120			this.tooltips.hideTooltip();
1121			this.battlePaused = false;
1122			this.battle.play();
1123			this.updateControls();
1124		},
1125		instantReplay: function () {
1126			this.tooltips.hideTooltip();
1127			this.request = null;
1128			this.battlePaused = false;
1129			this.battle.reset();
1130			this.battle.play();
1131		},
1132		skipTurn: function () {
1133			this.battle.skipTurn();
1134		},
1135		rewindTurn: function () {
1136			if (this.battle.turn) {
1137				this.battle.seekTurn(this.battle.turn - 1);
1138			}
1139		},
1140		goToEnd: function () {
1141			this.battle.seekTurn(Infinity);
1142		},
1143		register: function (userid) {
1144			var registered = app.user.get('registered');
1145			if (registered && registered.userid !== userid) registered = false;
1146			if (!registered && userid === app.user.get('userid')) {
1147				app.addPopup(RegisterPopup);
1148			}
1149		},
1150		closeAndMainMenu: function () {
1151			this.close();
1152			app.focusRoom('');
1153		},
1154		closeAndRematch: function () {
1155			app.rooms[''].requestNotifications();
1156			app.rooms[''].challenge(this.battle.farSide.name, this.battle.tier);
1157			this.close();
1158			app.focusRoom('');
1159		},
1160
1161		// choice buttons
1162		chooseMove: function (pos, e) {
1163			if (!this.choice) return;
1164			this.tooltips.hideTooltip();
1165
1166			if (pos !== undefined) { // pos === undefined if called by chooseMoveTarget()
1167				var nearActive = this.battle.nearSide.active;
1168				var isMega = !!(this.$('input[name=megaevo]')[0] || '').checked;
1169				var isZMove = !!(this.$('input[name=zmove]')[0] || '').checked;
1170				var isUltraBurst = !!(this.$('input[name=ultraburst]')[0] || '').checked;
1171				var isDynamax = !!(this.$('input[name=dynamax]')[0] || '').checked;
1172
1173				var target = e.getAttribute('data-target');
1174				var choosableTargets = {normal: 1, any: 1, adjacentAlly: 1, adjacentAllyOrSelf: 1, adjacentFoe: 1};
1175
1176				this.choice.choices.push('move ' + pos + (isMega ? ' mega' : '') + (isZMove ? ' zmove' : '') + (isUltraBurst ? ' ultra' : '') + (isDynamax ? ' dynamax' : ''));
1177				if (nearActive.length > 1 && target in choosableTargets) {
1178					this.choice.type = 'movetarget';
1179					this.choice.moveTarget = target;
1180					this.updateControlsForPlayer();
1181					return false;
1182				}
1183			}
1184
1185			this.endChoice();
1186		},
1187		chooseMoveTarget: function (posString) {
1188			this.choice.choices[this.choice.choices.length - 1] += ' ' + posString;
1189			this.chooseMove();
1190		},
1191		chooseShift: function () {
1192			if (!this.choice) return;
1193			this.tooltips.hideTooltip();
1194
1195			this.choice.choices.push('shift');
1196			this.endChoice();
1197		},
1198		chooseSwitch: function (pos) {
1199			if (!this.choice) return;
1200			this.tooltips.hideTooltip();
1201
1202			if (pos !== undefined) { // pos === undefined if called by chooseSwitchTarget()
1203				this.choice.switchFlags[pos] = true;
1204				if (this.choice.freedomDegrees >= 1) {
1205					// Request selection of a Pokémon that will be switched out.
1206					this.choice.type = 'switchposition';
1207					this.updateControlsForPlayer();
1208					return false;
1209				}
1210				// Default: left to right.
1211				this.choice.switchOutFlags[this.choice.choices.length] = true;
1212				this.choice.choices.push('switch ' + (parseInt(pos, 10) + 1));
1213				this.endChoice();
1214				return;
1215			}
1216
1217			// After choosing the position to which a pokemon will switch in (Doubles/Triples end-game).
1218			if (!this.request || this.request.requestType !== 'switch') return false; //??
1219			if (this.choice.canSwitch > _.filter(this.choice.choices, function (choice) {return choice;}).length) {
1220				// More switches are pending.
1221				this.choice.type = 'switch2';
1222				this.updateControlsForPlayer();
1223				return false;
1224			}
1225
1226			this.endTurn();
1227		},
1228		chooseSwitchTarget: function (posString) {
1229			var slotSwitchIn = 0; // one-based
1230			for (var i in this.choice.switchFlags) {
1231				if (this.choice.choices.indexOf('switch ' + (+i + 1)) === -1) {
1232					slotSwitchIn = +i + 1;
1233					break;
1234				}
1235			}
1236			this.choice.choices[posString] = 'switch ' + slotSwitchIn;
1237			this.choice.switchOutFlags[posString] = true;
1238			this.chooseSwitch();
1239		},
1240		chooseTeamPreview: function (pos) {
1241			if (!this.choice) return;
1242			pos = parseInt(pos, 10);
1243			this.tooltips.hideTooltip();
1244			if (this.choice.count) {
1245				var temp = this.choice.teamPreview[pos];
1246				this.choice.teamPreview[pos] = this.choice.teamPreview[this.choice.done];
1247				this.choice.teamPreview[this.choice.done] = temp;
1248
1249				this.choice.done++;
1250
1251				if (this.choice.done < Math.min(this.choice.teamPreview.length, this.choice.count)) {
1252					this.choice.type = 'team2';
1253					this.updateControlsForPlayer();
1254					return false;
1255				}
1256			} else {
1257				this.choice.teamPreview = [pos + 1];
1258			}
1259
1260			this.endTurn();
1261		},
1262		chooseDisabled: function (data) {
1263			this.tooltips.hideTooltip();
1264			data = data.split(',');
1265			if (data[1] === 'fainted') {
1266				app.addPopupMessage("" + data[0] + " has no energy left to battle!");
1267			} else if (data[1] === 'notMine') {
1268				app.addPopupMessage("You cannot decide for your partner!");
1269			} else if (data[1] === 'trapped') {
1270				app.addPopupMessage("You are trapped and cannot select " + data[0] + "!");
1271			} else if (data[1] === 'active') {
1272				app.addPopupMessage("" + data[0] + " is already in battle!");
1273			} else {
1274				app.addPopupMessage("" + data[0] + " is already selected!");
1275			}
1276		},
1277		endChoice: function () {
1278			var choiceIndex = this.choice.choices.length - 1;
1279			if (!this.nextChoice()) {
1280				this.endTurn();
1281			} else if (this.request.partial) {
1282				for (var i = choiceIndex; i < this.choice.choices.length; i++) {
1283					this.sendDecision(this.choice.choices[i]);
1284				}
1285			}
1286		},
1287		nextChoice: function () {
1288			var choices = this.choice.choices;
1289			var nearActive = this.battle.nearSide.active;
1290
1291			if (this.request.requestType === 'switch' && this.request.forceSwitch !== true) {
1292				while (choices.length < this.battle.pokemonControlled && !this.request.forceSwitch[choices.length]) {
1293					choices.push('pass');
1294				}
1295				if (choices.length < this.battle.pokemonControlled) {
1296					this.choice.type = 'switch2';
1297					this.updateControlsForPlayer();
1298					return true;
1299				}
1300			} else if (this.request.requestType === 'move') {
1301				while (choices.length < this.battle.pokemonControlled && !nearActive[choices.length]) {
1302					choices.push('pass');
1303				}
1304
1305				if (choices.length < this.battle.pokemonControlled) {
1306					this.choice.type = 'move2';
1307					this.updateControlsForPlayer();
1308					return true;
1309				}
1310			}
1311
1312			return false;
1313		},
1314		endTurn: function () {
1315			var act = this.request && this.request.requestType;
1316			if (act === 'team') {
1317				if (this.choice.teamPreview.length >= 10) {
1318					this.sendDecision('team ' + this.choice.teamPreview.join(','));
1319				} else {
1320					this.sendDecision('team ' + this.choice.teamPreview.join(''));
1321				}
1322			} else {
1323				if (act === 'switch') {
1324					// Assert that the remaining Pokémon won't switch, even though
1325					// the player could have decided otherwise.
1326					for (var i = 0; i < this.battle.pokemonControlled; i++) {
1327						if (!this.choice.choices[i]) this.choice.choices[i] = 'pass';
1328					}
1329				}
1330
1331				if (this.choice.choices.length >= (this.choice.count || this.battle.pokemonControlled || this.request.active.length)) {
1332					this.sendDecision(this.choice.choices);
1333				}
1334
1335				if (!this.finalDecision) {
1336					var lastChoice = this.choice.choices[this.choice.choices.length - 1];
1337					if (lastChoice.substr(0, 5) === 'move ' && this.finalDecisionMove) {
1338						this.finalDecisionMove = true;
1339					} else if (lastChoice.substr(0, 7) === 'switch' && this.finalDecisionSwitch) {
1340						this.finalDecisionSwitch = true;
1341					}
1342				}
1343			}
1344			this.closeNotification('choice');
1345
1346			this.choice.waiting = true;
1347			this.updateControlsForPlayer();
1348		},
1349		undoChoice: function (pos) {
1350			this.send('/undo');
1351			this.notifyRequest();
1352
1353			this.clearChoice();
1354		},
1355		clearChoice: function () {
1356			this.choice = null;
1357			this.updateControlsForPlayer();
1358		},
1359		leaveBattle: function () {
1360			this.tooltips.hideTooltip();
1361			this.send('/leavebattle');
1362			this.side = '';
1363			this.closeNotification('choice');
1364		},
1365		selectSwitch: function () {
1366			this.tooltips.hideTooltip();
1367			this.$controls.find('.controls').attr('class', 'controls switch-controls');
1368		},
1369		selectMove: function () {
1370			this.tooltips.hideTooltip();
1371			this.$controls.find('.controls').attr('class', 'controls move-controls');
1372		}
1373	}, {
1374		readReplayFile: function (file) {
1375			var reader = new FileReader();
1376			reader.onload = function (e) {
1377				app.removeRoom('battle-uploadedreplay');
1378				var html = e.target.result;
1379				var titleStart = html.indexOf('<title>');
1380				var titleEnd = html.indexOf('</title>');
1381				var title = 'Uploaded Replay';
1382				if (titleStart >= 0 && titleEnd > titleStart) {
1383					title = html.slice(titleStart + 7, titleEnd - 1);
1384					var colonIndex = title.indexOf(':');
1385					var hyphenIndex = title.lastIndexOf('-');
1386					if (hyphenIndex > colonIndex + 2) {
1387						title = title.substring(colonIndex + 2, hyphenIndex - 1);
1388					} else {
1389						title = title.substring(colonIndex + 2);
1390					}
1391				}
1392				var index1 = html.indexOf('<script type="text/plain" class="battle-log-data">');
1393				var index2 = html.indexOf('<script type="text/plain" class="log">');
1394				if (index1 < 0 && index2 < 0) return alert("Unrecognized HTML file: Only replay files are supported.");
1395				if (index1 >= 0) {
1396					html = html.slice(index1 + 50);
1397				} else if (index2 >= 0) {
1398					html = html.slice(index2 + 38);
1399				}
1400				var index3 = html.indexOf('</script>');
1401				html = html.slice(0, index3);
1402				html = html.replace(/\\\//g, '/');
1403				app.receive('>battle-uploadedreplay\n|init|battle\n|title|' + title + '\n' + html);
1404				app.receive('>battle-uploadedreplay\n|expire|Uploaded replay');
1405			};
1406			reader.readAsText(file);
1407		}
1408	});
1409
1410	var ForfeitPopup = this.ForfeitPopup = Popup.extend({
1411		type: 'semimodal',
1412		initialize: function (data) {
1413			this.room = data.room;
1414			this.gameType = data.gameType;
1415			var buf = '<form><p>';
1416			if (this.gameType === 'battle') {
1417				buf += 'Forfeiting makes you lose the battle, and will not earn you or your opponent points from using Saurbot Analyze.';
1418			} else if (this.gameType === 'help') {
1419				buf += 'Leaving the room will close the ticket.';
1420			} else {
1421				// game
1422				buf += 'Forfeiting makes you lose the game.';
1423			}
1424			if (this.gameType === 'help') {
1425				buf += ' Are you sure?</p><p><label><input type="checkbox" name="closeroom" checked /> Close room</label></p>';
1426				buf += '<p><button type="submit"><strong>Close ticket</strong></button> ';
1427			} else {
1428				buf += ' Are you sure?</p><p><label><input type="checkbox" name="closeroom" checked /> Close after forfeiting</label></p>';
1429				buf += '<p><button type="submit"><strong>Forfeit</strong></button> ';
1430			}
1431			if (this.gameType === 'battle' && this.room.battle && !this.room.battle.rated) {
1432				buf += '<button type="button" name="replacePlayer">Replace player</button> ';
1433			}
1434			buf += '<button type="button" name="close" class="autofocus">Cancel</button></p></form>';
1435			this.$el.html(buf);
1436		},
1437		replacePlayer: function (data) {
1438			var room = this.room;
1439			var self = this;
1440			app.addPopupPrompt("Replacement player's username", "Replace player", function (target) {
1441				if (!target) return;
1442				var side = (room.battle.mySide.id === room.battle.p1.id ? 'p1' : 'p2');
1443				room.leaveBattle();
1444				room.send('/addplayer ' + target + ', ' + side);
1445				self.close();
1446			});
1447		},
1448		submit: function (data) {
1449			this.room.send('/forfeit');
1450			if (this.gameType === 'battle') this.room.battle.forfeitPending = true;
1451			if (this.$('input[name=closeroom]')[0].checked) {
1452				app.removeRoom(this.room.id);
1453			}
1454			this.close();
1455		}
1456	});
1457
1458	var BattleOptionsPopup = this.BattleOptionsPopup = Popup.extend({
1459		initialize: function (data) {
1460			this.battle = data.battle;
1461			this.room = data.room;
1462			var rightPanelBattlesPossible = (MainMenuRoom.prototype.bestWidth + BattleRoom.prototype.minWidth < $(window).width());
1463			var buf = '<p><strong>In this battle</strong></p>';
1464			buf += '<p><label class="optlabel"><input type="checkbox" name="hardcoremode"' + (this.battle.hardcoreMode ? ' checked' : '') + '/> Hardcore mode (hide info not shown in-game) (beta)</label></p>';
1465			buf += '<p><label class="optlabel"><input type="checkbox" name="ignorespects"' + (this.battle.ignoreSpects ? ' checked' : '') + '/> Ignore spectators</label></p>';
1466			buf += '<p><label class="optlabel"><input type="checkbox" name="ignoreopp"' + (this.battle.ignoreOpponent ? ' checked' : '') + '/> Ignore opponent</label></p>';
1467			buf += '<p><strong>All battles</strong></p>';
1468			buf += '<p><label class="optlabel"><input type="checkbox" name="ignorenicks"' + (Dex.prefs('ignorenicks') ? ' checked' : '') + ' /> Ignore nicknames</label></p>';
1469			buf += '<p><label class="optlabel"><input type="checkbox" name="allignorespects"' + (Dex.prefs('ignorespects') ? ' checked' : '') + '/> Ignore spectators</label></p>';
1470			buf += '<p><label class="optlabel"><input type="checkbox" name="allignoreopp"' + (Dex.prefs('ignoreopp') ? ' checked' : '') + '/> Ignore opponent</label></p>';
1471			buf += '<p><label class="optlabel"><input type="checkbox" name="autotimer"' + (Dex.prefs('autotimer') ? ' checked' : '') + '/> Automatically start timer</label></p>';
1472			if (rightPanelBattlesPossible) buf += '<p><label class="optlabel"><input type="checkbox" name="rightpanelbattles"' + (Dex.prefs('rightpanelbattles') ? ' checked' : '') + ' /> Open new battles on the right side</label></p>';
1473			buf += '<p><button name="close">Close</button></p>';
1474			this.$el.html(buf);
1475		},
1476		events: {
1477			'change input[name=ignorespects]': 'toggleIgnoreSpects',
1478			'change input[name=ignorenicks]': 'toggleIgnoreNicks',
1479			'change input[name=ignoreopp]': 'toggleIgnoreOpponent',
1480			'change input[name=hardcoremode]': 'toggleHardcoreMode',
1481			'change input[name=allignorespects]': 'toggleAllIgnoreSpects',
1482			'change input[name=allignoreopp]': 'toggleAllIgnoreOpponent',
1483			'change input[name=autotimer]': 'toggleAutoTimer',
1484			'change input[name=rightpanelbattles]': 'toggleRightPanelBattles'
1485		},
1486		toggleHardcoreMode: function (e) {
1487			this.room.setHardcoreMode(!!e.currentTarget.checked);
1488			if (this.battle.hardcoreMode) {
1489				this.battle.add('Hardcore mode ON: Information not available in-game is now hidden.');
1490			} else {
1491				this.battle.add('Hardcore mode OFF: Information not available in-game is now shown.');
1492			}
1493		},
1494		toggleIgnoreSpects: function (e) {
1495			this.battle.ignoreSpects = !!e.currentTarget.checked;
1496			this.battle.add('Spectators ' + (this.battle.ignoreSpects ? '' : 'no longer ') + 'ignored.');
1497			var $messages = $('.battle-log').find('.chat').has('small').not(':contains(\u2605), :contains(\u2606)');
1498			if (!$messages.length) return;
1499			if (this.battle.ignoreSpects) {
1500				$messages.hide();
1501			} else {
1502				$messages.show();
1503			}
1504		},
1505		toggleAllIgnoreSpects: function (e) {
1506			var ignoreSpects = !!e.currentTarget.checked;
1507			Storage.prefs('ignorespects', ignoreSpects);
1508			if (ignoreSpects && !this.battle.ignoreSpects) this.$el.find('input[name=ignorespects]').click();
1509		},
1510		toggleIgnoreNicks: function (e) {
1511			this.battle.ignoreNicks = !!e.currentTarget.checked;
1512			Storage.prefs('ignorenicks', this.battle.ignoreNicks);
1513			this.battle.add('Nicknames ' + (this.battle.ignoreNicks ? '' : 'no longer ') + 'ignored.');
1514			this.battle.resetToCurrentTurn();
1515		},
1516		toggleIgnoreOpponent: function (e) {
1517			this.battle.ignoreOpponent = !!e.currentTarget.checked;
1518			this.battle.add('Opponent ' + (this.battle.ignoreOpponent ? '' : 'no longer ') + 'ignored.');
1519			this.battle.resetToCurrentTurn();
1520		},
1521		toggleAllIgnoreOpponent: function (e) {
1522			var ignoreOpponent = !!e.currentTarget.checked;
1523			Storage.prefs('ignoreopp', ignoreOpponent);
1524			if (ignoreOpponent && !this.battle.ignoreOpponent) this.$el.find('input[name=ignoreopp]').click();
1525		},
1526		toggleAutoTimer: function (e) {
1527			var autoTimer = !!e.currentTarget.checked;
1528			Storage.prefs('autotimer', autoTimer);
1529			if (autoTimer) {
1530				this.room.setTimer('on');
1531				this.room.autoTimerActivated = true;
1532			}
1533		},
1534		toggleRightPanelBattles: function (e) {
1535			Storage.prefs('rightpanelbattles', !!e.currentTarget.checked);
1536		}
1537	});
1538
1539	var TimerPopup = this.TimerPopup = Popup.extend({
1540		initialize: function (data) {
1541			this.room = data.room;
1542			if (this.room.battle.kickingInactive) {
1543				this.$el.html('<p><button name="timerOff"><strong>Stop timer</strong></button></p>');
1544			} else {
1545				this.$el.html('<p><button name="timerOn"><strong>Start timer</strong></button></p>');
1546			}
1547		},
1548		timerOff: function () {
1549			this.room.setTimer('off');
1550			this.close();
1551		},
1552		timerOn: function () {
1553			this.room.setTimer('on');
1554			this.close();
1555		}
1556	});
1557
1558}).call(this, jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.