diff --git a/js/client-tournament.js b/js/client-tournament.js
new file mode 100644
index 000000000..2b308b413
--- /dev/null
+++ b/js/client-tournament.js
@@ -0,0 +1,271 @@
+(function($) {
+
+ function arrayToPhrase(array, finalSeparator) {
+ if (array.length <= 1)
+ return array.join();
+ finalSeparator = finalSeparator || "and";
+ return array.slice(0, -1).join(", ") + " " + finalSeparator + " " + array.slice(-1)[0];
+ }
+
+ var TournamentBox = this.TournamentBox = (function () {
+ function TournamentBox(room, $wrapper) {
+ this.room = room;
+ this.$wrapper = $wrapper;
+
+ $wrapper.html(
+ '
Tournament
' +
+ '');
+
+ this.$title = $wrapper.find('.tournament-title');
+ this.$format = $wrapper.find('.tournament-format');
+ this.$generator = $wrapper.find('.tournament-generator');
+ this.$box = $wrapper.find('.tournament-box');
+ this.$bracket = $wrapper.find('.tournament-bracket');
+ this.$tools = $wrapper.find('.tournament-tools');
+ this.$join = $wrapper.find('.tournament-join');
+ this.$leave = $wrapper.find('.tournament-leave');
+ this.$noMatches = $wrapper.find('.tournament-nomatches');
+ this.$challenge = $wrapper.find('.tournament-challenge');
+ this.$challengeUser = $wrapper.find('.tournament-challenge-user');
+ this.$challengeTeam = $wrapper.find('.tournament-challenge-team');
+ this.$challengeChallenge = $wrapper.find('.tournament-challenge-challenge');
+ this.$challengeBy = $wrapper.find('.tournament-challengeby');
+ this.$challenging = $wrapper.find('.tournament-challenging');
+ this.$challenged = $wrapper.find('.tournament-challenged');
+ this.$challengedMessage = $wrapper.find('.tournament-challenged-message');
+ this.$challengeAccept = $wrapper.find('.tournament-challenge-accept');
+
+ this.isActive = false;
+ this.info = null;
+ this.bracketData = null;
+ this.challenges = null;
+ this.challengeBys = null;
+
+ var self = this;
+ this.$title.on('click', function() {
+ self.$box.toggleClass('active');
+ self.$box.css('transition', '');
+ self.$box.css('max-height', self.$box.hasClass('active') ? self.$box[0].scrollHeight: '');
+ });
+ this.$box.on('transitionend webkitTransitionEnd oTransitionEnd otransitionend', function() {
+ if (self.$box.hasClass('active'))
+ self.$box.css('transition', 'none');
+ if (!self.isActive)
+ self.$wrapper.removeClass('active');
+ });
+ this.$join.on('click', function() {
+ self.room.send('/tournament join');
+ });
+ this.$leave.on('click', function() {
+ self.room.send('/tournament leave');
+ });
+ this.$challengeChallenge.on('click', function() {
+ var team = app.user.teams[self.$challengeTeam.children().val()];
+ self.room.send('/utm ' + JSON.stringify(team ? team.team : null));
+ self.room.send('/tournament challenge ' + self.$challengeUser.children().val());
+ });
+ this.$challengeAccept.on('click', function() {
+ var team = app.user.teams[self.$challengeTeam.children().val()];
+ self.room.send('/utm ' + JSON.stringify(team ? team.team : null));
+ self.room.send('/tournament acceptchallenge');
+ });
+ }
+
+ TournamentBox.prototype.parseMessage = function (data, isBroadcast) {
+ if (isBroadcast) {
+ // TODO
+ return true;
+ } else {
+ if (!this.isActive) {
+ this.$wrapper.addClass("active");
+ this.$box.addClass("active");
+ this.isActive = true;
+ }
+
+ var cmd = data.shift().toLowerCase();
+ switch (cmd) {
+ case 'create':
+ this.room.$chat.append("A " + BattleFormats[data.shift()].name + " " + data.join('|') + " Tournament has been created.
");
+ break;
+
+ case 'update':
+ this.$tools.find('.active').removeClass('active');
+ if (this.info && this.info.isStarted)
+ this.$noMatches.addClass('active');
+ this.challenges = null;
+ this.challengeBys = null;
+ break;
+
+ case 'info':
+ this.info = JSON.parse(data.join('|'));
+ this.$format.text(BattleFormats[this.info.format].name);
+ this.$generator.text(this.info.generator);
+ break;
+
+ case 'bracketdata':
+ this.bracketData = JSON.parse(data.join('|'));
+ this.showBracket(this.bracketData);
+ break;
+
+ case 'challenges':
+ this.$noMatches.removeClass('active');
+ this.challenges = data.join('|').split(',');
+ this.$challengeUser.html(this.renderChallengeUsers());
+ this.$challengeTeam.html(app.rooms[''].renderTeams(this.info.format));
+ this.$challengeTeam.children().data('type', 'challengeTeam');
+ this.$challengeTeam.children().attr('name', 'tournamentButton');
+ this.$challenge.addClass("active");
+ break;
+
+ case 'challengebys':
+ this.$noMatches.removeClass('active');
+ this.challengeBys = data.join('|').split(',');
+ this.$challengeBy.text((this.challenges ? "Or" : "Please") + " wait for " + arrayToPhrase(this.challengeBys, "or") + " to challenge you.");
+ this.$challengeBy.addClass("active");
+ break;
+
+ case 'challenging':
+ this.$noMatches.removeClass('active');
+ this.$challenging.text("Challenging " + data[0] + "...").addClass("active");
+ break;
+
+ case 'challenged':
+ this.$noMatches.removeClass('active');
+ this.$challengedMessage.text(data[0] + " has challenged you.");
+ this.$challengeTeam.html(app.rooms[''].renderTeams(this.info.format));
+ this.$challengeTeam.children().data('type', 'challengeTeam');
+ this.$challengeTeam.children().attr('name', 'tournamentButton');
+ this.$challenged.addClass("active");
+ break;
+
+ case 'end':
+ this.room.$chat.append("Congratulations to " + Tools.escapeHTML(arrayToPhrase(data[0].split(','))) + " for winning the tournament!
");
+ if (data[1])
+ this.room.$chat.append("Runner-up" + (data[1].length > 1 ? "s" : "") +": " + Tools.escapeHTML(arrayToPhrase(data[1].split(','))) + "
");
+ // Fallthrough
+
+ case 'forceend':
+ this.isActive = false;
+ this.info = null;
+ this.bracketData = null;
+
+ // A quick start and end will not fire the transitionend event, so we delay a bit so the transition happens
+ var self = this;
+ setTimeout(function () {
+ if (self.isActive)
+ return;
+ self.$box.removeClass("active");
+ self.$box.css('max-height', '');
+ self.$box.css('transition', '');
+ }, 0);
+
+ if (cmd === 'forceend')
+ this.room.$chat.append("The tournament was forcibly ended.
");
+ break;
+
+ default:
+ return true;
+ }
+
+ this.$box.css('max-height', this.$box.hasClass('active') ? this.$box[0].scrollHeight : '');
+ }
+ };
+
+ TournamentBox.prototype.showBracket = function (data) {
+ var isJoined = false;
+ if (data.type === 'tree') {
+ // TODO
+ } else if (data.type === 'table') {
+ this.$bracket.empty();
+ var $table = $('');
+
+ var $colHeaders = $(' ');
+ $table.append($colHeaders);
+ data.tableHeaders.cols.forEach(function (name) {
+ $colHeaders.append($(' ').text(name));
+ if (app.user.attributes.name === name)
+ isJoined = true;
+ });
+
+ data.tableHeaders.rows.forEach(function (name, r) {
+ var $row = $(' ');
+ $table.append($row);
+ $row.append($(' ').text(name));
+ data.tableContents[r].forEach(function (cell) {
+ var $cell = $(' ');
+ $row.append($cell);
+ if (!cell) {
+ $cell.addClass('tournament-bracket-table-cell-null');
+ return;
+ }
+ $cell.addClass('tournament-bracket-table-cell-' + cell.state);
+ if (cell.state === 'unavailable') {
+ $cell.text("Unavailable");
+ } else if (cell.state === 'available') {
+ $cell.text("Waiting");
+ } else if (cell.state === "inprogress") {
+ $cell.html('In-progress ');
+ } else if (cell.state === 'finished') {
+ $cell.addClass('tournament-bracket-table-cell-result-' + cell.result);
+ $cell.text(cell.score.join(" - "));
+ }
+ });
+ $row.append($(' ').text(data.scores[r]));
+ });
+
+ if (data.tableContents.length > 0)
+ this.$bracket.append($table);
+ }
+
+ if (!this.info.isStarted)
+ if (isJoined)
+ this.$leave.addClass('active');
+ else
+ this.$join.addClass('active');
+ },
+
+ TournamentBox.prototype.renderChallengeUsers = function () {
+ return '' + Tools.escapeHTML(this.challenges[0]) + ' ';
+ };
+ TournamentBox.prototype.challengeUser = function (user, button) {
+ app.addPopup(UserPopup, {user: user, users: this.challenges, sourceEl: button});
+ };
+
+ TournamentBox.prototype.challengeTeam = function (team, button) {
+ app.addPopup(TeamPopup, {team: team, format: this.info.format, sourceEl: button});
+ };
+
+ return TournamentBox;
+ })();
+
+ var UserPopup = this.Popup.extend({
+ initialize: function (data) {
+ this.$el.html('');
+ },
+ selectUser: function (user) {
+ this.sourceEl.val(toId(user)).text(user);
+ this.close();
+ }
+ });
+
+}).call(this, jQuery);