mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-05-05 21:16:26 -05:00
1690 lines
53 KiB
JavaScript
1690 lines
53 KiB
JavaScript
(function($) {
|
|
|
|
// `defaultserver` specifies the server to use when the domain name in the
|
|
// address bar is `play.pokemonshowdown.com`. If the domain name in the
|
|
// address bar is something else (including `dev.pokemonshowdown.com`), the
|
|
// server to use will be determined by `crossdomain.php`, not this object.
|
|
Config.defaultserver = {
|
|
id: 'showdown',
|
|
host: 'sim.smogon.com',
|
|
port: 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
|
|
// trying to use new-spec notifications on old versions of Chrome will crash
|
|
// certain older versions of Chrome and there doesn't appear to be a way to
|
|
// feature-detect them
|
|
if (!window.Notification && window.webkitNotification && navigator.userAgent.indexOf('Chrome') <= 0) {
|
|
window.Notification = window.webkitNotification;
|
|
}
|
|
|
|
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://play.pokemonshowdown.com' + 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);
|
|
if (!app.socket) {
|
|
document.location.reload();
|
|
return;
|
|
}
|
|
self.finishRename(name, data.assertion);
|
|
} else {
|
|
// wrong password
|
|
app.addPopup('password', 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 (including `dev.pokemonshowdown.com`),
|
|
* 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.topbar = new Topbar({el: $('#header')});
|
|
this.addRoom('');
|
|
|
|
var self = this;
|
|
|
|
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.user.on('login:authrequired', function(name) {
|
|
self.addPopup('password', LoginPasswordPopup, {username: name});
|
|
});
|
|
|
|
$(window).on('focus click', function() {
|
|
if (!self.focused) {
|
|
self.focused = true;
|
|
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:loadteams` (teams)
|
|
* triggered when loads are finished loading
|
|
*
|
|
* `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
|
|
*
|
|
* `init:identify`
|
|
* triggered once the user has successfully identified (i.e. logged
|
|
* in) with the server
|
|
*/
|
|
initializeConnection: function() {
|
|
var origindomain = 'play.pokemonshowdown.com';
|
|
if (document.location.hostname === origindomain) {
|
|
// TODO: BEFORE DEPLOYING THIS, add in code to check for use
|
|
// of http://play.pokemonshowdown.com here.
|
|
this.user.loadTeams();
|
|
this.trigger('init:loadteams');
|
|
Config.server = Config.defaultserver;
|
|
return this.connect();
|
|
} else if (!window.postMessage) {
|
|
// 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://' + origindomain) ||
|
|
(e.origin === 'https://' + 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 = $('<link rel="stylesheet" ' +
|
|
'href="//play.pokemonshowdown.com/customcss.php?server=' +
|
|
encodeURIComponent(Config.server.id) + '" />');
|
|
$('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)});
|
|
};
|
|
self.trigger('init:loadteams');
|
|
// prefs
|
|
if (data.prefs) {
|
|
Tools.prefs.data = $.parseJSON(data.prefs);
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
};
|
|
})());
|
|
// Note that the URI here is intentionally `play.pokemonshowdown.com`,
|
|
// and not `dev.pokemonshowdown.com`, in order to make teams, prefs,
|
|
// and other things work properly.
|
|
var $iframe = $(
|
|
'<iframe src="//play.pokemonshowdown.com/crossdomain.php?host=' +
|
|
encodeURIComponent(document.location.hostname) +
|
|
'&path=' + encodeURIComponent(document.location.pathname.substr(1)) +
|
|
'" style="display: none;"></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'));
|
|
}
|
|
}
|
|
},
|
|
update: function (data) {
|
|
if (data.name !== undefined) {
|
|
// Legacy
|
|
self.user.set({
|
|
name: data.name,
|
|
userid: toUserid(data.name),
|
|
named: data.named
|
|
});
|
|
if (!data.named) {
|
|
self.user.setPersistentName(null); // kill `showdown_username` cookie
|
|
}
|
|
}
|
|
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 (data.request) {
|
|
// 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: (unimplemented)
|
|
// |popup|MESSAGE
|
|
self.addPopupMessage(message.message);
|
|
if (self.rooms['']) self.rooms[''].resetPending();
|
|
},
|
|
console: function (message) {
|
|
if (message.pm) {
|
|
// Correct way to send PMs: (unimplemented)
|
|
// |pm|SOURCE|TARGET|MESSAGE
|
|
self.rooms[''].addPM(message.name, message.message, message.pm);
|
|
if (self.rooms['lobby']) {
|
|
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);
|
|
}
|
|
},
|
|
disconnect: function () {},
|
|
nameTaken: function (data) {},
|
|
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. This is necessary for now.
|
|
// TODO: Revise this later if desired.
|
|
self.send({room: 'lobby'}, 'join');
|
|
if (self.sendQueue) {
|
|
var queue = self.sendQueue;
|
|
delete self.sendQueue;
|
|
for (var i=0; i<queue.length; i++) {
|
|
self.send(queue[i]);
|
|
}
|
|
}
|
|
};
|
|
this.socket.onmessage = function(msg) {
|
|
if (msg.data.substr(0,1) !== '{') {
|
|
self.receive(msg.data);
|
|
return;
|
|
}
|
|
var data = $.parseJSON(msg.data);
|
|
if (!data) return;
|
|
// Handle JSON messages.
|
|
if (events[data.type]) events[data.type](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) {
|
|
this.tryJoinRoom(fragment||'');
|
|
},
|
|
/**
|
|
* Send to sim server
|
|
*/
|
|
send: function(data, type) {
|
|
if (!this.socket) {
|
|
if (typeof data === 'string') {
|
|
if (!this.sendQueue) this.sendQueue = [];
|
|
this.sendQueue.push(data);
|
|
}
|
|
return;
|
|
}
|
|
if (typeof data === 'object') {
|
|
if (type) data.type = type;
|
|
this.socket.send($.toJSON(data));
|
|
} else {
|
|
this.socket.send('|'+data);
|
|
}
|
|
},
|
|
/**
|
|
* Receive from sim server
|
|
*/
|
|
receive: function(data) {
|
|
console.log('received: '+data);
|
|
var roomid = '';
|
|
if (data.substr(0,1) === '>') {
|
|
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;
|
|
|
|
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<formatsList.length; j++) {
|
|
if (isSection) {
|
|
section = formatsList[j];
|
|
isSection = false;
|
|
} else if (formatsList[j] === '') {
|
|
isSection = true;
|
|
} else {
|
|
var searchShow = true;
|
|
var challengeShow = true;
|
|
var team = null;
|
|
var name = formatsList[j];
|
|
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);
|
|
}
|
|
BattleFormats[toId(name)] = {
|
|
id: toId(name),
|
|
name: name,
|
|
team: team,
|
|
section: section,
|
|
searchShow: searchShow,
|
|
challengeShow: challengeShow,
|
|
rated: challengeShow && searchShow,
|
|
isTeambuilderFormat: challengeShow && searchShow && !team,
|
|
effectType: 'Format'
|
|
};
|
|
}
|
|
}
|
|
this.trigger('init:formats');
|
|
},
|
|
|
|
/*********************************************************
|
|
* 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 = $('<div class="ps-room" style="display:none"></div>').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(id, type, data) {
|
|
if (!data) data = {};
|
|
|
|
if (data.sourceEl === undefined && app.dispatchingButton) {
|
|
data.sourceEl = app.dispatchingButton;
|
|
}
|
|
if (this.dismissingSource && $(data.sourceEl)[0] === this.dismissingSource) return;
|
|
while (this.popups.length) {
|
|
var prevPopup = this.popups[this.popups.length-1];
|
|
if (prevPopup.id === id) {
|
|
var sourceEl = prevPopup.sourceEl ? prevPopup.sourceEl[0] : null;
|
|
this.popups.pop().remove();
|
|
if ($(data.sourceEl)[0] === sourceEl) return;
|
|
} else if (prevPopup.id !== id.substr(0,prevPopup.id.length)) {
|
|
this.popups.pop().remove();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
data.id = id;
|
|
if (!type) type = Popup;
|
|
|
|
var popup = new type(data);
|
|
|
|
if (popup.type === 'normal') {
|
|
$('body').append(popup.el);
|
|
} else {
|
|
var $overlay = $('<div class="ps-overlay"></div>').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);
|
|
this.popups.push(popup);
|
|
return popup;
|
|
},
|
|
addPopupMessage: function(message) {
|
|
// shorthand for adding a popup message
|
|
// this is the equivalent of alert(message)
|
|
app.addPopup('message', Popup, {message: message});
|
|
},
|
|
closePopup: function(id) {
|
|
if (this.popups.length) {
|
|
var popup = this.popups.pop();
|
|
popup.remove();
|
|
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('<img class="logo" src="//dev.pokemonshowdown.com/pokemonshowdownbeta.png" alt="Pokemon Showdown! (beta)" /><div class="tabbar maintabbar"></div><div class="userbar"></div>');
|
|
this.$tabbar = this.$('.maintabbar');
|
|
// 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 = '<span class="username" data-name="'+Tools.escapeHTML(name)+'" style="'+color+'"><i class="icon-user" style="color:#779EC5"></i> '+Tools.escapeHTML(name)+'</span> <button class="icon" name="openSounds"><i class="'+(Tools.prefs('mute')?'icon-volume-off':'icon-volume-up')+'"></i></button> <button class="icon" name="openOptions"><i class="icon-cog"></i></button>';
|
|
} else {
|
|
buf = '<button name="login">Choose name</button> <button class="icon" name="openSounds"><i class="'+(Tools.prefs('mute')?'icon-volume-off':'icon-volume-up')+'"></i></button> <button class="icon" name="openOptions"><i class="icon-cog"></i></button>';
|
|
}
|
|
this.$userbar.html(buf);
|
|
},
|
|
login: function() {
|
|
app.addPopup('login', LoginPopup);
|
|
},
|
|
openSounds: function() {
|
|
app.addPopup('sounds', SoundsPopup);
|
|
},
|
|
openOptions: function() {
|
|
app.addPopup('options', OptionsPopup);
|
|
},
|
|
clickUsername: function(e) {
|
|
e.stopPropagation();
|
|
var name = $(e.currentTarget).data('name');
|
|
app.addPopup('user', 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 = '<ul><li><a class="button'+(curId===''?' cur':'')+(app.rooms['']&&app.rooms[''].notifications?' notifying':'')+'" href="'+app.root+'"><i class="icon-home"></i> <span>Home</span></a></li>';
|
|
if (app.rooms['teambuilder']) buf += '<li><a class="button'+(curId==='teambuilder'?' cur':'')+' closable" href="'+app.root+'teambuilder"><i class="icon-edit"></i> <span>Teambuilder</span></a><a class="closebutton" href="'+app.root+'teambuilder"><i class="icon-remove-sign"></i></a></li>';
|
|
if (app.rooms['ladder']) buf += '<li><a class="button'+(curId==='ladder'?' cur':'')+' closable" href="'+app.root+'ladder"><i class="icon-list-ol"></i> <span>Ladder</span></a><a class="closebutton" href="'+app.root+'ladder"><i class="icon-remove-sign"></i></a></li>';
|
|
buf += '</ul>';
|
|
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 = '<i class="icon-comment-alt"></i> <span>'+id+'</span>';
|
|
if (id === 'lobby') name = '<i class="icon-comments-alt"></i> <span>Lobby</span>';
|
|
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 = '<i class="text">'+parts[0]+'</i><span>'+name+'</span>';
|
|
}
|
|
if (room.isSideRoom) {
|
|
sideBuf += '<li><a class="button'+(curId===id||curSideId===id?' cur':'')+(room.notifications?' notifying':'')+' closable" href="'+app.root+id+'">'+name+'</a><a class="closebutton" href="'+app.root+id+'"><i class="icon-remove-sign"></i></a></li>';
|
|
continue;
|
|
}
|
|
if (!atLeastOne) {
|
|
buf += '<ul>';
|
|
atLeastOne = true;
|
|
}
|
|
buf += '<li><a class="button'+(curId===id?' cur':'')+(room.notifications?' notifying':'')+' closable" href="'+app.root+id+'">'+name+'</a><a class="closebutton" href="'+app.root+id+'"><i class="icon-remove-sign"></i></a></li>';
|
|
}
|
|
if (atLeastOne) buf += '</ul>';
|
|
if (app.curSideRoom) {
|
|
if (sideBuf) buf += '<ul class="siderooms" style="float:none;margin-left:'+(app.curSideRoom.leftWidth-144)+'px">'+sideBuf+'</ul>';
|
|
this.$tabbar.html(buf);
|
|
} else {
|
|
if (sideBuf) buf += '<ul>'+sideBuf+'</ul>';
|
|
this.$tabbar.html(buf);
|
|
}
|
|
|
|
if (app.rooms['']) app.rooms[''].updateRightMenu();
|
|
},
|
|
dispatchClickButton: function(e) {
|
|
var target = e.currentTarget;
|
|
if (target.name) {
|
|
app.dismissingSource = app.dismissPopups();
|
|
app.dispatchingButton = target;
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
this[target.name].call(this, target.value, target);
|
|
delete app.dismissingSource;
|
|
delete app.dispatchingButton;
|
|
}
|
|
},
|
|
click: function(e) {
|
|
e.preventDefault();
|
|
var $target = $(e.currentTarget);
|
|
var id = $target.attr('href');
|
|
if (id.substr(0, app.root.length) === app.root) {
|
|
id = id.substr(app.root.length);
|
|
}
|
|
if ($target.hasClass('closebutton')) {
|
|
app.leaveRoom(id);
|
|
} else {
|
|
app.focusRoom(id);
|
|
}
|
|
}
|
|
});
|
|
|
|
var Room = this.Room = Backbone.View.extend({
|
|
className: 'ps-room',
|
|
constructor: function() {
|
|
if (!this.events) this.events = {};
|
|
if (!this.events['click button']) this.events['click button'] = 'dispatchClickButton';
|
|
if (!this.events['click']) this.events['click'] = 'dispatchClickBackground';
|
|
|
|
Backbone.View.apply(this, arguments);
|
|
|
|
this.join();
|
|
},
|
|
dispatchClickButton: function(e) {
|
|
var target = e.currentTarget;
|
|
if (target.name) {
|
|
app.dismissingSource = app.dismissPopups();
|
|
app.dispatchingButton = target;
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
this[target.name].call(this, target.value, target);
|
|
delete app.dismissingSource;
|
|
delete app.dispatchingButton;
|
|
}
|
|
},
|
|
dispatchClickBackground: function(e) {
|
|
app.dismissPopups();
|
|
if (e.shiftKey || (window.getSelection && !window.getSelection().isCollapsed)) {
|
|
return;
|
|
}
|
|
this.focus();
|
|
},
|
|
|
|
// communication
|
|
|
|
/**
|
|
* Send to sim server
|
|
*/
|
|
send: function(data, type) {
|
|
if (!app.socket) return;
|
|
if (typeof data === 'object') {
|
|
if (type) data.type = type;
|
|
data.room = this.id;
|
|
app.socket.send($.toJSON(data));
|
|
} else {
|
|
app.socket.send(''+this.id+'|'+data);
|
|
}
|
|
},
|
|
/**
|
|
* Receive from sim server
|
|
*/
|
|
receive: function(data) {
|
|
//
|
|
},
|
|
|
|
// layout
|
|
|
|
bestWidth: 659,
|
|
show: function(position, leftWidth) {
|
|
switch (position) {
|
|
case 'left':
|
|
this.$el.css({left: 0, width: leftWidth, right: 'auto'});
|
|
break;
|
|
case 'right':
|
|
this.$el.css({left: leftWidth+1, width: 'auto', right: 0});
|
|
this.leftWidth = leftWidth;
|
|
break;
|
|
case 'full':
|
|
this.$el.css({left: 0, width: 'auto', right: 0});
|
|
break;
|
|
}
|
|
this.$el.show();
|
|
this.dismissNotification();
|
|
this.focus();
|
|
},
|
|
hide: function() {
|
|
this.blur();
|
|
this.$el.hide();
|
|
},
|
|
focus: function() {},
|
|
blur: function() {},
|
|
join: function() {},
|
|
leave: function() {},
|
|
|
|
// notifications
|
|
|
|
requestNotifications: function() {
|
|
if (window.Notification && Notification.requestPermission) {
|
|
Notification.requestPermission(function(permission) {});
|
|
}
|
|
},
|
|
notifications: null,
|
|
notify: function(title, body, tag, once) {
|
|
if (once && app.focused && (this === app.curRoom || this == app.curSideRoom)) return;
|
|
if (!tag) tag = 'message';
|
|
if (!this.notifications) this.notifications = {};
|
|
if (app.focused && (this === app.curRoom || this == app.curSideRoom)) {
|
|
this.notifications[tag] = {};
|
|
} else if (window.Notification) {
|
|
// old one doesn't need to be closed; sending the tag should
|
|
// automatically replace the old notification
|
|
var notification = this.notifications[tag] = new Notification(title, {
|
|
lang: 'en',
|
|
body: body,
|
|
tag: this.id+':'+tag,
|
|
});
|
|
var self = this;
|
|
notification.onclose = function() {
|
|
self.dismissNotification(tag);
|
|
};
|
|
notification.onclick = function() {
|
|
self.clickNotification(tag);
|
|
};
|
|
if (once) notification.psAutoclose = true;
|
|
} else if (window.macgap) {
|
|
macgap.growl.notify({
|
|
title: title,
|
|
content: body
|
|
});
|
|
this.notifications[tag] = {};
|
|
if (once) notification.psAutoclose = true;
|
|
} else {
|
|
this.notifications[tag] = {};
|
|
if (once) notification.psAutoclose = true;
|
|
}
|
|
app.topbar.updateTabbar();
|
|
},
|
|
notifyOnce: function(title, body, tag) {
|
|
return this.notify(title, body, tag, true);
|
|
},
|
|
closeNotification: function(tag, alreadyClosed) {
|
|
if (!this.notifications) return;
|
|
if (!tag) {
|
|
for (tag in this.notifications) {
|
|
if (this.notifications[tag].close) this.notifications[tag].close();
|
|
}
|
|
this.notifications = null;
|
|
app.topbar.updateTabbar();
|
|
return;
|
|
}
|
|
if (!this.notifications[tag]) return;
|
|
if (!alreadyClosed && this.notifications[tag].close) this.notifications[tag].close();
|
|
delete this.notifications[tag];
|
|
if (_.isEmpty(this.notifications)) {
|
|
this.notifications = null;
|
|
app.topbar.updateTabbar();
|
|
}
|
|
},
|
|
dismissNotification: function(tag) {
|
|
if (!this.notifications) return;
|
|
if (!tag) {
|
|
for (tag in this.notifications) {
|
|
if (!this.notifications[tag].psAutoclose) continue;
|
|
if (this.notifications[tag].close) this.notifications[tag].close();
|
|
delete this.notifications[tag];
|
|
}
|
|
if (_.isEmpty(this.notifications)) {
|
|
this.notifications = null;
|
|
app.topbar.updateTabbar();
|
|
}
|
|
return;
|
|
}
|
|
if (!this.notifications[tag]) return;
|
|
if (this.notifications[tag].close) this.notifications[tag].close();
|
|
if (this.notifications[tag].psAutoclose) {
|
|
delete this.notifications[tag];
|
|
if (_.isEmpty(this.notifications)) {
|
|
this.notifications = null;
|
|
app.topbar.updateTabbar();
|
|
}
|
|
} else {
|
|
this.notifications[tag] = {};
|
|
}
|
|
},
|
|
clickNotification: function(tag) {
|
|
this.dismissNotification(tag);
|
|
app.focusRoom(this.id);
|
|
},
|
|
|
|
// allocation
|
|
|
|
destroy: function() {
|
|
this.closeNotification();
|
|
this.leave();
|
|
this.remove();
|
|
delete this.app;
|
|
}
|
|
});
|
|
|
|
var Popup = this.Popup = Backbone.View.extend({
|
|
|
|
// If type is 'modal', background will turn gray and popup won't be
|
|
// dismissible except by interacting with it.
|
|
// If type is 'semimodal', background will turn gray, but clicking
|
|
// the background will dismiss it.
|
|
// Otherwise, background won't change, and interacting with anything
|
|
// other than the popup will still be possible (and will dismiss
|
|
// the popup).
|
|
type: 'normal',
|
|
|
|
className: 'ps-popup',
|
|
constructor: function(data) {
|
|
if (!this.events) this.events = {};
|
|
if (!this.events['click button']) this.events['click button'] = 'dispatchClickButton';
|
|
if (!this.events['submit form']) this.events['submit form'] = 'dispatchSubmit';
|
|
if (data && data.sourceEl) {
|
|
this.sourceEl = data.sourceEl = $(data.sourceEl);
|
|
}
|
|
|
|
Backbone.View.apply(this, arguments);
|
|
|
|
// if we have no source, we can't attach to anything
|
|
if (this.type === 'normal' && !this.sourceEl) this.type = 'semimodal';
|
|
|
|
if (this.type === 'normal') {
|
|
// nonmodal popup: should be positioned near source element
|
|
var $el = this.$el;
|
|
var $measurer = $('<div style="position:relative;height:0;overflow:hidden"></div>').appendTo('body').append($el);
|
|
$el.css('width', this.width - 22);
|
|
|
|
var offset = this.sourceEl.offset();
|
|
|
|
var room = $(window).height();
|
|
var height = $el.outerHeight();
|
|
var sourceHeight = this.sourceEl.outerHeight();
|
|
if (room > offset.top + sourceHeight + height + 5 &&
|
|
(offset.top + sourceHeight < room * 2/3 || offset.top + sourceHeight + 200 < room)) {
|
|
$el.css('top', offset.top + sourceHeight);
|
|
} else if (height + 5 <= offset.top) {
|
|
$el.css('bottom', room - offset.top);
|
|
} else if (height + 10 < room) {
|
|
$el.css('bottom', 5);
|
|
} else {
|
|
$el.css('top', 0);
|
|
}
|
|
|
|
room = $(window).width() - offset.left;
|
|
var outerWidth = $el.outerWidth();
|
|
if (room < outerWidth + 10) {
|
|
$el.css('right', 10);
|
|
} else {
|
|
$el.css('left', offset.left);
|
|
}
|
|
$el.detach();
|
|
$measurer.remove();
|
|
}
|
|
},
|
|
initialize: function(data) {
|
|
this.type = 'semimodal';
|
|
this.$el.html('<p style="white-space:pre-wrap">'+Tools.escapeHTML(data.message)+'</p><p class="buttonbar"><button name="close" autofocus><strong>OK</strong></button></p>').css('max-width', 480);
|
|
},
|
|
|
|
dispatchClickButton: function(e) {
|
|
var target = e.currentTarget;
|
|
if (target.name) {
|
|
app.dispatchingButton = target;
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
this[target.name].call(this, target.value, target);
|
|
delete app.dispatchingButton;
|
|
}
|
|
},
|
|
dispatchSubmit: function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var dataArray = $(e.currentTarget).serializeArray();
|
|
var data = {};
|
|
for (var i=0, len=dataArray.length; i<len; i++) {
|
|
var name = dataArray[i].name, value = dataArray[i].value;
|
|
if (data[name]) {
|
|
if (!data[name].push) data[name] = [data[name]];
|
|
data[name].push(value||'');
|
|
} else {
|
|
data[name] = (value||'');
|
|
}
|
|
}
|
|
this.submit(data);
|
|
},
|
|
|
|
remove: function() {
|
|
var $parent = this.$el.parent();
|
|
Backbone.View.prototype.remove.apply(this, arguments);
|
|
if ($parent.hasClass('ps-overlay')) $parent.remove();
|
|
},
|
|
|
|
close: function() {
|
|
app.closePopup();
|
|
}
|
|
});
|
|
|
|
var UserPopup = this.UserPopup = Popup.extend({
|
|
initialize: function(data) {
|
|
data.userid = toId(data.name);
|
|
var name = data.name;
|
|
if (/[a-zA-Z0-9]/.test(name.charAt(0))) name = ' '+name;
|
|
this.data = data = _.extend(data, UserPopup.dataCache[data.userid]);
|
|
data.name = name;
|
|
app.on('response:userdetails', this.update, this);
|
|
app.send('/cmd userdetails '+data.userid);
|
|
this.update();
|
|
},
|
|
events: {
|
|
'click .ilink': 'clickLink',
|
|
'click .yours': 'avatars'
|
|
},
|
|
update: function(data) {
|
|
if (data && data.userid === this.data.userid) {
|
|
data = _.extend(this.data, data);
|
|
UserPopup.dataCache[data.userid] = data;
|
|
} else {
|
|
data = this.data;
|
|
}
|
|
var userid = data.userid;
|
|
var name = data.name;
|
|
var avatar = data.avatar || '';
|
|
var groupDetails = {
|
|
'~': "Administrator (~)",
|
|
'&': "Leader (&)",
|
|
'@': "Moderator (@)",
|
|
'%': "Driver (%)",
|
|
'+': "Voiced (+)",
|
|
'!': "<span style='color:#777777'>Muted (!)</span>"
|
|
};
|
|
var group = (groupDetails[name.substr(0, 1)] || '');
|
|
if (group || name.charAt(0) === ' ') name = name.substr(1);
|
|
|
|
var buf = '<div class="userdetails">';
|
|
if (avatar) buf += '<img class="trainersprite'+(userid===app.user.get('userid')?' yours':'')+'" src="'+Tools.resolveAvatar(avatar)+'" />';
|
|
buf += '<strong>' + Tools.escapeHTML(name) + '</strong><br />';
|
|
buf += '<small>' + (group || ' ') + '</small>';
|
|
if (data.rooms) {
|
|
var battlebuf = '';
|
|
var chatbuf = '';
|
|
for (var i in data.rooms) {
|
|
if (i === 'global') continue;
|
|
var roomid = toRoomid(i);
|
|
if (roomid.substr(0,7) === 'battle-') {
|
|
if (!battlebuf) battlebuf = '<br /><em>Battles:</em> ';
|
|
else battlebuf += ', ';
|
|
battlebuf += '<a href="'+app.root+roomid+'" class="ilink">'+roomid.substr(7)+'</a>';
|
|
} else {
|
|
if (!chatbuf) chatbuf = '<br /><em>Chatrooms:</em> ';
|
|
else chatbuf += ', ';
|
|
chatbuf += '<a href="'+app.root+roomid+'" class="ilink">'+roomid+'</a>';
|
|
}
|
|
}
|
|
buf += '<small class="rooms">'+battlebuf+chatbuf+'</small>';
|
|
} else if (data.rooms === false) {
|
|
buf += '<strong class="offline">OFFLINE</strong>';
|
|
}
|
|
buf += '</div>';
|
|
|
|
if (userid === app.user.get('userid') || !app.user.get('named')) {
|
|
buf += '<p class="buttonbar"><button disabled>Challenge</button> <button disabled>PM</button> <button name="close">Close</button></p>';
|
|
} else {
|
|
buf += '<p class="buttonbar"><button name="challenge">Challenge</button> <button name="pm">PM</button> <button name="close">Close</button></p>';
|
|
}
|
|
|
|
this.$el.html(buf);
|
|
},
|
|
clickLink: function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.close();
|
|
var roomid = $(e.currentTarget).attr('href').substr(app.root.length);
|
|
app.tryJoinRoom(roomid);
|
|
},
|
|
avatars: function() {
|
|
app.addPopup('user:avatars', AvatarsPopup);
|
|
},
|
|
challenge: function() {
|
|
app.rooms[''].requestNotifications();
|
|
this.close();
|
|
app.focusRoom('');
|
|
app.rooms[''].challenge(this.data.name);
|
|
},
|
|
pm: function() {
|
|
app.rooms[''].requestNotifications();
|
|
this.close();
|
|
app.focusRoom('');
|
|
app.rooms[''].focusPM(this.data.name);
|
|
}
|
|
},{
|
|
dataCache: {}
|
|
});
|
|
|
|
var LoginPopup = this.LoginPopup = Popup.extend({
|
|
type: 'semimodal',
|
|
initialize: function(data) {
|
|
var buf = '<form>';
|
|
|
|
if (data.error) {
|
|
buf += '<p class="error">' + Tools.escapeHTML(data.error) + '</p>';
|
|
if (data.error.indexOf(' forced you to change ') >= 0) {
|
|
buf += '<p>Keep in mind these rules:</p>';
|
|
buf += '<ol>';
|
|
buf += '<li>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).</li>';
|
|
buf += '<li>Usernames may not reference sexual activity, directly or indirectly.</li>';
|
|
buf += '<li>Usernames may not impersonate a recognized user (a user with %, @, &, or ~ next to their name).</li>';
|
|
buf += '</ol>';
|
|
}
|
|
} else if (data.reason) {
|
|
buf += '<p>' + Tools.escapeHTML(data.reason) + '</p>';
|
|
}
|
|
|
|
var name = (data.name || '');
|
|
if (!name && app.user.get('named')) name = app.user.get('name');
|
|
buf += '<p><label class="label">Username: <input class="textbox" type="text" name="username" value="'+Tools.escapeHTML(name)+'" autofocus></label></p>';
|
|
buf += '<p class="buttonbar"><button type="submit"><strong>Choose name</strong></button> <button name="close">Cancel</button></p>';
|
|
|
|
buf += '</form>';
|
|
this.$el.html(buf);
|
|
},
|
|
submit: function(data) {
|
|
app.user.rename(data.username);
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
var LoginPasswordPopup = this.LoginPasswordPopup = Popup.extend({
|
|
type: 'semimodal',
|
|
initialize: function(data) {
|
|
var buf = '<form>';
|
|
|
|
if (data.error) {
|
|
buf += '<p class="error">' + Tools.escapeHTML(data.error) + '</p>';
|
|
if (data.error.indexOf(' forced you to change ') >= 0) {
|
|
buf += '<p>Keep in mind these rules:</p>';
|
|
buf += '<ol>';
|
|
buf += '<li>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).</li>';
|
|
buf += '<li>Usernames may not reference sexual activity, directly or indirectly.</li>';
|
|
buf += '<li>Usernames may not impersonate a recognized user (a user with %, @, &, or ~ next to their name).</li>';
|
|
buf += '</ol>';
|
|
}
|
|
} else if (data.reason) {
|
|
buf += '<p>' + Tools.escapeHTML(data.reason) + '</p>';
|
|
} else {
|
|
buf += '<p class="error">The name you chose is registered.</p>';
|
|
}
|
|
|
|
buf += '<p>Log in:</p>';
|
|
buf += '<p><label class="label">Username: </label><strong>'+Tools.escapeHTML(data.username)+'<input type="hidden" name="username" value="'+Tools.escapeHTML(data.username)+'" /></strong></p>';
|
|
buf += '<p><label class="label">Password: <input class="textbox" type="password" name="password" autofocus></label></p>';
|
|
buf += '<p class="buttonbar"><button type="submit"><strong>Log in</strong></button> <button name="close">Cancel</button></p>';
|
|
|
|
buf += '<p class="or">or</p>';
|
|
buf += '<p class="buttonbar"><button name="login">Choose another name</button></p>';
|
|
|
|
buf += '</form>';
|
|
this.$el.html(buf);
|
|
},
|
|
login: function() {
|
|
this.close();
|
|
app.addPopup('login', LoginPopup);
|
|
},
|
|
submit: function(data) {
|
|
this.close();
|
|
app.user.passwordRename(data.username, data.password);
|
|
}
|
|
});
|
|
|
|
var SoundsPopup = this.SoundsPopup = Popup.extend({
|
|
initialize: function(data) {
|
|
var buf = '';
|
|
var muted = !!Tools.prefs('mute');
|
|
buf += '<p class="effect-volume"><label class="optlabel">Effect volume:</label>'+(muted?'<em>(muted)</em>':'<input type="slider" name="effectvolume" value="'+(Tools.prefs('effectvolume')||50)+'" />')+'</p>';
|
|
buf += '<p class="music-volume"><label class="optlabel">Music volume:</label>'+(muted?'<em>(muted)</em>':'<input type="slider" name="musicvolume" value="'+(Tools.prefs('musicvolume')||50)+'" />')+'</p>'
|
|
buf += '<p><label class="optlabel"><input type="checkbox" name="muted"'+(muted?' checked':'')+' /> Mute sounds</label></p>';
|
|
this.$el.html(buf).css('min-width', 160);
|
|
},
|
|
events: {
|
|
'change input[name=muted]': 'setMute'
|
|
},
|
|
domInitialize: function() {
|
|
var self = this;
|
|
this.$('.effect-volume input').slider({
|
|
from: 0,
|
|
to: 100,
|
|
step: 1,
|
|
dimension: '%',
|
|
skin: 'round_plastic',
|
|
onstatechange: function(val) {
|
|
self.setEffectVolume(val);
|
|
}
|
|
});
|
|
this.$('.music-volume input').slider({
|
|
from: 0,
|
|
to: 100,
|
|
step: 1,
|
|
dimension: '%',
|
|
skin: 'round_plastic',
|
|
onstatechange: function(val) {
|
|
self.setMusicVolume(val);
|
|
}
|
|
});
|
|
},
|
|
setMute: function(e) {
|
|
var muted = !!e.currentTarget.checked;
|
|
Tools.prefs('mute', muted);
|
|
BattleSound.setMute(muted);
|
|
|
|
if (!muted) {
|
|
this.$('.effect-volume').html('<label class="optlabel">Effect volume:</label><input type="slider" name="effectvolume" value="'+(Tools.prefs('effectvolume')||50)+'" />');
|
|
this.$('.music-volume').html('<label class="optlabel">Music volume:</label><input type="slider" name="musicvolume" value="'+(Tools.prefs('musicvolume')||50)+'" />');
|
|
this.domInitialize();
|
|
} else {
|
|
this.$('.effect-volume').html('<label class="optlabel">Effect volume:</label><em>(muted)</em>');
|
|
this.$('.music-volume').html('<label class="optlabel">Music volume:</label><em>(muted)</em>');
|
|
}
|
|
|
|
app.topbar.$('button[name=openSounds]').html('<i class="'+(muted?'icon-volume-off':'icon-volume-up')+'"></i>');
|
|
},
|
|
setEffectVolume: function(volume) {
|
|
BattleSound.setEffectVolume(volume);
|
|
Tools.prefs('effectvolume', volume);
|
|
},
|
|
setMusicVolume: function(volume) {
|
|
BattleSound.setBgmVolume(volume);
|
|
Tools.prefs('musicvolume', volume);
|
|
}
|
|
});
|
|
|
|
var OptionsPopup = this.OptionsPopup = Popup.extend({
|
|
initialize: function(data) {
|
|
app.user.on('change', this.update, this);
|
|
app.send('/cmd userdetails '+app.user.get('userid'));
|
|
this.update();
|
|
},
|
|
events: {
|
|
'change input[name=noanim]': 'setNoanim',
|
|
'change input[name=ignorespects]': 'setIgnoreSpects',
|
|
'change select[name=timestamps-lobby]': 'setTimestampsLobby',
|
|
'change select[name=timestamps-pms]': 'setTimestampsPMs',
|
|
'click img': 'avatars'
|
|
},
|
|
update: function() {
|
|
var name = app.user.get('name');
|
|
var avatar = app.user.get('avatar');
|
|
|
|
var buf = '';
|
|
buf += '<p>'+(avatar?'<img class="trainersprite" src="'+Tools.resolveAvatar(avatar)+'" width="40" height="40" style="vertical-align:middle" />':'')+'<strong>'+Tools.escapeHTML(name)+'</strong></p>';
|
|
buf += '<p><button name="avatars">Change avatar</button></p>';
|
|
|
|
buf += '<hr />';
|
|
buf += '<p><label class="optlabel"><input type="checkbox" name="noanim"'+(Tools.prefs('noanim')?' checked':'')+' /> Disable animations</label></p>';
|
|
|
|
var timestamps = this.timestamps = (Tools.prefs('timestamps') || {});
|
|
buf += '<p><label class="optlabel">Timestamps in lobby chat: <select name="timestamps-lobby"><option value="off">Off</option><option value="minutes"'+(timestamps.lobby==='minutes'?' selected="selected"':'')+'>[HH:MM]</option><option value="seconds"'+(timestamps.lobby==='seconds'?' selected="selected"':'')+'>[HH:MM:SS]</option></select></label></p>';
|
|
buf += '<p><label class="optlabel">Timestamps in PM\'s: <select name="timestamps-pms"><option value="off">Off</option><option value="minutes"'+(timestamps.pms==='minutes'?' selected="selected"':'')+'>[HH:MM]</option><option value="seconds"'+(timestamps.pms==='seconds'?' selected="selected"':'')+'>[HH:MM:SS]</option></select></label></p>';
|
|
|
|
if (app.curRoom.battle) {
|
|
buf += '<hr />';
|
|
buf += '<h3>Current room</h3>';
|
|
buf += '<p><label class="optlabel"><input type="checkbox" name="ignorespects"'+(app.curRoom.battle.ignoreSpects?' checked':'')+'> Ignore spectators</label></p>';
|
|
}
|
|
|
|
buf += '<hr />';
|
|
buf += '<p class="buttonbar" style="text-align:right"><button name="logout"><strong>Log out</strong></button></p>';
|
|
this.$el.html(buf).css('min-width', 160);
|
|
},
|
|
setNoanim: function(e) {
|
|
var noanim = !!e.currentTarget.checked;
|
|
Tools.prefs('noanim', noanim);
|
|
},
|
|
setIgnoreSpects: function(e) {
|
|
if (app.curRoom.battle) {
|
|
app.curRoom.battle.ignoreSpects = !!e.currentTarget.checked;
|
|
}
|
|
},
|
|
setTimestampsLobby: function(e) {
|
|
this.timestamps.lobby = e.currentTarget.value;
|
|
},
|
|
setTimestampsPMs: function(e) {
|
|
this.timestamps.pms = e.currentTarget.value;
|
|
},
|
|
avatars: function() {
|
|
app.addPopup('options:avatars', AvatarsPopup);
|
|
},
|
|
logout: function() {
|
|
app.user.logout();
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
var AvatarsPopup = this.AvatarsPopup = Popup.extend({
|
|
type: 'semimodal',
|
|
initialize: function() {
|
|
var cur = +app.user.get('avatar');
|
|
var buf = '';
|
|
buf += '<p>Choose an avatar or <button name="close">Cancel</button></p>';
|
|
|
|
buf += '<div class="avatarlist">';
|
|
for (var i=1; i<=293; i++) {
|
|
var offset = '-'+(((i-1)%16)*80)+'px -'+(Math.floor((i-1)/16)*80)+'px'
|
|
buf += '<button name="setAvatar" value="'+i+'" style="background-position:'+offset+'"'+(i===cur?' class="cur"':'')+'></button>';
|
|
}
|
|
buf += '</div><div style="clear:left"></div>';
|
|
|
|
buf += '<p><button name="close">Cancel</button></p>';
|
|
this.$el.html(buf).css('max-width', 780);
|
|
},
|
|
setAvatar: function(i) {
|
|
app.send('/avatar '+i);
|
|
app.send('/cmd userdetails '+app.user.get('userid'));
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
}).call(this, jQuery);
|