(function ($) {
Config.sockjsprefix = '/showdown';
Config.root = '/';
if (window.nodewebkit) {
window.gui = require('nw.gui');
window.nwWindow = gui.Window.get();
}
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
// Android mobile-web-app-capable doesn't support it very well, but iOS
// does it fine, so we're only going to show this to iOS for now
$('head').append('');
}
$(document).on('keydown', function (e) {
if (e.keyCode == 27) { // Esc
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
app.closePopup();
}
});
$(window).on('dragover', function (e) {
if (/^text/.test(e.target.type)) return; // Ignore text fields
e.preventDefault();
});
$(document).on('dragenter', function (e) {
if (/^text/.test(e.target.type)) return; // Ignore text fields
e.preventDefault();
if (!app.dragging && app.curRoom.id === 'teambuilder') {
var dataTransfer = e.originalEvent.dataTransfer;
if (dataTransfer.files && dataTransfer.files[0]) {
var file = dataTransfer.files[0];
if (file.name.slice(-4) === '.txt') {
// Someone dragged in a .txt file, hand it to the teambuilder
app.curRoom.defaultDragEnterTeam(e);
}
} else if (dataTransfer.items && dataTransfer.items[0]) {
// no files or no permission to access files
var item = dataTransfer.items[0];
if (item.kind === 'file' && item.type === 'text/plain') {
// Someone dragged in a .txt file, hand it to the teambuilder
app.curRoom.defaultDragEnterTeam(e);
}
}
}
// dropEffect !== 'none' prevents buggy bounce-back animation in
// Chrome/Safari/Opera
e.originalEvent.dataTransfer.dropEffect = 'move';
});
$(window).on('drop', function (e) {
if (/^text/.test(e.target.type)) return; // Ignore text fields
// The default team drop action for Firefox is to open the team as a
// URL, which needs to be prevented.
// The default file drop action for most browsers is to open the file
// in the tab, which is generally undesirable anyway.
e.preventDefault();
if (app.dragging && app.draggingRoom) {
app.rooms[app.draggingRoom].defaultDropTeam(e);
} else if (e.originalEvent.dataTransfer.files && e.originalEvent.dataTransfer.files[0]) {
var file = e.originalEvent.dataTransfer.files[0];
if (file.name.slice(-4) === '.txt' && app.curRoom.id === 'teambuilder') {
// Someone dragged in a .txt file, hand it to the teambuilder
app.curRoom.defaultDragEnterTeam(e);
app.curRoom.defaultDropTeam(e);
} else if (file.type && file.type.substr(0, 6) === 'image/') {
// It's an image file, try to set it as a background
CustomBackgroundPopup.readFile(file);
} else if (file.type && file.type === 'text/html') {
BattleRoom.readReplayFile(file);
}
}
});
if (window.nodewebkit) {
$(document).on("contextmenu", function (e) {
e.preventDefault();
var target = e.target;
var isEditable = (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT');
var menu = new gui.Menu();
if (isEditable) menu.append(new gui.MenuItem({
label: "Cut",
click: function () {
document.execCommand("cut");
}
}));
var link = $(target).closest('a')[0];
if (link) menu.append(new gui.MenuItem({
label: "Copy Link URL",
click: function () {
gui.Clipboard.get().set(link.href);
}
}));
if (target.tagName === 'IMG') menu.append(new gui.MenuItem({
label: "Copy Image URL",
click: function () {
gui.Clipboard.get().set(target.src);
}
}));
menu.append(new gui.MenuItem({
label: "Copy",
click: function () {
document.execCommand("copy");
}
}));
if (isEditable) menu.append(new gui.MenuItem({
label: "Paste",
enabled: !!gui.Clipboard.get().get(),
click: function () {
document.execCommand("paste");
}
}));
menu.popup(e.originalEvent.x, e.originalEvent.y);
});
}
// support Safari 6 notifications
if (!window.Notification && window.webkitNotification) {
window.Notification = window.webkitNotification;
}
// this is called being lazy
window.selectTab = function (tab) {
app.tryJoinRoom(tab);
return false;
};
var User = this.User = Backbone.Model.extend({
defaults: {
name: '',
userid: '',
registered: false,
named: false,
avatar: 0
},
initialize: function () {
app.addGlobalListeners();
app.on('response:userdetails', function (data) {
if (data.userid === this.get('userid')) {
this.set('avatar', data.avatar);
}
}, this);
var self = this;
this.on('change:name', function () {
if (!self.get('named')) {
self.nameRegExp = null;
} else {
var escaped = self.get('name').replace(/[^A-Za-z0-9]+$/, '');
// we'll use `,` as a sentinel character to mean "any non-alphanumeric char"
// unicode characters can be replaced with any non-alphanumeric char
for (var i = escaped.length - 1; i > 0; i--) {
if (/[^\ -\~]/.test(escaped[i])) {
escaped = escaped.slice(0, i) + ',' + escaped.slice(i + 1);
}
}
escaped = escaped.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
escaped = escaped.replace(/,/g, "[^A-Za-z0-9]?");
self.nameRegExp = new RegExp('(?:\\b|(?!\\w))' + escaped + '(?:\\b|\\B(?!\\w))', 'i');
}
});
var replaceList = {'A': 'AⱯȺ', 'B': 'BƂƁɃ', 'C': 'CꜾȻ', 'D': 'DĐƋƊƉꝹ', 'E': 'EƐƎ', 'F': 'FƑꝻ', 'G': 'GꞠꝽꝾ', 'H': 'HĦⱧⱵꞍ', 'I': 'IƗ', 'J': 'JɈ', 'K': 'KꞢ', 'L': 'LꝆꞀ', 'M': 'MⱮƜ', 'N': 'NȠƝꞐꞤ', 'O': 'OǪǬØǾƆƟꝊꝌ', 'P': 'PƤⱣꝐꝒꝔ', 'Q': 'QꝖꝘɊ', 'R': 'RɌⱤꝚꞦꞂ', 'S': 'SẞꞨꞄ', 'T': 'TŦƬƮȾꞆ', 'U': 'UɄ', 'V': 'VƲꝞɅ', 'W': 'WⱲ', 'X': 'X', 'Y': 'YɎỾ', 'Z': 'ZƵȤⱿⱫꝢ', 'a': 'aąⱥɐ', 'b': 'bƀƃɓ', 'c': 'cȼꜿↄ', 'd': 'dđƌɖɗꝺ', 'e': 'eɇɛǝ', 'f': 'fḟƒꝼ', 'g': 'gɠꞡᵹꝿ', 'h': 'hħⱨⱶɥ', 'i': 'iɨı', 'j': 'jɉ', 'k': 'kƙⱪꝁꝃꝅꞣ', 'l': 'lſłƚɫⱡꝉꞁꝇ', 'm': 'mɱɯ', 'n': 'nƞɲʼnꞑꞥ', 'o': 'oǫǭøǿɔꝋꝍɵ', 'p': 'pƥᵽꝑꝓꝕ', 'q': 'qɋꝗꝙ', 'r': 'rɍɽꝛꞧꞃ', 's': 'sꞩꞅẛ', 't': 'tŧƭʈⱦꞇ', 'u': 'uưừứữửựųṷṵʉ', 'v': 'vʋꝟʌ', 'w': 'wⱳ', 'x': 'x', 'y': 'yɏỿ', 'z': 'zƶȥɀⱬꝣ', 'AA': 'Ꜳ', 'AE': 'ÆǼǢ', 'AO': 'Ꜵ', 'AU': 'Ꜷ', 'AV': 'ꜸꜺ', 'AY': 'Ꜽ', 'DZ': 'DZDŽ', 'Dz': 'DzDž', 'LJ': 'LJ', 'Lj': 'Lj', 'NJ': 'NJ', 'Nj': 'Nj', 'OI': 'Ƣ', 'OO': 'Ꝏ', 'OU': 'Ȣ', 'TZ': 'Ꜩ', 'VY': 'Ꝡ', 'aa': 'ꜳ', 'ae': 'æǽǣ', 'ao': 'ꜵ', 'au': 'ꜷ', 'av': 'ꜹꜻ', 'ay': 'ꜽ', 'dz': 'dzdž', 'hv': 'ƕ', 'lj': 'lj', 'nj': 'nj', 'oi': 'ƣ', 'ou': 'ȣ', 'oo': 'ꝏ', 'ss': 'ß', 'tz': 'ꜩ', 'vy': 'ꝡ'};
var normalizeList = {'A': 'ÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄ', 'B': 'ḂḄḆ', 'C': 'ĆĈĊČÇḈƇ', 'D': 'ḊĎḌḐḒḎ', 'E': 'ÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚ', 'F': 'Ḟ', 'G': 'ǴĜḠĞĠǦĢǤƓ', 'H': 'ĤḢḦȞḤḨḪ', 'I': 'ÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬ', 'J': 'Ĵ', 'K': 'ḰǨḲĶḴƘⱩꝀꝂꝄ', 'L': 'ĿĹĽḶḸĻḼḺŁȽⱢⱠꝈ', 'M': 'ḾṀṂ', 'N': 'ǸŃÑṄŇṆŅṊṈ', 'O': 'ÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘ', 'P': 'ṔṖ', 'Q': '', 'R': 'ŔṘŘȐȒṚṜŖṞ', 'S': 'ŚṤŜṠŠṦṢṨȘŞⱾ', 'T': 'ṪŤṬȚŢṰṮ', 'U': 'ÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴ', 'V': 'ṼṾ', 'W': 'ẀẂŴẆẄẈ', 'X': 'ẊẌ', 'Y': 'ỲÝŶỸȲẎŸỶỴƳ', 'Z': 'ŹẐŻŽẒẔ', 'a': 'ẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁ', 'b': 'ḃḅḇ', 'c': 'ćĉċčçḉƈ', 'd': 'ḋďḍḑḓḏ', 'e': 'èéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛ', 'f': '', 'g': 'ǵĝḡğġǧģǥ', 'h': 'ĥḣḧȟḥḩḫẖ', 'i': 'ìíîĩīĭïḯỉǐȉȋịįḭ', 'j': 'ĵǰ', 'k': 'ḱǩḳķḵ', 'l': 'ŀĺľḷḹļḽḻ', 'm': 'ḿṁṃ', 'n': 'ǹńñṅňṇņṋṉ', 'o': 'òóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộ', 'p': 'ṕṗ', 'q': '', 'r': 'ŕṙřȑȓṛṝŗṟ', 's': 'śṥŝṡšṧṣṩșşȿ', 't': 'ṫẗťṭțţṱṯ', 'u': 'ùúûũṹūṻŭüǜǘǖǚủůűǔȕȗụṳ', 'v': 'ṽṿ', 'w': 'ẁẃŵẇẅẘẉ', 'x': 'ẋẍ', 'y': 'ỳýŷỹȳẏÿỷẙỵƴ', 'z': 'źẑżžẓẕ'};
for (var i in replaceList) {
replaceList[i] = new RegExp('[' + replaceList[i] + ']', 'g');
}
for (var i in normalizeList) {
normalizeList[i] = new RegExp('[' + normalizeList[i] + ']', 'g');
}
this.replaceList = replaceList;
this.normalizeList = normalizeList;
},
/**
* Return the path to the login server `action.php` file. AJAX requests
* to this file will always be made on the `play.pokemonshowdown.com`
* domain in order to have access to the correct cookies.
*/
getActionPHP: function () {
var ret = '/~~' + Config.server.id + '/action.php';
if (Config.testclient) {
ret = 'https://' + Config.origindomain + ret;
}
return (this.getActionPHP = function () {
return ret;
})();
},
/**
* Process a signed assertion returned from the login server.
* Emits the following events (arguments in brackets):
*
* `login:authrequired` (name)
* triggered if the user needs to authenticate with this name
*
* `login:invalidname` (name, error)
* triggered if the user's name is invalid
*
* `login:noresponse`
* triggered if the login server did not return a response
*/
finishRename: function (name, assertion) {
if (assertion.slice(0, 14).toLowerCase() === '');
if (endIndex > 0) assertion = assertion.slice(endIndex + 1);
}
if (assertion.charAt(0) === '\r') assertion = assertion.slice(1);
if (assertion.charAt(0) === '\n') assertion = assertion.slice(1);
if (assertion.indexOf('<') >= 0) {
app.addPopupMessage("Something is interfering with our connection to the login server. Most likely, your internet provider needs you to re-log-in, or your internet provider is blocking Pokémon Showdown.");
return;
}
if (assertion === ';') {
this.trigger('login:authrequired', name);
} else if (assertion === ';;@gmail') {
this.trigger('login:authrequired', name, '@gmail');
} else if (assertion.substr(0, 2) === ';;') {
this.trigger('login:invalidname', name, assertion.substr(2));
} else if (assertion.indexOf('\n') >= 0 || !assertion) {
app.addPopupMessage("Something is interfering with our connection to the login server.");
} else {
app.send('/trn ' + name + ',0,' + assertion);
}
},
/**
* Rename this user to an arbitrary username. If the username is
* registered and the user does not currently have a session
* associated with that userid, then the user will be required to
* authenticate.
*
* See `finishRename` above for a list of events this can emit.
*/
rename: function (name) {
// | , ; are not valid characters in names
name = name.replace(/[\|,;]+/g, '');
for (var i in this.replaceList) {
name = name.replace(this.replaceList[i], i);
}
for (var i in this.normalizeList) {
name = name.replace(this.normalizeList[i], i);
}
var userid = toUserid(name);
if (!userid) {
app.addPopupMessage("Usernames must contain at least one letter.");
return;
}
if (this.get('userid') !== userid) {
var self = this;
$.post(this.getActionPHP(), {
act: 'getassertion',
userid: userid,
challstr: this.challstr
}, function (data) {
self.finishRename(name, data);
});
} else {
app.send('/trn ' + name);
}
},
passwordRename: function (name, password, special) {
var self = this;
$.post(this.getActionPHP(), {
act: 'login',
name: name,
pass: password,
challstr: this.challstr
}, Storage.safeJSON(function (data) {
if (data && data.curuser && data.curuser.loggedin) {
// success!
self.set('registered', data.curuser);
self.finishRename(name, data.assertion);
} else {
// wrong password
if (special === '@gmail') {
try {
gapi.auth2.getAuthInstance().signOut(); // eslint-disable-line no-undef
} catch (e) {}
}
app.addPopup(LoginPasswordPopup, {
username: name,
error: data.error || 'Wrong password.',
special: special
});
}
}), 'text');
},
challstr: '',
receiveChallstr: function (challstr) {
if (challstr) {
/**
* Rename the user based on the `sid` and `showdown_username` cookies.
* Specifically, if the user has a valid session, the user will be
* renamed to the username associated with that session. If the user
* does not have a valid session but does have a persistent username
* (i.e. a `showdown_username` cookie), the user will be renamed to
* that name; if that name is registered, the user will be required
* to authenticate.
*
* See `finishRename` above for a list of events this can emit.
*/
this.challstr = challstr;
var self = this;
$.post(this.getActionPHP(), {
act: 'upkeep',
challstr: this.challstr
}, Storage.safeJSON(function (data) {
self.loaded = true;
if (!data.username) {
app.topbar.updateUserbar();
return;
}
// | , ; are not valid characters in names
data.username = data.username.replace(/[\|,;]+/g, '');
if (data.loggedin) {
self.set('registered', {
username: data.username,
userid: toUserid(data.username)
});
}
self.finishRename(data.username, data.assertion);
}), 'text');
}
},
/**
* Log out from the server (but remain connected as a guest).
*/
logout: function () {
$.post(this.getActionPHP(), {
act: 'logout',
userid: this.get('userid')
});
app.send('/logout');
app.trigger('init:socketclosed', "You have been logged out and disconnected.
If you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command.", false);
app.socket.close();
},
setPersistentName: function (name) {
if (location.host !== 'play.pokemonshowdown.com') return;
$.cookie('showdown_username', (name !== undefined) ? name : this.get('name'), {
expires: 14
});
}
});
this.App = Backbone.Router.extend({
root: '/',
routes: {
'*path': 'dispatchFragment'
},
focused: true,
initialize: function () {
window.app = this;
this.initializeRooms();
this.initializePopups();
this.user = new User();
this.ignore = {};
this.supports = {};
// down
// if (document.location.hostname === 'play.pokemonshowdown.com') this.down = 'dos';
this.addRoom('');
this.topbar = new Topbar({el: $('#header')});
if (this.down) {
this.isDisconnected = true;
} else {
if (document.location.hostname === 'play.pokemonshowdown.com' || Config.testclient) {
this.addRoom('rooms', null, true);
} else {
this.addRoom('lobby', null, true);
}
Storage.whenPrefsLoaded(function () {
if (!Config.server.registered) {
app.send('/autojoin');
Backbone.history.start({pushState: !Config.testclient});
return;
}
var autojoin = (Dex.prefs('autojoin') || '');
var autojoinIds = [];
if (typeof autojoin === 'string') {
// Use the existing autojoin string for showdown, and an empty string for other servers.
if (Config.server.id !== 'showdown') autojoin = '';
} else {
// If there is not autojoin data for this server, use a empty string.
autojoin = autojoin[Config.server.id] || '';
}
if (autojoin) {
var autojoins = autojoin.split(',');
for (var i = 0; i < autojoins.length; i++) {
var roomid = toRoomid(autojoins[i]);
app.addRoom(roomid, null, true, autojoins[i]);
if (roomid === 'staff' || roomid === 'upperstaff') continue;
if (Config.server.id !== 'showdown' && roomid === 'lobby') continue;
autojoinIds.push(roomid);
}
}
app.send('/autojoin ' + autojoinIds.join(','));
// HTML5 history throws exceptions when running on file://
Backbone.history.start({pushState: !Config.testclient});
});
}
var self = this;
Storage.whenPrefsLoaded(function () {
Storage.prefs('bg', null);
var muted = Dex.prefs('mute');
BattleSound.setMute(muted);
$('html').toggleClass('dark', !!Dex.prefs('dark'));
var effectVolume = Dex.prefs('effectvolume');
if (effectVolume !== undefined) BattleSound.setEffectVolume(effectVolume);
var musicVolume = Dex.prefs('musicvolume');
if (musicVolume !== undefined) BattleSound.setBgmVolume(musicVolume);
if (Dex.prefs('logchat')) Storage.startLoggingChat();
if (Dex.prefs('showdebug')) {
var debugStyle = $('#debugstyle').get(0);
var onCSS = '.debug {display: block;}';
if (!debugStyle) {
$('head').append('');
} else {
debugStyle.innerHTML = onCSS;
}
}
if (Dex.prefs('onepanel')) {
self.singlePanelMode = true;
self.updateLayout();
}
if (Dex.prefs('bwgfx') || Dex.prefs('noanim')) {
// since xy data is loaded by default, only call
// loadSpriteData if we want bw sprites or if we need bw
// sprite data (if animations are disabled)
Dex.loadSpriteData('bw');
}
});
this.on('init:unsupported', function () {
self.addPopupMessage('Your browser is unsupported.');
});
this.on('init:nothirdparty', function () {
self.addPopupMessage('You have third-party cookies disabled in your browser, which is likely to cause problems. You should enable them and then refresh this page.');
});
this.on('init:socketclosed', function (message, showNotification) {
// Display a desktop notification if the user won't immediately see the popup.
if (self.isDisconnected) return;
self.isDisconnected = true;
if (showNotification !== false && (self.popups.length || !self.focused) && window.Notification) {
self.rooms[''].requestNotifications();
var disconnect = new Notification("Disconnected!", {lang: 'en', body: "You have been disconnected from Pokémon Showdown."});
disconnect.onclick = function (e) {
window.focus();
};
}
self.rooms[''].updateFormats();
$('.pm-log-add form').html('You are disconnected and cannot chat.');
$('.chat-log-add').html('You are disconnected and cannot chat.');
self.reconnectPending = (message || true);
if (!self.popups.length) self.addPopup(ReconnectPopup, {message: message});
});
this.on('init:connectionerror', function () {
self.isDisconnected = true;
self.rooms[''].updateFormats();
self.addPopup(ReconnectPopup, {cantconnect: true});
});
this.user.on('login:invalidname', function (name, reason) {
self.addPopup(LoginPopup, {name: name, reason: reason});
});
this.user.on('login:authrequired', function (name, special) {
self.addPopup(LoginPasswordPopup, {username: name, special: special});
});
this.on('response:savereplay', this.uploadReplay, this);
this.on('response:rooms', this.roomsResponse, this);
if (window.nodewebkit) {
nwWindow.on('focus', function () {
if (!self.focused) {
self.focused = true;
if (self.curRoom) self.curRoom.dismissNotification();
if (self.curSideRoom) self.curSideRoom.dismissNotification();
}
});
nwWindow.on('blur', function () {
self.focused = false;
});
} else {
$(window).on('focus click', function () {
if (!self.focused) {
self.focused = true;
if (self.curRoom) self.curRoom.dismissNotification();
if (self.curSideRoom) self.curSideRoom.dismissNotification();
}
});
$(window).on('blur', function () {
self.focused = false;
});
}
$(window).on('beforeunload', function (e) {
if (Config.server && Config.server.host === 'localhost') return;
if (app.isDisconnected) return;
for (var id in self.rooms) {
var room = self.rooms[id];
if (room && room.requestLeave && !room.requestLeave()) return "You have active battles.";
}
});
$(window).on('keydown', function (e) {
var el = e.target;
var tagName = el.tagName.toUpperCase();
// keypress happened in an empty textarea or a button
var safeLocation = ((tagName === 'TEXTAREA' && !el.value.length) || tagName === 'BUTTON');
var isMac = (navigator.userAgent.indexOf("Mac") !== -1);
if (e.keyCode === 70 && window.nodewebkit && (isMac ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
e.stopImmediatePropagation();
var query = window.getSelection().toString();
query = window.prompt("find?", query);
if (query) window.find(query);
return;
}
if (e.keyCode === 71 && window.nodewebkit && (isMac ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
e.stopImmediatePropagation();
var query = window.getSelection().toString();
if (query) window.find(query);
return;
}
if (app.curSideRoom && $(e.target).closest(app.curSideRoom.$el).length) {
// keypress happened in sideroom
if (e.shiftKey && e.keyCode === 37 && safeLocation) {
// Shift+Left on desktop client
if (app.moveRoomBy(app.curSideRoom, -1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.shiftKey && e.keyCode === 39 && safeLocation) {
// Shift+Right on desktop client
if (app.moveRoomBy(app.curSideRoom, 1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.keyCode === 37 && safeLocation || window.nodewebkit && e.ctrlKey && e.shiftKey && e.keyCode === 9) {
// Left or Ctrl+Shift+Tab on desktop client
if (app.focusRoomBy(app.curSideRoom, -1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.keyCode === 39 && safeLocation || window.nodewebkit && e.ctrlKey && e.keyCode === 9) {
// Right or Ctrl+Tab on desktop client
if (app.focusRoomBy(app.curSideRoom, 1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
return;
}
// keypress happened outside of sideroom
if (e.shiftKey && e.keyCode === 37 && safeLocation) {
// Shift+Left on desktop client
if (app.moveRoomBy(app.curRoom, -1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.shiftKey && e.keyCode === 39 && safeLocation) {
// Shift+Right on desktop client
if (app.moveRoomBy(app.curRoom, 1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.keyCode === 37 && safeLocation || window.nodewebkit && e.ctrlKey && e.shiftKey && e.keyCode === 9) {
// Left or Ctrl+Shift+Tab on desktop client
if (app.focusRoomBy(app.curRoom, -1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
} else if (e.keyCode === 39 && safeLocation || window.nodewebkit && e.ctrlKey && e.keyCode === 9) {
// Right or Ctrl+Tab on desktop client
if (app.focusRoomBy(app.curRoom, 1)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
});
Storage.whenAppLoaded.load(this);
this.initializeConnection();
},
/**
* Start up the client, including loading teams and preferences,
* determining which server to connect to, and actually establishing
* a connection to that server.
*
* Triggers the following events (arguments in brackets):
* `init:unsupported`
* triggered if the user's browser is unsupported
*
* `init:nothirdparty`
* triggered if the user has third-party cookies disabled and
* third-party cookies/storage are necessary for full functioning
* (i.e. stuff will probably be broken for the user, so show an
* error message)
*
* `init:socketopened`
* triggered once a socket has been opened to the sim server; this
* does NOT mean that the user has signed in yet, merely that the
* SockJS connection has been established.
*
* `init:connectionerror`
* triggered if a connection to the sim server could not be
* established
*
* `init:socketclosed`
* triggered if the SockJS socket closes
*/
initializeConnection: function () {
Storage.whenPrefsLoaded(function () {
// if (Config.server.id !== 'smogtours') Config.server.afd = true;
app.connect();
});
},
/**
* This function establishes the actual connection to the sim server.
* This is intended to be called only by `initializeConnection` above.
* Don't call this function directly.
*/
connect: function () {
if (this.down) return;
if (Config.server.banned || (Config.bannedHosts && Config.bannedHosts.indexOf(Config.server.host) >= 0)) {
this.addPopupMessage("This server has been deleted for breaking US laws, impersonating PS global staff, or other major rulebreaking.");
return;
}
var self = this;
var constructSocket = function () {
var protocol = (Config.server.port === 443) ? 'https' : 'http';
Config.server.host = $.trim(Config.server.host);
return new SockJS(protocol + '://' + Config.server.host + ':' +
Config.server.port + Config.sockjsprefix);
};
this.socket = constructSocket();
setInterval(function () {
if (Config.server.host !== $.trim(Config.server.host)) {
app.socket.close();
}
}, 500);
var socketopened = false;
var altport = (Config.server.port === Config.server.altport);
var altprefix = false;
this.socket.onopen = function () {
socketopened = true;
if (altport && window.ga) {
ga('send', 'event', 'Alt port connection', Config.server.id);
}
self.trigger('init:socketopened');
var avatar = Dex.prefs('avatar');
if (avatar) {
// This will be compatible even with servers that don't support
// the second argument for /avatar yet.
self.send('/avatar ' + avatar + ',1');
}
if (self.sendQueue) {
var queue = self.sendQueue;
delete self.sendQueue;
for (var i = 0; i < queue.length; i++) {
self.send(queue[i], true);
}
}
};
this.socket.onmessage = function (msg) {
if (window.console && console.log) {
console.log('<< ' + msg.data);
}
self.receive(msg.data);
};
var reconstructSocket = function (socket) {
var s = constructSocket();
s.onopen = socket.onopen;
s.onmessage = socket.onmessage;
s.onclose = socket.onclose;
return s;
};
this.socket.onclose = function () {
if (!socketopened) {
if (Config.server.altport && !altport) {
altport = true;
Config.server.port = Config.server.altport;
self.socket = reconstructSocket(self.socket);
return;
}
if (!altprefix) {
altprefix = true;
Config.sockjsprefix = '';
self.socket = reconstructSocket(self.socket);
return;
}
return self.trigger('init:connectionerror');
}
self.trigger('init:socketclosed');
};
},
dispatchFragment: function (fragment) {
if (!Config.testclient && location.search && window.history) {
history.replaceState(null, null, location.pathname);
}
this.fragment = fragment = toRoomid(fragment || '');
if (this.initialFragment === undefined) this.initialFragment = fragment;
this.tryJoinRoom(fragment);
this.updateTitle(this.rooms[fragment]);
},
/**
* Send to sim server
*/
send: function (data, room) {
if (room && room !== 'lobby' && room !== true) {
data = room + '|' + data;
} else if (room !== true) {
data = '|' + data;
}
if (!this.socket || (this.socket.readyState !== SockJS.OPEN)) {
if (!this.sendQueue) this.sendQueue = [];
this.sendQueue.push(data);
return;
}
if (window.console && console.log) {
console.log('>> ' + data);
}
this.socket.send(data);
},
/**
* Send team to sim server
*/
sendTeam: function (team) {
var packedTeam = '' + Storage.getPackedTeam(team);
if (packedTeam.length > 100 * 1024 - 6) {
alert("Your team is over 100 KB, usually caused by having over 600 Pokemon in it. Please use a smaller team.");
return;
}
this.send('/utm ' + packedTeam);
},
/**
* Receive from sim server
*/
receive: function (data) {
var roomid = '';
var autojoined = false;
if (data.charAt(0) === '>') {
var nlIndex = data.indexOf('\n');
if (nlIndex < 0) return;
roomid = toRoomid(data.substr(1, nlIndex - 1));
data = data.substr(nlIndex + 1);
}
if (data.substr(0, 6) === '|init|') {
if (!roomid) roomid = 'lobby';
var roomType = data.substr(6);
var roomTypeLFIndex = roomType.indexOf('\n');
if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex);
roomType = toId(roomType);
if (this.rooms[roomid] || roomid === 'staff' || roomid === 'upperstaff') {
// autojoin rooms are joined in background
this.addRoom(roomid, roomType, true);
} else {
this.joinRoom(roomid, roomType, true);
}
if (roomType === 'chat') autojoined = true;
} else if ((data + '|').substr(0, 8) === '|expire|') {
var room = this.rooms[roomid];
if (room) {
room.expired = (data.substr(8) || true);
if (room.updateUser) room.updateUser();
}
return;
} else if ((data + '|').substr(0, 8) === '|deinit|' || (data + '|').substr(0, 8) === '|noinit|') {
if (!roomid) roomid = 'lobby';
if (this.rooms[roomid] && this.rooms[roomid].expired) {
// expired rooms aren't closed when left
return;
}
var isdeinit = (data.charAt(1) === 'd');
data = data.substr(8);
var pipeIndex = data.indexOf('|');
var errormessage;
if (pipeIndex >= 0) {
errormessage = data.substr(pipeIndex + 1);
data = data.substr(0, pipeIndex);
}
// handle error codes here
// data is the error code
if (data === 'namerequired') {
var self = this;
this.once('init:choosename', function () {
self.send('/join ' + roomid);
});
} else if (data === 'rename') {
this.renameRoom(roomid, errormessage);
} else if (data === 'nonexistent' && Config.server.id && roomid.slice(0, 7) === 'battle-' && errormessage) {
var replayid = roomid.slice(7);
if (Config.server.id !== 'showdown') replayid = Config.server.id + '-' + replayid;
var replayLink = 'https://replay.pokemonshowdown.com/' + replayid;
$.ajax(replayLink + '.json', {dataType: 'json'}).done(function (replay) {
if (replay) {
var title = BattleLog.escapeHTML(replay.p1) + ' vs. ' + BattleLog.escapeHTML(replay.p2);
app.receive('>battle-' + replayid + '\n|init|battle\n|title|' + title + '\n' + replay.log);
app.receive('>battle-' + replayid + '\n|expire|Open replay in new tab');
} else {
errormessage += '\n\nResponse received, but no data.';
app.addPopupMessage(errormessage);
}
}).fail(function () {
app.removeRoom(roomid, true);
errormessage += "\n\nThe battle you're looking for has expired. Battles expire after 15 minutes of inactivity unless they're saved.\nIn the future, remember to click \"Save replay\" to save a replay permanently.";
app.addPopupMessage(errormessage);
});
} else if (data !== 'namepending') {
if (isdeinit) { // deinit
if (this.rooms[roomid] && this.rooms[roomid].type === 'chat') {
this.removeRoom(roomid, true);
this.updateAutojoin();
} else {
this.removeRoom(roomid, true);
}
} else { // noinit
this.unjoinRoom(roomid);
if (roomid === 'lobby') this.joinRoom('rooms');
}
if (errormessage) this.addPopupMessage(errormessage);
}
return;
} else if (data.substr(0, 3) === '|N|') {
var names = data.substr(1).split('|');
if (app.ignore[toUserid(names[2])]) {
app.ignore[toUserid(names[1])] = 1;
}
}
if (roomid) {
if (this.rooms[roomid]) {
this.rooms[roomid].receive(data);
}
if (autojoined) this.updateAutojoin();
return;
}
// Since roomid is blank, it could be either a global message or
// a lobby message. (For bandwidth reasons, lobby messages can
// have blank roomids.)
// If it starts with a messagetype in the global messagetype
// list, we'll assume global; otherwise, we'll assume lobby.
var parts;
if (data.charAt(0) === '|') {
parts = data.substr(1).split('|');
} else {
parts = [];
}
switch (parts[0]) {
case 'customgroups':
var nlIndex = data.indexOf('\n');
if (nlIndex > 0) {
this.receive(data.substr(nlIndex + 1));
}
var tarRow = data.slice(14, nlIndex);
this.parseGroups(tarRow);
break;
case 'challstr':
if (parts[2]) {
this.user.receiveChallstr(parts[1] + '|' + parts[2]);
} else {
this.user.receiveChallstr(parts[1]);
}
break;
case 'formats':
this.parseFormats(parts);
break;
case 'updateuser':
var nlIndex = data.indexOf('\n');
if (nlIndex > 0) {
this.receive(data.substr(nlIndex + 1));
nlIndex = parts[3].indexOf('\n');
parts[3] = parts[3].substr(0, nlIndex);
}
var name = parts[1];
var named = !!+parts[2];
var userid = toUserid(name);
if (userid === this.user.get('userid') && name !== this.user.get('name')) {
$.post(app.user.getActionPHP(), {
act: 'changeusername',
username: name
}, function () {}, 'text');
}
this.user.set({
name: name,
userid: userid,
named: named,
avatar: parts[3]
});
this.user.setPersistentName(named ? name : null);
if (named) {
this.trigger('init:choosename');
}
if (app.ignore[toUserid(name)]) {
delete app.ignore[toUserid(name)];
}
break;
case 'nametaken':
app.addPopup(LoginPopup, {name: parts[1] || '', error: parts[2] || ''});
break;
case 'queryresponse':
var responseData = JSON.parse(data.substr(16 + parts[1].length));
app.trigger('response:' + parts[1], responseData);
break;
case 'updatechallenges':
if (this.rooms['']) {
this.rooms[''].updateChallenges(JSON.parse(data.substr(18)));
}
break;
case 'updatesearch':
if (this.rooms['']) {
this.rooms[''].updateSearch(JSON.parse(data.substr(14)));
}
break;
case 'popup':
var maxWidth;
var type = 'semimodal';
data = data.substr(7);
if (data.substr(0, 6) === '|wide|') {
data = data.substr(6);
maxWidth = 960;
}
if (data.substr(0, 7) === '|modal|') {
data = data.substr(7);
type = 'modal';
}
if (data.substr(0, 6) === '|html|') {
data = data.substr(6);
app.addPopup(Popup, {
type: type,
maxWidth: maxWidth,
htmlMessage: BattleLog.sanitizeHTML(data)
});
} else {
app.addPopup(Popup, {
type: type,
maxWidth: maxWidth,
message: data.replace(/\|\|/g, '\n')
});
}
if (this.rooms['']) this.rooms[''].resetPending();
break;
case 'disconnect':
app.trigger('init:socketclosed', BattleLog.sanitizeHTML(data.substr(12)));
break;
case 'pm':
var dataLines = data.split('\n');
for (var i = 0; i < dataLines.length; i++) {
parts = dataLines[i].slice(1).split('|');
var message = parts.slice(3).join('|');
this.rooms[''].addPM(parts[1], message, parts[2]);
if (toUserid(parts[1]) !== app.user.get('userid')) {
app.user.lastPM = toUserid(parts[1]);
}
}
break;
case 'roomerror':
// deprecated; use |deinit| or |noinit|
this.unjoinRoom(parts[1]);
this.addPopupMessage(parts.slice(2).join('|'));
break;
case 'refresh':
// refresh the page
document.location.reload(true);
break;
case 'c':
case 'chat':
if (parts[1] === '~') {
if (parts[2].substr(0, 6) === '/warn ') {
app.addPopup(RulesPopup, {warning: parts[2].substr(6)});
break;
}
}
/* fall through */
default:
// the messagetype wasn't in our list of recognized global
// messagetypes; so the message is presumed to be for the
// lobby.
if (this.rooms['lobby']) {
this.rooms['lobby'].receive(data);
}
break;
}
},
parseGroups: function (groupsList) {
var data = null;
try {
data = JSON.parse(groupsList);
} catch (e) {}
if (!data) return; // broken JSON - keep default ranks
var groups = {};
// process the data and sort into the three auth tiers, 0, 1, and 2
for (var i = 0; i < data.length; i++) {
var entry = data[i];
if (!entry) continue;
var symbol = entry.symbol || ' ';
var groupName = entry.name;
var groupType = entry.type || 'user';
if (groupType === 'normal' && !Config.defaultOrder) Config.defaultOrder = i + 0.5; // this is where any undeclared groups will be positioned in userlist
if (!groupName) Config.defaultGroup = symbol;
groups[symbol] = {
name: groupName ? BattleLog.escapeHTML(groupName + ' (' + symbol + ')') : null,
type: groupType,
order: i + 1
};
}
Config.groups = groups; // if nothing from above crashes (malicious json), then the client will use the new custom groups
},
parseFormats: function (formatsList) {
var isSection = false;
var section = '';
var column = 0;
var columnChanged = false;
window.BattleFormats = {};
for (var j = 1; j < formatsList.length; j++) {
if (isSection) {
section = formatsList[j];
isSection = false;
} else if (formatsList[j] === ',LL') {
app.localLadder = true;
} else if (formatsList[j] === '' || (formatsList[j].charAt(0) === ',' && !isNaN(formatsList[j].substr(1)))) {
isSection = true;
if (formatsList[j]) {
var newColumn = parseInt(formatsList[j].substr(1), 10) || 0;
if (column !== newColumn) {
column = newColumn;
columnChanged = true;
}
}
} else {
var name = formatsList[j];
var searchShow = true;
var challengeShow = true;
var tournamentShow = true;
var team = null;
var teambuilderLevel = null;
var lastCommaIndex = name.lastIndexOf(',');
var code = lastCommaIndex >= 0 ? parseInt(name.substr(lastCommaIndex + 1), 16) : NaN;
if (!isNaN(code)) {
name = name.substr(0, lastCommaIndex);
if (code & 1) team = 'preset';
if (!(code & 2)) searchShow = false;
if (!(code & 4)) challengeShow = false;
if (!(code & 8)) tournamentShow = false;
if (code & 16) teambuilderLevel = 50;
} else {
// Backwards compatibility: late 0.9.0 -> 0.10.0
if (name.substr(name.length - 2) === ',#') { // preset teams
team = 'preset';
name = name.substr(0, name.length - 2);
}
if (name.substr(name.length - 2) === ',,') { // search-only
challengeShow = false;
name = name.substr(0, name.length - 2);
} else if (name.substr(name.length - 1) === ',') { // challenge-only
searchShow = false;
name = name.substr(0, name.length - 1);
}
}
var id = toId(name);
var isTeambuilderFormat = !team && name.slice(-11) !== 'Custom Game';
var teambuilderFormat = '';
var teambuilderFormatName = '';
if (isTeambuilderFormat) {
teambuilderFormatName = name;
if (id.slice(0, 3) !== 'gen') {
teambuilderFormatName = '[Gen 6] ' + name;
}
var parenPos = teambuilderFormatName.indexOf('(');
if (parenPos > 0 && name.slice(-1) === ')') {
// variation of existing tier
teambuilderFormatName = $.trim(teambuilderFormatName.slice(0, parenPos));
}
if (teambuilderFormatName !== name) {
teambuilderFormat = toId(teambuilderFormatName);
if (BattleFormats[teambuilderFormat]) {
BattleFormats[teambuilderFormat].isTeambuilderFormat = true;
} else {
BattleFormats[teambuilderFormat] = {
id: teambuilderFormat,
name: teambuilderFormatName,
team: team,
section: section,
column: column,
rated: false,
isTeambuilderFormat: true,
effectType: 'Format'
};
}
isTeambuilderFormat = false;
}
}
if (BattleFormats[id] && BattleFormats[id].isTeambuilderFormat) {
isTeambuilderFormat = true;
}
// make sure formats aren't out-of-order
if (BattleFormats[id]) delete BattleFormats[id];
BattleFormats[id] = {
id: id,
name: name,
team: team,
section: section,
column: column,
searchShow: searchShow,
challengeShow: challengeShow,
tournamentShow: tournamentShow,
rated: searchShow && id.substr(4, 7) !== 'unrated',
teambuilderLevel: teambuilderLevel,
teambuilderFormat: teambuilderFormat,
isTeambuilderFormat: isTeambuilderFormat,
effectType: 'Format'
};
}
}
// Match base formats to their variants, if they are unavailable in the server.
var multivariantFormats = {};
for (var id in BattleFormats) {
var teambuilderFormat = BattleFormats[BattleFormats[id].teambuilderFormat];
if (!teambuilderFormat || multivariantFormats[teambuilderFormat.id]) continue;
if (!teambuilderFormat.searchShow && !teambuilderFormat.challengeShow && !teambuilderFormat.tournamentShow) {
// The base format is not available.
if (teambuilderFormat.battleFormat) {
multivariantFormats[teambuilderFormat.id] = 1;
teambuilderFormat.battleFormat = '';
} else {
teambuilderFormat.battleFormat = id;
}
}
}
if (columnChanged) app.supports['formatColumns'] = true;
this.trigger('init:formats');
},
uploadReplay: function (data) {
var id = data.id;
var serverid = Config.server.id && toId(Config.server.id.split(':')[0]);
if (serverid && serverid !== 'showdown') id = serverid + '-' + id;
$.post(app.user.getActionPHP() + '?act=uploadreplay', {
log: data.log,
id: id
}, function (data) {
if (data === 'success') {
app.addPopup(ReplayUploadedPopup, {id: id});
} else if (data === 'hash mismatch') {
app.addPopupMessage("Someone else is already uploading a replay of this battle. Try again in five seconds.");
} else if (data === 'not found') {
app.addPopupMessage("This server isn't registered, and doesn't support uploading replays.");
} else if (data === 'invalid id') {
app.addPopupMessage("This server is using invalid battle IDs, so this replay can't be uploaded.");
} else {
app.addPopupMessage("Error while uploading replay: " + data);
}
});
},
roomsResponse: function (data) {
if (data) {
this.roomsData = data;
}
app.topbar.updateTabbar();
},
addGlobalListeners: function () {
$(document).on('click', 'a', function (e) {
if (this.className === 'closebutton') return; // handled elsewhere
if (this.className.indexOf('minilogo') >= 0) return; // handled elsewhere
if (!this.href) return; // should never happen
if (this.host === 'play.pokemonshowdown.com' || this.host === 'psim.us' || this.host === location.host) {
if (!e.cmdKey && !e.metaKey && !e.ctrlKey) {
var target = this.pathname.substr(1);
var shortLinks = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|news|privacy|contact|dex|insecure)$/;
if (target.indexOf('/') < 0 && target.indexOf('.') < 0 && !shortLinks.test(target)) {
if (this.dataset && this.dataset.target === 'replace') {
var roomEl = $(this).closest('.ps-room')[0];
if (roomEl && roomEl.id) {
var roomid = roomEl.id.slice(5);
window.app.renameRoom(roomid, target);
window.app.rooms[target].join();
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
}
window.app.tryJoinRoom(target);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return;
}
}
}
if (window.nodewebkit && this.target === '_blank') {
gui.Shell.openExternal(this.href);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return;
}
if (this.rel === 'noopener') {
var formatOptions = Dex.prefs('chatformatting') || {};
if (!formatOptions.hideinterstice && !BattleLog.interstice.isWhitelisted(this.href)) {
this.href = BattleLog.interstice.getURI(this.href);
}
} else if (this.target === '_blank') {
// for performance reasons, there's no reason to ever have an opener
this.rel = 'noopener';
}
});
},
/*********************************************************
* Rooms
*********************************************************/
initializeRooms: function () {
this.rooms = Object.create(null); // {}
this.roomList = [];
this.sideRoomList = [];
$(window).on('resize', _.bind(this.resize, this));
},
fixedWidth: true,
resize: function () {
if (window.screen && screen.width && screen.width >= 320) {
if (this.fixedWidth) {
document.getElementById('viewport').setAttribute('content', 'width=device-width');
this.fixedWidth = false;
}
} else {
if (!this.fixedWidth) {
document.getElementById('viewport').setAttribute('content', 'width=320');
this.fixedWidth = true;
}
}
if (!app.roomsFirstOpen && !this.down && $(window).width() >= 916 && document.location.hostname === 'play.pokemonshowdown.com') {
this.addRoom('rooms');
}
this.updateLayout();
},
// the currently active room
curRoom: null,
curSideRoom: null,
sideRoom: null,
joinRoom: function (id, type, nojoin) {
if (this.rooms[id]) {
this.focusRoom(id);
if (this.rooms[id].rejoin) this.rooms[id].rejoin();
return this.rooms[id];
}
if (id.substr(0, 11) === 'battle-gen5' && !Dex.loadedSpriteData['bw']) Dex.loadSpriteData('bw');
var room = this._addRoom(id, type, nojoin);
this.focusRoom(id);
return room;
},
/**
* We tried to join a room but it didn't exist
*/
unjoinRoom: function (id, reason) {
this.removeRoom(id, true);
if (this.curRoom) this.navigate(this.curRoom.id, {replace: true});
this.updateAutojoin();
},
tryJoinRoom: function (id) {
this.joinRoom(id);
},
addRoom: function (id, type, nojoin, title) {
this._addRoom(id, type, nojoin, title);
this.updateSideRoom();
this.updateLayout();
},
_addRoom: function (id, type, nojoin, title) {
var oldRoom;
if (this.rooms[id]) {
if (type && this.rooms[id].type !== type) {
// this room changed type
// (or the type we guessed it would be was wrong)
var oldRoom = this.rooms[id];
var index = this.roomList.indexOf(oldRoom);
if (index >= 0) this.roomList.splice(index, 1);
index = this.sideRoomList.indexOf(oldRoom);
if (index >= 0) this.sideRoomList.splice(index, 1);
oldRoom.destroy();
delete this.rooms[id];
} else {
return this.rooms[id];
}
}
var el;
if (!id) {
el = $('#mainmenu');
} else {
el = $('
Your replay has been uploaded! It\'s available at:
'; buf += 'http://replay.pokemonshowdown.com/' + data.id + '
'; buf += ''; this.$el.html(buf).css('max-width', 620); }, clickClose: function () { this.close(); }, submit: function (i) { this.close(); } }); var RulesPopup = this.RulesPopup = Popup.extend({ type: 'modal', initialize: function (data) { var warning = ('warning' in data); var buf = ''; if (warning) { buf += '' + (BattleLog.escapeHTML(data.warning) || 'You have been warned for breaking the rules.') + '
'; } buf += '