(function($) {
Config.version = '0.9.0';
Config.origindomain = 'play.pokemonshowdown.com';
// `defaultserver` specifies the server to use when the domain name in the
// address bar is `Config.origindomain`.
Config.defaultserver = {
id: 'showdown',
host: 'sim.psim.us',
port: 443,
httpport: 8000,
altport: 80,
registered: true
};
Config.sockjsprefix = '/showdown';
Config.root = '/';
// sanitize a room ID
// shouldn't actually do anything except against a malicious server
var toRoomid = this.toRoomid = function(roomid) {
return roomid.replace(/[^a-zA-Z0-9-]+/g, '');
};
// 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;
};
// placeholder until the real chart loads
window.Chart = {
pokemonRow: function() {},
itemRow: function() {},
abilityRow: function() {},
moveRow: function() {}
};
var User = this.User = Backbone.Model.extend({
defaults: {
name: '',
userid: '',
registered: false,
named: false,
avatar: 0
},
initialize: function() {
app.on('response:userdetails', function(data) {
if (data.userid === this.get('userid')) {
this.set('avatar', data.avatar);
}
}, this);
},
/**
* 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 = 'http://' + 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 === ';') {
this.trigger('login:authrequired', name);
} else if (assertion.substr(0, 2) === ';;') {
this.trigger('login:invalidname', name, assertion.substr(2));
} else if (assertion.indexOf('\n') >= 0) {
this.trigger('login:noresponse');
} 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) {
if (this.get('userid') !== toUserid(name)) {
var query = this.getActionPHP() + '?act=getassertion&userid=' +
encodeURIComponent(toUserid(name)) +
'&challengekeyid=' + encodeURIComponent(this.challengekeyid) +
'&challenge=' + encodeURIComponent(this.challenge);
var self = this;
$.get(query, function(data) {
self.finishRename(name, data);
});
} else {
app.send('/trn ' + name);
}
},
passwordRename: function(name, password) {
var self = this;
$.post(this.getActionPHP(), {
act: 'login',
name: name,
pass: password,
challengekeyid: this.challengekeyid,
challenge: this.challenge
}, Tools.safeJSON(function(data) {
if (data && data.curuser && data.curuser.loggedin) {
// success!
self.set('registered', data.curuser);
self.finishRename(name, data.assertion);
} else {
// wrong password
app.addPopup(LoginPasswordPopup, {
username: name,
error: 'Wrong password.'
});
}
}), 'text');
},
challengekeyid: -1,
challenge: '',
receiveChallenge: function(attrs) {
if (attrs.challenge) {
/**
* 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.
*/
var query = this.getActionPHP() + '?act=upkeep' +
'&challengekeyid=' + encodeURIComponent(attrs.challengekeyid) +
'&challenge=' + encodeURIComponent(attrs.challenge);
var self = this;
$.get(query, Tools.safeJSON(function(data) {
if (!data.username) return;
if (data.loggedin) {
self.set('registered', {
username: data.username,
userid: toUserid(data.username)
});
}
self.finishRename(data.username, data.assertion);
}), 'text');
}
this.challengekeyid = attrs.challengekeyid;
this.challenge = attrs.challenge;
},
/**
* 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');
},
setPersistentName: function(name) {
$.cookie('showdown_username', (name !== undefined) ? name : this.get('name'), {
expires: 14
});
},
/**
* This function loads teams from `localStorage` or cookies. This function
* is only used if the client is running on `play.pokemonshowdown.com`. If the
* client is running on another domain, then teams are received from
* `crossdomain.php` instead.
*/
teams: null,
loadTeams: function() {
this.teams = [];
if (window.localStorage) {
var teamString = localStorage.getItem('showdown_teams');
if (teamString) this.teams = JSON.parse(teamString);
} else {
this.cookieTeams = true;
var savedTeam = $.parseJSON($.cookie('showdown_team1'));
if (savedTeam) {
this.teams.push(savedTeam);
}
savedTeam = $.parseJSON($.cookie('showdown_team2'));
if (savedTeam) {
this.teams.push(savedTeam);
}
savedTeam = $.parseJSON($.cookie('showdown_team3'));
if (savedTeam) {
this.teams.push(savedTeam);
}
}
}
});
var App = this.App = Backbone.Router.extend({
root: '/',
routes: {
'*path': 'dispatchFragment'
},
focused: true,
initialize: function() {
window.app = this;
$('#main').html('');
this.initializeRooms();
this.initializePopups();
this.user = new User();
this.ignore = {};
this.topbar = new Topbar({el: $('#header')});
this.addRoom('');
var self = this;
this.on('init:loadprefs', function() {
var bg = Tools.prefs('bg');
if (bg) {
$(document.body).css({
background: bg,
'background-size': 'cover'
});
}
var muted = Tools.prefs('mute');
BattleSound.setMute(muted);
var effectVolume = Tools.prefs('effectvolume');
if (effectVolume !== undefined) BattleSound.setEffectVolume(effectVolume);
var musicVolume = Tools.prefs('musicvolume');
if (musicVolume !== undefined) BattleSound.setBgmVolume(musicVolume);
});
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() {
self.reconnectPending = true;
if (!self.popups.length) self.addPopup(ReconnectPopup);
});
this.on('init:connectionerror', function() {
self.addPopup(ReconnectPopup, {cantconnect: true});
});
this.user.on('login:authrequired', function(name) {
self.addPopup(LoginPasswordPopup, {username: name});
});
this.on('response:savereplay', this.uploadReplay, this);
$(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;
});
this.initializeConnection();
Backbone.history.start({pushState: true});
},
/**
* 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:loadprefs`
* triggered when preferences/teams are finished loading and are
* safe to read
*
* `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() {
if ((document.location.hostname !== Config.origindomain) && !Config.testclient) {
// Handle *.psim.us.
return this.initializeCrossDomainConnection();
}
if (document.location.protocol === 'https:') {
if (!$.cookie('showdown_ssl')) {
// Never used HTTPS before, so we have to copy over the
// HTTP origin localStorage. We have to redirect to the
// HTTP site in order to do this. We set a cookie
// indicating that we redirected for the purpose of copying
// over the localStorage.
$.cookie('showdown_ssl_convert', 1);
return document.location.replace('http://' + document.location.hostname +
document.location.pathname);
}
// Renew the `showdown_ssl` cookie.
$.cookie('showdown_ssl', 1, {expires: 365*3});
} else if (!$.cookie('showdown_ssl')) {
// localStorage is currently located on the HTTP origin.
if (!$.cookie('showdown_ssl_convert') || !('postMessage' in window)) {
// This user is not using HTTPS now and has never used
// HTTPS before, so her localStorage is still under the
// HTTP origin domain: connect on port 8000, not 443.
Config.defaultserver.port = Config.defaultserver.httpport;
} else {
// First time using HTTPS: copy the existing HTTP storage
// over to the HTTPS origin.
$(window).on('message', function($e) {
var e = $e.originalEvent;
var origin = 'https://' + Config.origindomain;
if (e.origin !== origin) return;
if (e.data === 'init') {
app.user.loadTeams();
e.source.postMessage($.toJSON({
teams: $.toJSON(app.user.teams),
prefs: $.toJSON(Tools.prefs.data)
}), origin);
} else if (e.data === 'done') {
// Set a cookie to indicate that localStorage is now under
// the HTTPS origin.
$.cookie('showdown_ssl', 1, {expires: 365*3});
localStorage.clear();
return document.location.replace('https://' + document.location.hostname +
document.location.pathname);
}
});
var $iframe = $('');
$('body').append($iframe);
return;
}
} else if (!Config.testclient) {
// The user is using HTTP right now, but has used HTTPS in the
// past, so her localStorage is located on the HTTPS origin:
// hence we need to use the cross-domain code to load the
// localStorage because the HTTPS origin is considered a
// different domain for the purpose of localStorage.
return this.initializeCrossDomainConnection();
}
// Simple connection: no cross-domain logic needed.
Config.server = Config.server || Config.defaultserver;
this.user.loadTeams();
this.trigger('init:loadprefs');
return this.connect();
},
/**
* Handle a cross-domain connection: that is, a connection where the
* client is loaded from a different domain from the one where the
* user's localStorage is located.
*/
initializeCrossDomainConnection: function() {
if (!('postMessage' in window)) {
// browser does not support cross-document messaging
return this.trigger('init:unsupported');
}
// If the URI in the address bar is not `play.pokemonshowdown.com`,
// we receive teams, prefs, and server connection information from
// crossdomain.php on play.pokemonshowdown.com.
var self = this;
$(window).on('message', (function() {
var origin;
var callbacks = {};
var callbackIdx = 0;
return function($e) {
var e = $e.originalEvent;
if ((e.origin === 'http://' + Config.origindomain) ||
(e.origin === 'https://' + Config.origindomain)) {
origin = e.origin;
} else {
return; // unauthorised source origin
}
var data = $.parseJSON(e.data);
if (data.server) {
var postCrossDomainMessage = function(data) {
return e.source.postMessage($.toJSON(data), origin);
};
// server config information
Config.server = data.server;
if (Config.server.registered) {
var $link = $('');
$('head').append($link);
}
// persistent username
self.user.setPersistentName = function() {
postCrossDomainMessage({username: this.get('name')});
};
// ajax requests
$.get = function(uri, callback, type) {
var idx = callbackIdx++;
callbacks[idx] = callback;
postCrossDomainMessage({get: [uri, idx, type]});
};
$.post = function(uri, data, callback, type) {
var idx = callbackIdx++;
callbacks[idx] = callback;
postCrossDomainMessage({post: [uri, data, idx, type]});
};
// teams
if (data.teams) {
self.user.cookieTeams = false;
self.user.teams = $.parseJSON(data.teams) || [];
} else {
self.user.teams = [];
}
TeambuilderRoom.saveTeams = function() {
postCrossDomainMessage({teams: $.toJSON(app.user.teams)});
};
// prefs
if (data.prefs) {
Tools.prefs.data = $.parseJSON(data.prefs);
}
self.trigger('init:loadprefs');
Tools.prefs.save = function() {
postCrossDomainMessage({prefs: $.toJSON(this.data)});
};
// check for third-party cookies being disabled
if (data.nothirdparty) {
self.trigger('init:nothirdparty');
}
// connect
self.connect();
} else if (data.ajax) {
var idx = data.ajax[0];
if (callbacks[idx]) {
callbacks[idx](data.ajax[1]);
delete callbacks[idx];
}
}
};
})());
var $iframe = $(
''
);
$('body').append($iframe);
},
/**
* 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() {
var self = this;
var constructSocket = function() {
var protocol = (Config.server.port === 443) ? 'https' : 'http';
return new SockJS(protocol + '://' + Config.server.host + ':' +
Config.server.port + Config.sockjsprefix);
};
this.socket = constructSocket();
/**
* This object defines event handles for JSON-style messages.
*/
var events = {
/**
* These are all deprecated. Stop using them. :|
*/
init: function (data) {
if (data.name !== undefined) {
// Legacy
self.user.set({
name: data.name,
userid: toUserid(data.name),
named: data.named
});
}
if (data.room) {
// Correct way to initialize rooms:
// >ROOMID
// |init|ROOMTYPE
// LOG
if (data.room === 'lobby') {
self.addRoom('lobby');
} else {
self.joinRoom(data.room, data.roomType);
}
if (data.log) {
self.rooms[data.room].add(data.log.join('\n'));
} else if (data.battlelog) {
self.rooms[data.room].init(data.battlelog.join('\n'));
}
if (data.u) {
self.rooms[data.room].parseUserList(data.u);
}
}
},
update: function (data) {
if (data.name !== undefined) {
// Legacy
self.user.set({
name: data.name,
userid: toUserid(data.name),
named: data.named
});
self.user.setPersistentName(data.named ? data.name : null);
}
if (data.updates) {
// Correct way to send battlelog updates:
// >ROOMID
// BATTLELOG
var room = self.rooms[data.room];
if (room) room.receive(data.updates.join('\n'));
}
if ('challengesFrom' in data) {
// Legacy
if (self.rooms['']) self.rooms[''].updateChallenges(data);
}
if ('searching' in data) {
// Legacy
if (self.rooms['']) self.rooms[''].updateSearch(data);
}
if ('request' in data) {
// Legacy
var room = self.rooms[data.room];
if (room && room.receiveRequest) {
if (data.request.side) data.request.side.id = data.side;
room.receiveRequest(data.request);
}
}
},
message: function (message) {
// Correct way to send popups:
// |popup|MESSAGE
self.addPopupMessage(message.message);
if (self.rooms['']) self.rooms[''].resetPending();
},
console: function (message) {
if (message.pm) {
// Correct way to send PMs:
// |pm|SOURCE|TARGET|MESSAGE
self.rooms[''].addPM(message.name, message.message, message.pm);
if (self.rooms['lobby'] && !Tools.prefs('nolobbypm')) {
self.rooms['lobby'].addPM(message.name, message.message, message.pm);
}
} else if (message.rawMessage) {
// Correct way to send raw console messages:
// |raw|RAWMESSAGE
self.receive('|raw|'+message.rawMessage);
} else {
// Correct way to send console messages:
// MESSAGE
self.receive(message.message);
}
},
nameTaken: function (data) {
// Legacy -- but still how the server communicates that a
// name is taken, so we need to support it for now.
app.addPopup(LoginPopup, {reason: data.reason || ''});
},
command: function (message) {
// Legacy
self.trigger('response:'+message.command, message);
}
};
var socketopened = false;
var altport = (Config.server.port === Config.server.altport);
var altprefix = false;
this.socket.onopen = function() {
socketopened = true;
if (altport && window._gaq) {
_gaq.push(['_trackEvent', 'Alt port connection', Config.server.id]);
}
self.trigger('init:socketopened');
// Join the lobby if it fits on the screen.
// Send the join message even if it doesn't, for legacy servers.
if (Config.server.id !== 'showdown') {
self.send('{"room":"lobby","nojoin":1,"type":"join"}', true);
}
if ($(window).width() >= 916) {
self.send('/join lobby');
}
var avatar = Tools.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') {
var nlIndex = data.indexOf('\n');
if (nlIndex < 0) return;
roomid = data.substr(1,nlIndex-1);
data = data.substr(nlIndex+1);
}
if (roomid) {
if (data.substr(0,6) === '|init|') {
var roomType = data.substr(6);
var roomTypeLFIndex = roomType.indexOf('\n');
if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex);
roomType = toId(roomType);
if (roomid === 'lobby') {
this.addRoom(roomid, roomType);
} else {
this.joinRoom(roomid, roomType);
}
}
if (this.rooms[roomid]) {
this.rooms[roomid].receive(data);
}
return;
}
var parts;
if (data.charAt(0) === '|') {
parts = data.substr(1).split('|');
} else {
parts = [];
}
switch (parts[0]) {
case 'challenge-string':
case 'challstr':
this.user.receiveChallenge({
challengekeyid: parseInt(parts[1], 10),
challenge: parts[2]
});
break;
case 'formats':
this.parseFormats(parts);
break;
case 'popup':
this.addPopupMessage(data.substr(7).replace(/\|\|/g, '\n'));
if (this.rooms['']) this.rooms[''].resetPending();
break;
case 'pm':
var message = parts.slice(3).join('|');
this.rooms[''].addPM(parts[1], message, parts[2]);
if (this.rooms['lobby'] && !Tools.prefs('nolobbypm')) {
this.rooms['lobby'].addPM(parts[1], message, parts[2]);
}
break;
default:
if (data.substr(0,6) === '|init|') {
this.addRoom('lobby');
}
if (this.rooms['lobby']) {
this.rooms['lobby'].receive(data);
}
break;
}
},
parseFormats: function(formatsList) {
var isSection = false;
var section = '';
BattleFormats = {};
for (var j=1; j 0 && name.charAt(name.length-1) === ')') {
// variation of existing tier
teambuilderFormat = toId(name.substr(0, parenPos));
if (BattleFormats[teambuilderFormat]) {
BattleFormats[teambuilderFormat].isTeambuilderFormat = true;
} else {
BattleFormats[teambuilderFormat] = {
id: teambuilderFormat,
name: $.trim(name.substr(0, parenPos)),
team: team,
section: section,
rated: challengeShow && searchShow,
isTeambuilderFormat: true,
effectType: 'Format'
};
}
isTeambuilderFormat = false;
}
}
if (BattleFormats[id] && BattleFormats[id].isTeambuilderFormat) {
isTeambuilderFormat = true;
}
BattleFormats[id] = {
id: id,
name: name,
team: team,
section: section,
searchShow: searchShow,
challengeShow: challengeShow,
rated: challengeShow && searchShow,
teambuilderFormat: teambuilderFormat,
isTeambuilderFormat: isTeambuilderFormat,
effectType: 'Format'
};
}
}
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 ((serverid === 'showdown') && (data === 'invalid id')) {
data = 'not found';
}
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);
}
});
},
/*********************************************************
* Rooms
*********************************************************/
initializeRooms: function() {
this.rooms = {};
$(window).on('resize', _.bind(this.updateLayout, this));
},
// the currently active room
curRoom: null,
curSideRoom: null,
sideRoom: null,
joinRoom: function(id, type) {
if (this.rooms[id]) {
this.focusRoom(id);
return this.rooms[id];
}
var room = this._addRoom(id, type);
this.focusRoom(id);
return room;
},
tryJoinRoom: function(id) {
if (this.rooms[id] || id === 'teambuilder' || id === 'ladder') {
this.joinRoom(id);
} else {
this.send('/join '+id);
}
},
addRoom: function(id, type) {
this._addRoom(id, type);
this.updateSideRoom();
this.updateLayout();
},
_addRoom: function(id, type) {
if (this.rooms[id]) return this.rooms[id];
var el = $('').appendTo('body');
var typeName = '';
if (typeof type === 'string') {
typeName = type;
type = null;
}
var roomTable = {
'': MainMenuRoom,
'teambuilder': TeambuilderRoom,
'ladder': LadderRoom,
'lobby': ChatRoom,
};
var typeTable = {
'battle': BattleRoom,
'chat': ChatRoom
};
if (roomTable[id]) type = roomTable[id];
if (!type) type = typeTable[typeName];
if (!type) type = ChatRoom;
var room = this.rooms[id] = new type({
id: id,
el: el
});
return room;
},
focusRoom: function(id) {
var room = this.rooms[id];
if (!room) return false;
if (this.curRoom === room || this.curSideRoom === room) return true;
this.updateSideRoom(id);
this.updateLayout();
if (this.curSideRoom !== room) {
if (this.curRoom) {
this.curRoom.hide();
this.curRoom = null;
}
this.curRoom = window.room = room;
this.updateLayout();
if (this.curRoom.id === id) this.navigate(id);
}
return;
},
updateLayout: function() {
if (!this.curRoom) return; // can happen during initialization
this.dismissPopups();
if (!this.sideRoom) {
this.curRoom.show('full');
if (this.curRoom.id === '') {
if ($('body').width() < this.curRoom.bestWidth) {
this.curRoom.$el.addClass('tiny-layout');
} else {
this.curRoom.$el.removeClass('tiny-layout');
}
}
this.topbar.updateTabbar();
return;
}
var leftMin = (this.curRoom.minWidth || this.curRoom.bestWidth);
var rightMin = (this.sideRoom.minWidth || this.sideRoom.bestWidth);
var available = $(window).width();
if (this.curRoom.isSideRoom) {
// we're trying to focus a side room
if (available >= this.rooms[''].tinyWidth + leftMin) {
// it fits to the right of the main menu, so do that
this.curSideRoom = this.sideRoom = this.curRoom;
this.curRoom = this.rooms[''];
leftMin = this.curRoom.tinyWidth;
rightMin = (this.sideRoom.minWidth || this.sideRoom.bestWidth);
} else if (this.sideRoom) {
// nooo
if (this.curSideRoom) {
this.curSideRoom.hide();
this.curSideRoom = null;
}
this.curRoom.show('full');
this.topbar.updateTabbar();
return;
}
} else if (this.curRoom.id === '') {
leftMin = this.curRoom.tinyWidth;
}
if (available < leftMin + rightMin) {
if (this.curSideRoom) {
this.curSideRoom.hide();
this.curSideRoom = null;
}
this.curRoom.show('full');
this.topbar.updateTabbar();
return;
}
this.curSideRoom = this.sideRoom;
if (leftMin === this.curRoom.tinyWidth) {
if (available < this.curRoom.bestWidth + 570) {
// there's only room for the tiny layout :(
this.curRoom.show('left', leftMin);
this.curRoom.$el.addClass('tiny-layout');
this.curSideRoom.show('right', leftMin);
this.topbar.updateTabbar();
return;
}
leftMin = (this.curRoom.minWidth || this.curRoom.bestWidth);
this.curRoom.$el.removeClass('tiny-layout');
}
var leftMax = (this.curRoom.maxWidth || this.curRoom.bestWidth);
var rightMax = (this.sideRoom.maxWidth || this.sideRoom.bestWidth);
var leftWidth = leftMin;
if (leftMax + rightMax <= available) {
leftWidth = leftMax;
} else {
var bufAvailable = available - leftMin - rightMin;
var wanted = leftMax - leftMin + rightMax - rightMin;
if (wanted) leftWidth = Math.floor(leftMin + (leftMax - leftMin) * bufAvailable / wanted);
}
this.curRoom.show('left', leftWidth);
this.curSideRoom.show('right', leftWidth);
this.topbar.updateTabbar();
},
updateSideRoom: function(id) {
if (id && this.rooms[id].isSideRoom) {
this.sideRoom = this.rooms[id];
if (this.curSideRoom && this.curSideRoom !== this.sideRoom) {
this.curSideRoom.hide();
this.curSideRoom = this.sideRoom;
}
// updateLayout will null curSideRoom if there's
// no room for this room
} else if (!this.sideRoom) {
for (var i in this.rooms) {
if (this.rooms[i].isSideRoom) {
this.sideRoom = this.rooms[i];
}
}
}
},
leaveRoom: function(id) {
var room = this.rooms[id];
if (!room) return false;
if (room.requestLeave && !room.requestLeave()) return false;
return this.removeRoom(id);
},
removeRoom: function(id) {
var room = this.rooms[id];
if (room) {
if (room === this.curRoom) this.focusRoom('');
delete this.rooms[id];
room.destroy();
if (room === this.sideRoom) {
this.sideRoom = null;
this.curSideRoom = null;
this.updateSideRoom();
}
this.updateLayout();
return true;
}
return false;
},
/*********************************************************
* Popups
*********************************************************/
popups: null,
initializePopups: function() {
this.popups = [];
},
addPopup: function(type, data) {
if (!data) data = {};
if (data.sourceEl === undefined && app.dispatchingButton) {
data.sourceEl = app.dispatchingButton;
}
if (data.sourcePopup === undefined && app.dispatchingPopup) {
data.sourcePopup = app.dispatchingPopup;
}
if (this.dismissingSource && $(data.sourceEl)[0] === this.dismissingSource) return;
while (this.popups.length) {
var prevPopup = this.popups[this.popups.length-1];
if (data.sourcePopup === prevPopup) {
break;
}
var sourceEl = prevPopup.sourceEl ? prevPopup.sourceEl[0] : null;
this.popups.pop().remove();
if ($(data.sourceEl)[0] === sourceEl) return;
}
if (!type) type = Popup;
var popup = new type(data);
var $overlay;
if (popup.type === 'normal') {
$('body').append(popup.el);
} else {
$overlay = $('').appendTo('body').append(popup.el)
if (popup.type === 'semimodal') {
$overlay.on('click', function(e) {
if (e.currentTarget === e.target) {
popup.close();
}
});
}
}
if (popup.domInitialize) popup.domInitialize(data);
popup.$('.autofocus').focus();
if ($overlay) $overlay.scrollTop(0);
this.popups.push(popup);
return popup;
},
addPopupMessage: function(message) {
// shorthand for adding a popup message
// this is the equivalent of alert(message)
app.addPopup(Popup, {message: message});
},
closePopup: function(id) {
if (this.popups.length) {
var popup = this.popups.pop();
popup.remove();
if (this.reconnectPending) this.addPopup(ReconnectPopup);
return true;
}
return false;
},
dismissPopups: function() {
var source = false;
while (this.popups.length) {
var popup = this.popups[this.popups.length-1];
if (popup.type !== 'normal') return source;
if (popup.sourceEl) source = popup.sourceEl[0];
if (!source) source = true;
this.popups.pop().remove();
}
return source;
}
});
var Topbar = this.Topbar = Backbone.View.extend({
events: {
'click a': 'click',
'click .username': 'clickUsername',
'click button': 'dispatchClickButton'
},
initialize: function() {
this.$el.html('
');
this.$tabbar = this.$('.maintabbar .inner');
// this.$sidetabbar = this.$('.sidetabbar');
this.$userbar = this.$('.userbar');
this.updateTabbar();
app.user.on('change', this.updateUserbar, this);
this.updateUserbar();
},
// userbar
updateUserbar: function() {
var buf = '';
var name = ' '+app.user.get('name');
var color = hashColor(app.user.get('userid'));
if (app.user.get('named')) {
buf = ' '+Tools.escapeHTML(name)+' ';
} else {
buf = ' ';
}
this.$userbar.html(buf);
},
login: function() {
app.addPopup(LoginPopup);
},
openSounds: function() {
app.addPopup(SoundsPopup);
},
openOptions: function() {
app.addPopup(OptionsPopup);
},
clickUsername: function(e) {
e.stopPropagation();
var name = $(e.currentTarget).data('name');
app.addPopup(UserPopup, {name: name, sourceEl: e.currentTarget});
},
// tabbar
updateTabbar: function() {
var curId = (app.curRoom ? app.curRoom.id : '');
var curSideId = (app.curSideRoom ? app.curSideRoom.id : '');
var buf = '
';
var atLeastOne = false;
var sideBuf = '';
for (var id in app.rooms) {
if (!id || id === 'teambuilder' || id === 'ladder') continue;
var room = app.rooms[id];
var name = ' '+id+'';
if (id === 'lobby') name = ' Lobby';
if (id.substr(0,7) === 'battle-') {
var parts = id.substr(7).split('-');
var p1 = (room && room.battle && room.battle.p1 && room.battle.p1.name) || '';
var p2 = (room && room.battle && room.battle.p2 && room.battle.p2.name) || '';
if (p1 && p2) {
name = ''+Tools.escapeHTML(p1)+' v. '+Tools.escapeHTML(p2);
} else if (p1 || p2) {
name = ''+Tools.escapeHTML(p1)+Tools.escapeHTML(p2);
} else {
name = '(empty room)';
}
name = ''+parts[0]+''+name+'';
}
if (room.isSideRoom) {
sideBuf += '
';
this.$el.html(buf).css('max-width', 620);
},
clickClose: function() {
this.close();
},
submit: function(i) {
window.open('http://pokemonshowdown.com/replay/battle-'+this.id, '_blank');
this.close();
}
});
var RulesPopup = this.RulesPopup = Popup.extend({
type: 'modal',
initialize: function(data) {
var warning = ('warning' in data);
var buf = '';
if (warning) {
buf += '
'+(Tools.escapeHTML(data.warning)||'You have been warned for breaking the rules.')+'
';
}
buf += '
Pokémon Showdown Rules
';
buf += 'Global
1. Be nice to people. Respect people. Don\'t be rude to people.
2. PS is based in the US. Follow US laws. Don\'t distribute pirated material, and don\'t slander others. PS is available to users younger than 18, so porn is strictly forbidden.
3. No cheating. Don\'t exploit bugs to gain an unfair advantage. Don\'t game the system (by intentionally losing against yourself or a friend in a ladder match, by timerstalling, etc).
4. English only.
5. The First Amendment does not apply to PS, since PS is not a government organization.
6. Rules are subject to moderator interpretation, punishment is subject to moderator discretion.
';
buf += 'Lobby chat
1. Do not spam, flame, or troll. This includes advertising, asking questions with one-word answers, and flooding the chat by copy/pasting lots of text.
2. Don\'t call unnecessary attention to yourself. Don\'t be obnoxious. ALL CAPS, formatting, and -> ASCII art <- are acceptable to emphasize things, but should be used sparingly, not all the time.
3. No minimodding: don\'t mod if it\'s not your job. Don\'t tell people they\'ll be muted, don\'t ask for people to be muted, and don\'t talk about whether or not people should be muted ("inb4 mute", etc). This applies to bans and other punishments, too.
4. We reserve the right to tell you to stop discussing moderator decisions if you become unreasonable or belligerent.
';
if (!warning) {
buf += 'Usernames
Your username can be chosen and changed at any time. Keep in mind:
1. Usernames may not be derogatory or insulting in nature, to an individual or group (insulting yourself is okay as long as it\'s not too serious).
2. Usernames may not reference sexual activity, directly or indirectly.
3. Usernames may not impersonate a recognized user (a user with %, @, &, or ~ next to their name).
This policy is less restrictive than that of many places, so you might see some "borderline" nicknames that might not be accepted elsewhere. You might consider it unfair that they are allowed to keep their nickname. The fact remains that their nickname follows the above rules, and if you were asked to choose a new name, yours does not.';
}
if (warning) {
buf += '
';
var atLeastOne = false;
var sideBuf = '';
for (var id in app.rooms) {
if (!id || id === 'teambuilder' || id === 'ladder') continue;
var room = app.rooms[id];
var name = ' '+id+'';
if (id === 'lobby') name = ' Lobby';
if (id.substr(0,7) === 'battle-') {
var parts = id.substr(7).split('-');
var p1 = (room && room.battle && room.battle.p1 && room.battle.p1.name) || '';
var p2 = (room && room.battle && room.battle.p2 && room.battle.p2.name) || '';
if (p1 && p2) {
name = ''+Tools.escapeHTML(p1)+' v. '+Tools.escapeHTML(p2);
} else if (p1 || p2) {
name = ''+Tools.escapeHTML(p1)+Tools.escapeHTML(p2);
} else {
name = '(empty room)';
}
name = ''+parts[0]+''+name+'';
}
if (room.isSideRoom) {
sideBuf += '