////////////////////////////////////////////////////
// Game state machine.
var State = new function() { this.name = null; this.current = null; this.module = null; };

State.go = function(name, context) {
  this.name = name;
  
  // Get the next module and state.
  var next_module = null;
  var next_state = null;
  
  var dot = name.indexOf('.');
  if (dot >= 0) {
    next_module = this[name.substring(0, dot)];
    next_state = next_module[name.substring(dot + 1)];
  } else {
    next_state = this[name];
  }
  
  // If state doesn't change, do nothing.
  if (this.current == next_state)
    return;
  
  // Exit current state.
  if (this.current && this.current.leave) 
    this.current.leave(context);

  // Switch module if necessary.  
  if (this.module != next_module) {
    if (this.module && this.module.leave) 
      this.module.leave(context);
    
    this.module = next_module;
  
    if (this.module && this.module.enter)
      this.module.enter(context);  
  }
  
  // Enter a new state.  
  this.current = next_state;
  if (this.current && this.current.enter)
    this.current.enter(context);
};

////////////////////////////////////////////////////
// Launcher.
State.launch = {
  enter: function(context) {
    Widgets.Transitions.hide($("#id_login"));
    Widgets.Transitions.hide($("#id_register"));
    Widgets.Transitions.hide($("#id_reset_password"));
    Widgets.Transitions.hide($("#id_welcome"));
    Widgets.Transitions.hide($("#id_my_account"));
    Widgets.Transitions.hide($("#id_help"));
    Widgets.Transitions.hide($("#id_rankings"));
    Widgets.Transitions.hide($("#id_support"));
    Widgets.Transitions.hide($("#id_channel"));
    Widgets.Transitions.hide($("#id_channel_ko"));
    
    // Show first screen.
    if (startPoint == 'welcome') {
      Widgets.Transitions.fadeIn($("#frame_game"), function() {
        State.go("welcome.launch_logged_on");
      });
    } else {
      Widgets.Transitions.fadeIn($("#frame_lobby"), function() {
        if (context && context.skipToRegistration)
          State.go("register");
        else
          State.go("intro");
      });
    }
    
    // Clear out start point after the first initialization screen.
    startPoint = '';
  }  
};

////////////////////////////////////////////////////
// Logon lobby.
State.intro = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_login"));
  },
  leave: function() {
    $("#id_login").clearForm();    
    Widgets.Transitions.fadeOut($("#id_login"));
  },  
  login: function() {
    // Login with player's credentials.
    Widgets.MessageBox.loading("Logging in...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_login").coreSerialize("action_play"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          State.go("welcome.launch", { name: response.name, email: response.email, guest: false });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  },
  loginAsGuest: function(context) {
    // Login user as guest.
    Widgets.MessageBox.loading("Logging in...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: { source: "action_play_as_guest" },
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else if (!response.guest) {
          Widgets.MessageBox.error("Please login as member or guest.");
        } else {
          State.go("welcome.launch", { name: response.name, email: response.email, guest: true });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  }
};

////////////////////////////////////////////////////
// Reset password.
State.forgot = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_reset_password"));    
  },
  leave: function() {
    $("#id_reset_password").clearForm();
    Widgets.Transitions.fadeOut($("#id_reset_password"));
  },
  resetPassword: function() {
    // Resets user password.
    Widgets.MessageBox.loading("Sending you a new password...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_reset_password").coreSerialize("action_reset_password"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          Widgets.MessageBox.notice('Please check your email for your new password.', function() {
            State.go("intro");
          });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  } 
};

////////////////////////////////////////////////////
// Register new user.
State.register = {
  enter: function() {    
    Widgets.Transitions.fadeIn($("#id_register"));
  },
  leave: function() {
    $("#id_register").clearForm();
    Widgets.Transitions.fadeOut($("#id_register"));  
  },
  register: function() {
    // Registers a new user.
    Widgets.MessageBox.loading("Creating player record...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_register").coreSerialize("action_register"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          State.go("welcome.launch", { name: response.name, email: response.email, guest: true });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  }
}

////////////////////////////////////////////////////
// Matchmaking module.

State.welcome = { _statusTimer: 0, _headCount: {},
  enter: function() {
    Widgets.Transitions.show($("#id_welcome_loading"));
    Widgets.Transitions.hide($("#id_welcome_server_status"));
    
    this.retrieveServerStatus();
  },
  leave: function() {
    if (this._statusTimer)
      clearTimeout(this._statusTimer);
  },
  reloadServerStatus: function() {
    var w = this;
    
    w._statusTimer = setTimeout(function() {      
      w.retrieveServerStatus();
    }, 8000);
  },
  retrieveServerStatus: function() {
    var w = this;
    
    $.ajax({type: "POST", url: Utility.formatUrl("/frontend/welcome/"), data: "",
      success: function(json) {
        var response = JSON.parse(json);
        if (!response.errors) {
          Widgets.Transitions.hide($("#id_welcome_loading"));
          Widgets.Transitions.show($("#id_welcome_server_status"));
          
          // Populate user status.
          if (response.user != null && response.online != null) {
            $("#id_welcome_server_status > .user").html("Welcome, " + Utility.formatName($.encode(response.user), true) + "!");
            $("#id_welcome_server_status > .online").html($.encode(response.online) + " meditating");
          }
          
          // Populate channel status.
          if (!!response.channels) {
            for (channelIndex in response.channels) {
              var channel = response.channels[channelIndex];
              
              w._headCount[channel.id] = { current: channel.c, limit: channel.l };
              
              Widgets.FormBuilder.setChannelProgressBar($("#id_channel_" + channel.id),
                "Channel {0} ({1}/{2})".format(channel.id + 1, channel.c, channel.l), channel.c, channel.l);
            }            
          }
          
          w.reloadServerStatus();
        }
      },
      error: function() {
        w.reloadServerStatus();
      }}
    );
  },
  // Checks whether channel has free slots for a person to join.
  hasFreeSlots: function(channel) {
    var c = this._headCount[channel];
    
    if (c == null)
      return false;
    
    return c.current < c.limit;
  }
};

State.welcome.launch = {
  
  enter: function(context) {
    Widgets.MessageBox.hide();
    
    if (User == null)
      User = {};
      
    if (context) {
      User.name = context.name;
      User.guest = context.guest;
      User.email = context.email;
    }
    
    if (User && User.guest) {
      Widgets.Transitions.showButton($("#action_become_member"));
      Widgets.Transitions.hideButton($("#action_my_account"));      
    } else {
      Widgets.Transitions.hideButton($("#action_become_member"));
      Widgets.Transitions.showButton($("#action_my_account"));      
    }
    
    Widgets.Transitions.fadeOut($("#frame_lobby"), function() {
      Widgets.Transitions.fadeIn($("#frame_game"), function() {
        State.go("welcome.welcome");
      });
    });
  },
  leave: function() {}
};

State.welcome.launch_logged_on = {
  enter: function() {
    if (User && User.guest) {
      Widgets.Transitions.showButton($("#action_become_member"));
      Widgets.Transitions.hideButton($("#action_my_account"));      
    } else {
      Widgets.Transitions.hideButton($("#action_become_member"));
      Widgets.Transitions.showButton($("#action_my_account"));      
    }
    
    State.go("welcome.welcome");
  },
  leave: function() {}
};

////////////////////////////////////////////////////
// Welcome screen.
State.welcome.welcome = { _parent: State.welcome,
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_welcome"));
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_welcome"));    
  },
  logoff: function() {
    // Logs off.
    Widgets.MessageBox.loading("Bye...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_welcome_logoff").coreSerialize("action_logoff"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          Widgets.MessageBox.hide();          
          Widgets.Transitions.fadeOut($("#frame_game"));
          Widgets.Transitions.fadeIn($("#frame_lobby"), function() {
            State.go("launch");
          });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  },
  becomeMember: function() {
    Widgets.MessageBox.loading("Transferring...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_welcome_logoff").coreSerialize("action_logoff"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          Widgets.MessageBox.hide();          
          Widgets.Transitions.fadeOut($("#frame_game"));
          Widgets.Transitions.fadeIn($("#frame_lobby"), function() {
            State.go("launch", { skipToRegistration: true });
          });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );    
  },
  join: function(channel) {
    // Join a channel.
    if (this._parent.hasFreeSlots(channel))
      State.go("channel.launch", {channel: channel});
    else
      Widgets.MessageBox.error("Sorry, this channel is already full. Please pick another one or try again later.");
  }
};

////////////////////////////////////////////////////
// My account.
State.welcome.my_account = {
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_my_account"));
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_my_account"));
  },
  save: function() {
    Widgets.MessageBox.loading("Saving changes...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_my_account").coreSerialize("action_my_account_save"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          Widgets.MessageBox.notice('Your changes have been saved.', function() {
            State.go("welcome.welcome");
          });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  }
};

////////////////////////////////////////////////////
// Help.

State.welcome.help = { _chapter: null,
  enter: function() {
    Widgets.Transitions.fadeIn($("#id_help"));
    
    this._chapter = "intro";
    
    Widgets.Transitions.show($("#id_help_intro"));
    Widgets.Transitions.hide($("#id_help_gems"));
    Widgets.Transitions.hide($("#id_help_spells"));
    Widgets.Transitions.hide($("#id_help_experience"));
    
    $("#action_help_rules").html("Next:&nbsp;Gems");
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_help"));
  },
  next: function() {
    switch (this._chapter) {
      case "intro": this._chapter = "gems"; break;
      case "gems": this._chapter = "spells"; break;
      case "spells": this._chapter = "experience"; break;
      case "experience": this._chapter = "intro"; break;
    }
    
    switch (this._chapter) {
      case "intro":
        Widgets.Transitions.fadeIn($("#id_help_intro"));
        Widgets.Transitions.fadeOut($("#id_help_experience"));
        
        $("#action_help_rules").html("Next:&nbsp;Gems");
        break;
      
      case "gems":
        Widgets.Transitions.fadeIn($("#id_help_gems"));
        Widgets.Transitions.fadeOut($("#id_help_intro"));
        
        $("#action_help_rules").html("Next:&nbsp;Spells");
        break;
      
      case "spells":
        Widgets.Transitions.fadeIn($("#id_help_spells"));
        Widgets.Transitions.fadeOut($("#id_help_gems"));
        
        $("#action_help_rules").html("Next:&nbsp;Experience");
        break;
      
      case "experience":
        Widgets.Transitions.fadeIn($("#id_help_experience"));
        Widgets.Transitions.fadeOut($("#id_help_spells"));
        
        $("#action_help_rules").html("Next:&nbsp;Intro");
        break;
    }
  }
};

////////////////////////////////////////////////////
// Rankings.

State.welcome.rankings = {
  enter: function() {
    if (User.guest) {
      Widgets.Transitions.show($("#id_rankings-label").html("Greetings, Guest! Please register to see and partipate in rankings, it's quick and easy!"));
    } else {
      Widgets.Transitions.show($("#id_rankings-label").html("Loading..."));
      
      $.ajax({type: "POST", url: Utility.formatUrl("/frontend/rankings/"), data: "",
        success: function(json) {
          var response = JSON.parse(json);
          if (response.errors) {          
            Widgets.MessageBox.error(response.errors);
          }
          else {            
            Widgets.Transitions.hide($("#id_rankings-label").html("Loading..."));
            
            if ($.isArray(response)) {
              var sb = new StringBuilder();
              
              var e = Widgets.Sprites.star_small;
              var s = Widgets.Sprites.divider;
              
              sb.append('<table width="100%" class="rankings">');
              
              // Check if there is a separator.
              var hasSeparator = false;
              for (var i = 0; i < response.length; ++i) 
                if (response[i].u == null) {
                  hasSeparator = true;
                  break;
                }
              
              var separatorAdded = false;
              
              for (var i = 0; i < response.length; ++i) {
                var line = response[i];                
                
                if (line.u == null) {
                  // Separator.
                  separatorAdded = true;
                  
                  var w = 130;
                  
                  sb.appendList('<tr><td width="275" class="separator" colspan="3">',
                    '<img src="/static/dot.gif" style="width: ', w, 'px; height: ', s.height, 'px; background: url(', Widgets.images.atlas, ') no-repeat ', s.style, ';" />',
                    '<img src="/static/dot.gif" style="width: ', w, 'px; height: ', s.height, 'px; background: url(', Widgets.images.atlas, ') no-repeat -', s.x + s.width - w, 'px -', s.y, 'px;"/></td></tr>');
                } else {
                  if (separatorAdded || !hasSeparator || (i < 10)) {                  
                    var lineClass = line.u == User.name ? 'ranking-self' : 'ranking-main';
                  
                    sb.appendList(                                
                      '<tr><td width="50" class="', lineClass, ' index">', $.encode(line.i + ""), '.</td><td width="125" class="', lineClass, ' name">', $.encode(Utility.formatName(line.u)),
                      '</td><td width="100" class="', lineClass, ' experience"><img src="/static/dot.gif" style="width:', e.width, 'px; height: ', e.height, 'px; background: url(', Widgets.images.atlas, ') no-repeat ', e.style, ';"/>&nbsp;',
                      $.encode(line.e + ""), '</td></tr>'                    
                    );
                  }
                }
              }
              
              sb.append('</table>');
              
              $("#id_rankings-names").html(sb.toString());
            }
          }
        },
        error: function() {
          Widgets.MessageBox.error("Server is busy. Please try again.");
        }}
      );
    }
    Widgets.Transitions.fadeIn($("#id_rankings"));
  },
  leave: function() {
    Widgets.Transitions.fadeOut($("#id_rankings"));
  }
};

////////////////////////////////////////////////////
// Support message.

State.welcome.support = {
  enter: function() {
    $("#id_support-name").val(Utility.formatName(User.name));
    $("#id_support-email").val(Utility.formatName(User.email));
    
    Widgets.Transitions.fadeIn($("#id_support"));
  },
  leave: function() {
    $("#id_support").clearForm();
    
    Widgets.Transitions.fadeOut($("#id_support"));
  },
  save: function() {
    Widgets.MessageBox.loading("Contacting support...");
    $.ajax({type: "POST", url: Utility.formatUrl("/"), data: $("#id_support").coreSerialize("action_support_save"),
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors) {          
          Widgets.MessageBox.error(response.errors);
        }
        else {
          Widgets.MessageBox.notice('Your request has been sent and we will get back to you shortly.', function() {
            State.go("welcome.help");
          });
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }}
    );
  }  
};

////////////////////////////////////////////////////
// Game channel.

// Initializes game channel data.
function ChannelData(channel, data) {
  this._channel = channel;
  this._version = data.version;
  this._width = data.width;
  this._height = data.height;  
  this._widthHalf = Math.floor(this._width / 2);
  this._heightHalf = Math.floor(this._height / 2);  
  this._rows = data.field;
  this._chatMessages = [];
  
  this._user_health = 0;
  this._user_protection = 0;
  this._user_experience = 0;
  
  this._blockMapping = [ "block_empty", "block_water", "block_earth", "block_air", "block_fire", "block_aether", "block_void" ];
  this._blockTrample = [ 0, 2, 3, 4, 1, 6, 5 ];
  
  this._scheduledCommands = [];
  this._reschedule = [];
  
  this._selected = { first: null, second: null };
  
  this._joined = false;
  
  this._challenge = {};
  
  this._announcementsObject = $("#id_challenge");
  this._announcements = [];
  this._announceStage = "fadeIn";
  this._announceTimer = 0;
  
  this._spellCooldown = new Date();
  
  this._round_time_left = 0;
  this._round_time_last = 0;
  this._round_time_updated = new Date();
  
  $("#id_chat_messages").html("");
  $("#id_top").html("");
  
  var eventsContainer = $("#id_channel_events");
  eventsContainer.html("");
  for (var i = 0; i < 5; ++i)
    eventsContainer.append(StringBuilder.toStringList('<div id="id_event_', i, '" class="event"><span id="id_inner_text">&nbsp;</span></div>'));
  
  this._balloons = [];
  
  for (var i = 0; i < 5; ++i)
    this._balloons.push($("#id_event_" + i).css("opacity", "0").css("display", "none"))
    
  Widgets.Transitions.hide($("#id_channel_ko"));
}

ChannelData.config = { block: 35, half: 17, fieldOffsetX: -7, fieldOffsetY: 40, pickerAllWidth: 129, pickerAllHeight: 94, pickerTrampleWidth: 59, pickerTrampleHeight: 59 };

ChannelData.prototype.widthHalf = function() { return this._widthHalf; };
ChannelData.prototype.heightHalf = function() { return this._heightHalf; };

// Shows event balloon.
ChannelData.prototype.showBalloon = function(styledText, x, y, time) {
  // Get first balloon, free or not.
  var balloon = this._balloons.shift();
  
  balloon.find("#id_inner_text").html(styledText);
  
  if (time == null)
    time = 2000;
  
  balloon.css("left", (x > 160 ? x - 60 : x) + "px").css("top", y + "px").css("opacity", "0").css("display", "block").animate({ opacity: 1 }, 300, function() {
    setTimeout(function() {
      balloon.animate({ opacity: 0 }, 1000, function() {
        balloon.css("display", "none");
      })
    }, time);
  });
  
  // Put balloon at the end of the deque.
  this._balloons.push(balloon);
};

// Gets the time last spell was cast.
ChannelData.prototype.spellCooldown = function() {
  return this._spellCooldown;
};

// Checks whether gems are ready to be placed on the board.
ChannelData.prototype.spellReady = function() {
  return (new Date() - this.spellCooldown()) > 3000;
};

// Gets channel.
ChannelData.prototype.channel = function() {
  return this._channel;
};

// Gets whether user himself joined the channel.
ChannelData.prototype.joined = function() {
  return this._joined;
};

// Maps element color into image block name.
ChannelData.prototype.map = function(c) {
  return this._blockMapping[c];
};

// Gets element color at coordinates.
ChannelData.prototype.at = function(x, y) {
  if (x>= 0 && x < this._width && y >= 0 && y < this._height)
    return this._rows[y][x];
  else
    return 0;
};

// Sets element color at coordinates.
ChannelData.prototype.setAt = function(c, x, y) {
  if (x>= 0 && x < this._width && y >= 0 && y < this._height) {
    this._rows[y][x] = c;
    return true;
  } else
    return false;
};

// Gets element color that tramples given one.
ChannelData.prototype.trample = function(c) {
  return this._blockTrample[c];
};

// Updates round timer.
ChannelData.prototype.setRoundTimeLeft = function(timeLeft) {
  this._round_time_last = -1;
  this._round_time_left = timeLeft - 1;
  if (this._round_time_left < 0)
    this._round_time_left = 0;
  this._round_time_updated = new Date();
  
  this.updateRoundTimer();
};

// Updates round timer.
ChannelData.prototype.updateRoundTimer = function() {
  var wholeSeconds = Math.floor((new Date() - this._round_time_updated) / 1000);
  
  var timeLeft = this._round_time_left - wholeSeconds;
  if (timeLeft < 0)
    timeLeft = 0;
    
  if (timeLeft != this._round_time_last) {
    var minutes = Math.floor(timeLeft / 60)
    var seconds = timeLeft - minutes * 60;
    
    minutes = (minutes < 10 ? "0" : "") + minutes
    seconds = (seconds < 10 ? "0" : "") + seconds
    
    $("#id_round_timer_value").html(minutes + ":" + seconds);
    
    this._round_time_last = timeLeft;
  }
};

// Updates channel data with information from server.
ChannelData.prototype.mergeUpdate = function(data, initial) {
  // Process only if update is newer than channel version.
  if ($.isArray(data)) {
    for (index in data) {
      var chunk = data[index];
      if (chunk == null)
        continue;
      
      if (chunk.t == "channel")
        this.mergeChannelUpdate(chunk);
      if (chunk.t == "lost")
        this.mergeLost(chunk);
      if (chunk.t == "reset")
        this.mergeReset(chunk);
      else if (chunk.t == "chat")
        this.mergeChat(chunk);
      else if (chunk.t == "join")
        this.mergeJoin(chunk);
      else if (chunk.t == "disappear")
        this.mergeDisappear(chunk);
      else if (chunk.t == "leave")
        this.mergeLeave(chunk);
      else if (chunk.t == "system" && !initial)
        this.mergeSystem(chunk);
      else if (chunk.t == "challenge")
        this.mergeChallenge(chunk);
      else if (chunk.t == "challengeexpired")
        this.mergeChallengeExpired(chunk);
      else if (chunk.t == "challengeend")
        this.mergeChallengeEnded(chunk);
    }
  }
};

// Merges in chat update.
ChannelData.prototype.mergeChat = function(chunk) {
  var user = chunk.u || "";
  var message = chunk.m || "";
  
  if (user.length > 0 && message.length > 0) {
    if (user == "__system__") {
      var typeIndex = message.indexOf(' ');
      if (typeIndex > 0) {
        var type = message.substring(0, typeIndex);
        message = message.substring(typeIndex + 1);
      }
    }
    else {
      this._chatMessages.push('<span class="chat-user">' + $.encode(Utility.formatName(user)) + ":</span>&nbsp;" + $.encode(message));
    }
  }
  
  this.updateChat();
};

// Merges lost connection event.
ChannelData.prototype.mergeLost = function(chunk) {
  if (this._version >= chunk.v)
    return;
  
  var user = chunk.u || "";
  
  if (user == User.name)  
    State.go("welcome.launch");
}

// Merges avatars reset message.
ChannelData.prototype.mergeReset = function(chunk) {
  if (this._version >= chunk.v)
    return;
  
  this._version = chunk.v;
  
  this._user_health = chunk.koh;
  this._user_protection = chunk.kop;
  
  this.setRoundTimeLeft(chunk.rt);
  
  this._chatMessages.push('<span class="chat-round">New Round Started.</span>');
  
  this.updateChat();
  
  this.updateDashboard();
  
  // Process players top.
  if (chunk.top != null)
    this.processTop(chunk.top);
};

// Merges in challenge update.
ChannelData.prototype.mergeChallenge = function(chunk) {
  if (this._version >= chunk.v)
    return;
  
  this._version = chunk.v;
  
  if (chunk.w && chunk.r) {
    this._challenge = { what: chunk.w, reward: chunk.r, lead: chunk.l, leadStatus: chunk.ls, time: chunk.s, when: new Date() }
        
    this.addClearAnnouncement();
    if (chunk.l)
      this.addAnnouncement($.encode(Utility.formatName(chunk.l)) + " has the lead." + (chunk.ls ? " " + $.encode(chunk.ls) : ""));
    else
      this.addAnnouncement("Challenge is on!");
    this.addAnnouncement($.encode(chunk.w));
    
    var This = this;
    this.addAnnouncement(function() {
      var secondsLeft = 0;
      if (This._challenge.time)
        secondsLeft = Math.floor(This._challenge.time - (new Date() - This._challenge.when) / 1000);        
       
      if (secondsLeft > 0)
        return This._challenge.reward + " About " + secondsLeft + " seconds left.";
      else
        return This._challenge.reward;
    });
  }
};

// Merges in challenge expiration update.
ChannelData.prototype.mergeChallengeExpired = function(chunk) {
  if (this._version >= chunk.v)
    return;
  
  this._version = chunk.v;
  
  this.addClearAnnouncement();
  this.addOneTimeAnnouncement("Challenge expired with no winners...");
};

// Merges in challenge ending update.
ChannelData.prototype.mergeChallengeEnded = function(chunk) {
  if (this._version >= chunk.v)
    return;
  
  this._version = chunk.v;
  
  if (chunk.u && $.isArray(chunk.u)) {
    for (var p_index in chunk.u) {
      var p = chunk.u[p_index];
      if (p.u == User.name) {        
        this._user_protection = p.p;
        this._user_health = p.h;
        this._user_experience = p.e;
        this.updateDashboard();        
        break;
      }
    }
    
    for (var p_index in chunk.u) {
      var p = chunk.u[p_index];
      
      this.addClearAnnouncement();
      this.addOneTimeAnnouncement($.encode(Utility.formatName(p.u)) + ' has won the challenge.');
    }
  } else {
    this.addClearAnnouncement();
    this.addOneTimeAnnouncement("There were not enough witnesses to score...");
  }
};

// Displays new announcement.
ChannelData.prototype.addClearAnnouncement = function() {
  while (this._announcements.length > 0)
    this._announcements.pop()
    
  if (this._announceStage == "fadeIn") {
    if (this._announceTimer > 0)
      this._announceStage = "fadeOutClear";    
  } else if (this._announceStage == "pause") {
    this._announceStage = "fadeOutClear";
    this._announceTimer = 1;
  }
};

ChannelData.prototype.addAnnouncement = function(message) {
  this._announcements.push({ message: message, clear: false });
};

ChannelData.prototype.addOneTimeAnnouncement = function(message) {
  this._announcements.push({ message: message, clear: true });
};

// Merges in channel update.
ChannelData.prototype.mergeChannelUpdate = function(chunk) {        
  // Process only newer updates.
  if (this._version >= chunk.v)
    return;
  
  this._version = chunk.v;
  
  var c = chunk.c;
  
  var actionX = 0, actionY = 0;
  
  var block = ChannelData.config.block; var half = ChannelData.config.half;
  
  // Access the change.
  if (c != null && $.isArray(c))
  {
    if (c.length >= 3) {
      actionX = c[1];
      actionY = c[2];
    }
    for (var i = 0; i < c.length - 2; i += 3)
      if (this.setAt(c[i + 0], c[i + 1], c[i + 2])) {
        this.scheduleCommand(c[i + 1], c[i + 2], "show");
      }
  }
  
  // Process all related changes.
  if (chunk.r != null && $.isArray(chunk.r))
    for (k in chunk.r) {
      var v = chunk.r[k];
      
      if (this.setAt(0, v[0], v[1])) {
        this.scheduleCommand(v[0], v[1], "hide");
      }
    }
  
  // Process round timer.
  if (chunk.rt != null)
    this.setRoundTimeLeft(chunk.rt);
    
  // Process players top.
  if (chunk.top != null)
    this.processTop(chunk.top);
  
  // Process spells.
  if (chunk.s != null) {
    var s = chunk.s;
    
    switch (s.t) {
      case "heal":
        if (s.u == User.name) {
          var gain = s.h - this._user_health;
          
          this._user_health = s.h;
          this.showBalloon('<span class="spell-defensive">Heal +' + gain + '</span>', actionX * block + half, actionY * block);
          this.updateDashboard();
        }
        else
          this.showBalloon('<span class="name">' + $.encode(Utility.formatName(s.u)) + ':</span> <span class="spell-defensive">Heal</span>', actionX * block + half, actionY * block);
        
        break;
      
      case "shield":
        if (s.u == User.name) {
          var gain = s.p - this._user_protection;
          
          this._user_protection = s.p;
          this.showBalloon('<span class="spell-defensive">Shield +' + gain + '</span>', actionX * block + half, actionY * block);
          this.updateDashboard();
        }
        else
          this.showBalloon('<span class="name">' + $.encode(Utility.formatName(s.u)) + ':</span> <span class="spell-defensive">Shield</span>', actionX * block + half, actionY * block);          
        
        break;
        
      case "fireball":
        if (s.u == User.name) {
          this._user_experience = s.e;
          var time = s.tk > 0 ? 3000 : 2000;
          this.showBalloon('<span class="spell-offensive">Fireball</span> <span class="spell-offensive-what">' + s.td + '!</span>' + (s.tk > 0 ? ' <span class="spell-offensive-ko">KO ' + s.tk + '!</span>' : ''), actionX * block + half, actionY * block, time);
        }
        else {
          var damage = 0;
          
          this._user_protection -= s.d;
          if (this._user_protection < 0) {
            this._user_health += this._user_protection;
            damage = - this._user_protection;
            this._user_protection = 0;
          }
          
          if (this._user_health <= 0) {
            this.ko();
            
            this._user_health = s.koh;
            this._user_protection = s.kop;
            this._user_experience -= s.koe;
            if (this._user_experience < 0)
              this._user_experience = 0;
          }
          
          if (damage == 0)
            damage = "Absorbed"
          this.showBalloon('<span class="name">' + $.encode(Utility.formatName(s.u)) + ':</span> <span class="spell-offensive">Fireball</span> <span class="spell-offensive-what">' + damage + '</span>', actionX * block + half, actionY * block);
        }
          
        this.updateDashboard();
        
        break;
        
      case "airblast":
        if (s.u == User.name) {
          this._user_experience = s.e;
          var time = s.tk > 0 ? 3000 : 2000;
          this.showBalloon('<span class="spell-offensive">Air Blast</span> <span class="spell-offensive-what">' + s.td + '!</span>' + (s.tk > 0 ? ' <span class="spell-offensive-ko">KO ' + s.tk + '!</span>' : ''), actionX * block + half, actionY * block, time);
        }
        else {
          var damage = s.d;
          
          this._user_protection -= s.d;
          if (this._user_protection < 0)
            this._user_protection = 0;
          
          this._user_health -= s.d;          
          if (this._user_health <= 0) {
            this.ko();
            
            this._user_health = s.koh;
            this._user_protection = s.kop;
            this._user_experience -= s.koe;
            if (this._user_experience < 0)
              this._user_experience = 0;
          }
          
          if (damage == 0)
            damage = "Absorbed";
          this.showBalloon('<span class="name">' + $.encode(Utility.formatName(s.u)) + ':</span> <span class="spell-offensive">Air Blast</span> <span class="spell-offensive-what">' + damage + '</span>', actionX * block + half, actionY * block);
        }
        
        this.updateDashboard();
        
        break;
      
      case "leech":
        if (s.u == User.name) {
          var gain = s.h - this._user_health;
          
          this._user_health = s.h;
          this._user_experience = s.e;
          this.showBalloon('<span class="spell-offensive">Leech</span> <span class="spell-offensive-what">' + gain + '</span>', actionX * block + half, actionY * block);          
        }
        else {
          var damage = this._user_health;
          
          this._user_health -= s.d;          
          if (this._user_health <= 1)
            this._user_health = 1;
          
          damage -= this._user_health;
          
          if (damage == 0)
            damage = "Absorbed";
          this.showBalloon('<span class="name">' + $.encode(Utility.formatName(s.u)) + ':</span> <span class="spell-offensive">Leech</span> <span class="spell-offensive-what">' + damage + '</span>', actionX * block + half, actionY * block);
        }
        
        this.updateDashboard();
        
        break;
      
      case "spawn":
        this.showBalloon('<span class="spell-defensive">Spawn</span>', actionX * block + half, actionY * block);
        
        var p = s.p;
        
        for (var i = 0; i < p.length - 2; i += 3)
          if (this.setAt(p[i + 0], p[i + 1], p[i + 2])) {
            this.scheduleCommand(p[i + 1], p[i + 2], "show");
          }
          
        break;
    }
  }
};

// Process top players message.
ChannelData.prototype.processTop = function(t) {
  var x = 0;
  
  var sb = new StringBuilder();
  for (var i = 0; i < t.length; ++i) {
    var tu = t[i];
    if (tu.u) {
      sb.appendList('<div class="stats-top" style="left:', x, 'px;"><div>', $.encode(Utility.formatName(tu.u)), '</div><div class="stats-stats"><span class="stats-heart">', $.encode(tu.h + ""), '</span>&nbsp;<span class="stats-shield">', $.encode(tu.p + ""), '</span></div></div>');
      
      x += 90;
    }
  }
  
  $("#id_top").html(sb.toString());
};

// Shows KO sequence.
ChannelData.prototype.ko = function(chunk) {
  Widgets.Transitions.fadeIn($("#id_channel_ko"), function() {
    Widgets.Transitions.fadeOut($("#id_channel_ko"), null, 2500);
  }, 500)
};

// Merges in system notification.
ChannelData.prototype.mergeSystem = function(chunk) {
  if (chunk.m == "killswitch")
    State.go("welcome.launch")
  else
    Widgets.MessageBox.notice($.encode(chunk.m));
};

// Merges in join notification.
ChannelData.prototype.mergeJoin = function(chunk) {
  if (User.name == chunk.u && Meteor.hostid == chunk.id) {
    this._joined = true;    
    this._user_protection = chunk.d;
    this._user_health = chunk.h;
    this._user_experience = chunk.e;
    
    if (chunk.a) {
      this._chatMessages.push('<span class="chat-alone">Don\'t play alone, invite some friends!</span>');
    }
    
    if (chunk.rt != null)
      this.setRoundTimeLeft(chunk.rt);
  }
  
  this.updateDashboard();
  
  this._chatMessages.push('<span class="chat-join">' + $.encode(Utility.formatName(chunk.u) + " has joined.") + '</span>');
  
  this.updateChat();
  
  if (chunk.top)
    this.processTop(chunk.top);
};

ChannelData.prototype.mergeLeave = function(chunk) {
  this._chatMessages.push('<span class="chat-join">' + $.encode(Utility.formatName(chunk.u) + " has left.") + '</span>');
  
  this.updateChat();
  
  if (chunk.top)
    this.processTop(chunk.top);
};

ChannelData.prototype.mergeDisappear = function(chunk) {
  if (chunk.u && $.isArray(chunk.u) && chunk.u.length > 0) {
    var sb = new StringBuilder();
        
    for (var i = 0; i < chunk.u.length; ++i) {
      if (i != 0)
        sb.append(", ");
      sb.append(Utility.formatName(chunk.u[i]));
    }
    
    sb.append(" disappeared.");
  }
    
  this._chatMessages.push('<span class="chat-join">' + $.encode(sb.toString()) + '</span>');
  
  this.updateChat();
  
  if (chunk.top)
    this.processTop(chunk.top);
};

// Updates user information.
ChannelData.prototype.updateDashboard = function() {
  $("#id_dashboard_nickname").html($.encode(Utility.formatName(User.name)));
  $("#id_dashboard_health").html($.encode(this._user_health + ""));
  $("#id_dashboard_protection").html($.encode(this._user_protection + ""));
  $("#id_dashboard_experience").html($.encode(this._user_experience + ""));
};

// Updates chat.
ChannelData.prototype.updateChat = function() {
  while (this._chatMessages.length > 4)
    this._chatMessages.shift();
    
  var result = new StringBuilder();
  
  for (var i = 0; i < this._chatMessages.length; ++i) {
    if (i != 0)
      result.append("<br/>");
    result.append(this._chatMessages[i]);
  }
  
  $("#id_chat_messages").html(result.toString());
};

// Schedules a drawing command.
ChannelData.prototype.scheduleCommand = function(x, y, type) {
  var command = {x: x, y: y, type: type, time: 1.0 };
  
  for (var i = 0; i < this._scheduledCommands.length; ++i) {
    var v = this._scheduledCommands[i];
    
    if (v.x == x && v.y == y)
    {
      this._scheduledCommands[i] = command;
      return command;
    }
  }
  
  this._scheduledCommands.push(command);
  return command;
}

// Gets current drawing surface.
ChannelData.prototype.getCanvas = function() {
  return new Widgets.Canvas($("#id_channel_canvas"), 315, 210);
};

// Completely rerenders channel.
ChannelData.prototype.renderFull = function() {
  var canvas = this.getCanvas();
  
  var rows = this._rows; var block = ChannelData.config.block;
  
  canvas.clear();
  
  for (var y = 0; y < rows.length; ++y) {
    var row = rows[y];
    for (var x = 0; x < row.length; ++x) {
      canvas.drawAtlasImage(this._blockMapping[row[x]], x * block, y * block);
    }
  }
};

// Paints incremental channel updates.
ChannelData.prototype.renderIncremental = function(secondsElapsed) {
  // Update round timer.
  this.updateRoundTimer();
  
  // Update announcements.
  if (this._announceStage == "fadeIn") {
    if (this._announcements.length > 0) {
      if (this._announceTimer == 0) {
        if ($.isFunction(this._announcements[0].message)) {
          this._announcementsObject.html(this._announcements[0].message());
        } else {
          this._announcementsObject.html(this._announcements[0].message);
        }
      }
      
      this._announceTimer += secondsElapsed * 3.0;
      
      if (this._announceTimer >= 1) {
        this._announcementsObject.css("opacity", "1");
        
        this._announceStage = "pause";
        this._announceTimer = 3;
      } else      
        this._announcementsObject.css("opacity", this._announceTimer + "");      
    }
  } else if (this._announceStage == "pause") {
    this._announceTimer -= secondsElapsed;
    
    if (this._announceTimer < 0) {
      this._announceStage = "fadeOut";
      this._announceTimer = 1;
    }
  } else if (this._announceStage == "fadeOut") {
    this._announceTimer -= secondsElapsed * 3.0;
    
    if (this._announceTimer <= 0) {
      this._announcementsObject.css("opacity", "0");
      
      this._announceStage = "fadeIn";
      this._announceTimer = 0;
      
      if (this._announcements.length > 0) {
        if (this._announcements[0].clear)
          this._announcements.shift();
        else
          this._announcements.push(this._announcements.shift());
      }      
    } else      
      this._announcementsObject.css("opacity", this._announceTimer + "");
  } else if (this._announceStage == "fadeOutClear") {
    this._announceTimer -= secondsElapsed * 2.0;
    
    if (this._announceTimer <= 0) {
      this._announcementsObject.css("opacity", "0");
      this._announceStage = "fadeIn";
      this._announceTimer = 0;
    } else      
      this._announcementsObject.css("opacity", this._announceTimer + "");
  }   
  
  // Update game screen.
  var canvas = this.getCanvas();
  
  var scheduled = this._scheduledCommands;
  
  var rows = this._rows; var block = ChannelData.config.block;
  
  if (scheduled.length == 0)
    return;
  
  for (var i = 0;;) {
    var v = scheduled[i];
    
    var c = rows[v.y][v.x];
    
    if (v.type == "show") {
      var x = v.x * block;
      var y = v.y * block;
      
      canvas.clear(x, y, block, block);
      canvas.drawAtlasImage(this._blockMapping[c], x, y);

      if (v.time == 1.0) {
        canvas.fill("rgba(0, 0, 0, 0.5)", x, y, block, block);
        v.time = 0.6;
      } else if (v.time == 0.6) {
        canvas.fill("rgba(0, 0, 0, 0.3)", x, y, block, block);
        v.time = 0.3;
      } else if (v.time == 0.3) {        
        v.time = 0;
      }
    }
    if (v.type == 'select') {
      var x = v.x * block;
      var y = v.y * block;
      
      canvas.clear(x, y, block, block);
      canvas.drawAtlasImage(this._blockMapping[v.c], x, y);

      if (v.time == 1.0) {
        canvas.fill("rgba(0, 0, 0, 0.8)", x, y, block, block);
        v.time = 0.6;
      } else if (v.time == 0.6) {
        canvas.fill("rgba(0, 0, 0, 0.65)", x, y, block, block);
        v.time = 0.3;
      } else if (v.time == 0.3) {
        canvas.fill("rgba(0, 0, 0, 0.5)", x, y, block, block);
        v.time = 0;
      }
    }
    if (v.type == 'hide') {
      var x = v.x * block;
      var y = v.y * block;
      
      if (v.time == 1.0) {
        canvas.fill("rgba(0, 0, 0, 0.3)", x, y, block, block);
        v.time = 0.6;
      } else if (v.time == 0.6) {
        canvas.fill("rgba(0, 0, 0, 0.3)", x, y, block, block);
        v.time = 0.3;
      } else if (v.time == 0.3) {
        canvas.clear(x, y, block, block);
        canvas.drawAtlasImage(this._blockMapping[0], x, y);
        v.time = 0;
      }
    }    
    
    if (v.time == 0) {
      if (i == scheduled.length - 1)
        scheduled.pop();
      else
        scheduled[i] = scheduled.pop();        
    } else
      ++i;
    
    if (i >= scheduled.length)
      break;
  }
};

// Submits user's color selection to server.
ChannelData.prototype.pick = function(color, x, y) {
  var thisPointer = this; var selectX = x; var selectY = y;
  
  this.scheduleCommand(x, y, "select").c = color;
  
  this._spellCooldown = new Date();
  
  $.ajax({type: "POST", url: Utility.formatUrl("/frontend/pick/"), data: { channel: this._channel, color: color, x: x, y: y },
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors)
          Widgets.MessageBox.error(response.errors);
        else {
          // Do nothing, wait for update.
        }
      },
      error: function() {
        // In case of generic connection error, simply undo the selection.
        thisPointer.scheduleCommand(selectX, selectY, "show");
      }}
    );
};

////////////////////////////////////////////////////
// Channel connectivity controller.
State.channel_error = {
  enter: function() {
    setTimeout(function() {
      State.go("welcome.launch");
    }, 100);
  },
  leave: function() {
  }
};

////////////////////////////////////////////////////
// Channel connectivity controller.

State.channel = { _channel: null, _connectingTimer: null, _sessionValidationTimer: null, _lastSessionTimestamp: null, _giveUpConnect: false, _channelData: null, _channelUpdates: null, _channelTimer: null, _connectingStream: null, _connectingData: null, _timePerFrame: 150,
  enter: function(context) {
    this._channel = context.channel;
    this._connectingTimer = null;
    this._giveUpConnect = false;
    
    $("#id_dashboard_nickname").html("");
    $("#id_dashboard_health").html("");
    $("#id_dashboard_protection").html("");
    $("#id_dashboard_experience").html("");
    
    // Setup session validation timer. This is to prevent resuming game from the sleep, hibernation or viewing another page on iPhone.
    this._lastSessionTimestamp = null;
    this._sessionValidationTimer = null;
    this.validateSession();
    
    Widgets.Transitions.hide($("#id_channel"));
  },
  leave: function() {
    this.disconnect();
    
    Widgets.Transitions.fadeOut($("#id_channel"));
  },
  disconnect: function() {
    // Stop rendering updates.
    if (this._channelTimer) {
      clearTimeout(this._channelTimer);
      this._channelTimer = null;
    }
    
    // Stop connection timer.
    if (this._connectingTimer) {
      clearTimeout(this._connectingTimer);
      this._connectingTimer = null;
    }
    
    // Stop session validation.
    if (this._sessionValidationTimer) {
      clearTimeout(this._sessionValidationTimer);
      this._sessionValidationTimer = null;
    }
    
    // Disconnect from server.
    
    // Notify server that client is leaving.
    $.ajax({type: "POST", url: Utility.formatUrl("/frontend/leave/"), data: { channel: this._channel, id: Client.id() },
      success: function(json) { /* Do not care about result. */ }, error: function() { /* Do not care about result. */ }});
    
    if (!this._giveUpConnect)
      Client.disconnect("battleground-world-" + this._channel);
  },
  validateSession: function() {
    var now = new Date();
    
    if (this._lastSessionTimestamp != null) {
      if (now - this._lastSessionTimestamp > 15000) {
        // Whoops, too much time elapsed since the last check.
        State.go("channel_error");
        return;
      }
    }    
    
    this._lastSessionTimestamp = now;
    
    // Call timer in a bit.
    var This = this;
    this._sessionValidationTimer = setTimeout(function() {
      This.validateSession();
    }, 250);
  },
  connect: function() {
    var This = this;
    
    this._channelUpdates = [];
    this._connectingStream = true;
    this._connectingData = true;    
    
    this._connectingTimer = setTimeout(function() {
      if (This._connectingTimer) {
        clearTimeout(This._connectingTimer);
        
        This._connectingTimer = null;        
        This._giveUpConnect = true;
        
        Client.disconnect("battleground-world-" + This._channel);
        
        Widgets.MessageBox.error("We have been trying to connect to the server, but something didn't work. Please try again.", function() {
          State.go("welcome.launch");  
        });        
      }
    }, 60000);
    
    // Initiatializes connection to server, starting with stream connection.
    Client.connect("battleground-world-" + this._channel, 100, function(data) {
        State.channel.process(data);
      }, function(status) {
        State.channel.statusChanged(status);
      });
  },  
  connectData: function() {
    if (this._giveUpConnect)
      return;
    
    if (this._connectingTimer) {
      clearTimeout(this._connectingTimer)
      this._connectingTimer = null;
    }
    
    // Retrieves current channel state 
    $.ajax({type: "POST", url: Utility.formatUrl("/frontend/sync/"), data: { channel: this._channel, id: Client.id(), client_version: CLIENT_VERSION, cache_version: CACHE_VERSION },
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors) {
          if ($.isArray(response.errors) && response.errors.length > 0 && response.errors[0] == "Client version is out of date.") {
            Widgets.MessageBox.notice("There was a game update and page will need to be reloaded to continue.", function() {
              window.location.reload();
            });
          } else {          
            // Sleep and try again later.
            setTimeout(function() {
              State.channel.connectData();
            }, 3000);
          }
        }
        else if (response.version == null || response.version == "None") {
          // Channel is not yet ready.
          setTimeout(function() {
            State.channel.connectData();
          }, 3000);
        } else {          
          // Wow, we are connected.
          if (State.channel._connectingData)
            State.channel.processConnected(response);
        }
      },
      error: function() {
        // In case of generic connection error, sleep a bit and try to reconnect.
        setTimeout(function() {
          State.channel.connectData();
        }, 3000);
      }}
    );
  },
  processConnected: function(data) {
    // Called when client is connected to server stream to continue game initialization.    
    this._connectingData = false;
            
    // Initialize channel.
    this._channelData = new ChannelData(this._channel, data);
    
    // Merge accumulated updates.
    for (var i = 0; i < this._channelUpdates.length; ++i)
      this._channelData.mergeUpdate(this._channelUpdates[i], true);
    this._channelUpdates = [];
    
    // Render initial screen update.
    this._channelData.renderFull();
    
    // Setup a timer to render incremental updates.
    this.scheduleNextFrame();
    
    // Don't switch state yet, wait until join event.
    if (this._channelData.joined())
    {
      Widgets.MessageBox.hide();
      Widgets.Transitions.fadeIn($("#id_channel"));
    
      State.go("channel.connected")
    }
    
  },
  process: function(data) {
    // Handle a message from server.
    try
    {    
      response = JSON.parse(Utility.unpackMessage(data));
    }
    catch (e) {
      // Could not deserialize json data, possibly an attack?
      return;
    }
    
    if (this._connectingData) {
      // We don't have latest gameplay data yet, cache changes until that moment.
      this._channelUpdates.push(response);
    } else {
      var joinedStatus = this.channelData() ? this.channelData().joined() : false;
      this._channelData.mergeUpdate(response);
      var joinedNow = this.channelData() ? this.channelData().joined() : false;    
      // Switch state after joined event was receieved.
      if (!joinedStatus && joinedNow) {
        Widgets.MessageBox.hide();
        Widgets.Transitions.fadeIn($("#id_channel"));
      
        State.go("channel.connected")
      }
    }
  },
  statusChanged: function(status) {
    // Handle status changes in streaming.
    
    if (Client.connected() && this._connectingStream) {
      // Meteor stream is now connected, continue with data connect.
      Widgets.MessageBox.loading("Dreaming...");
                                 
      this._connectingStream = false;
      
      this.connectData();
    }
  },
  scheduleNextFrame: function() {
    // Primes timer for a next screen refresh.
    var thisPointer = this;
    this._channelTimer = setTimeout(function() {
      thisPointer.tick();
    }, this._timePerFrame);
  },
  tick: function() {
    // Handles frame updates.
    if (!!this._channelData)
      this._channelData.renderIncremental(this._timePerFrame / 1000.0);
    
    this.scheduleNextFrame();
  },
  channelData: function() {
    return this._channelData;
  }
};

////////////////////////////////////////////////////
// Initial server communication.

State.channel.launch = { _parent: State.channel,  
  enter: function() {
    // Get rid of pesky iPhone address bar.
    window.scrollTo(0, 1);
    
    Widgets.MessageBox.loading("Entering meditation...");
    
    State.channel.connect();    
  },
  leave: function() {}
};

////////////////////////////////////////////////////
// Main game connected state.

State.channel.connected = { _parent: State.channel, _selectX: null, _selectY: null, _cooldownTimer: null,
  enter: function() {
  },
  leave: function() {
    this.hideSelectors();
  },
  pickColor: function(color) {
    if (color == null)    
      color = this._trampleElemental;
      
    if (!this._parent.channelData().spellReady())
      return;
      
    this.hideSelectors();
    
    State.channel.channelData().pick(color, this._selectX, this._selectY);
  },  
  eventClick: function(pt) {
    if (State.name == "channel.connected" && this._parent.channelData().joined()) {
      
      this._selectX = Math.floor(pt.x / ChannelData.config.block);
      this._selectY = Math.floor(pt.y / ChannelData.config.block);      
       
      var s = Widgets.Sprites; var c = ChannelData.config; var widthHalf = this._parent.channelData().widthHalf(); var heightHalf = this._parent.channelData().heightHalf();
      
      var arrowX = this._selectX * c.block + c.half + c.fieldOffsetX - s.arrow_up.width / 2;
      var arrowY = this._selectY * c.block + c.half + c.fieldOffsetY;
    
      // Place selection arrows.
      if (this._selectY < heightHalf) {
        Widgets.Transitions.show($("#id_pick_arrow_up")).relocate(arrowX, arrowY);
        Widgets.Transitions.hide($("#id_pick_arrow_down"));
      } else {
        arrowY -= s.arrow_up.height;
        
        Widgets.Transitions.show($("#id_pick_arrow_down")).relocate(arrowX, arrowY);
        Widgets.Transitions.hide($("#id_pick_arrow_up"));
      }
    
      // Show correct elemental image.
      var elemental = State.channel.channelData().at(this._selectX, this._selectY);
      if (elemental == 0) {
        // Show all elements picker.
        Widgets.Transitions.hide($("#id_pick_trample"));        
        
        if (this._selectX < widthHalf)
          arrowX -= 3;
        else
          arrowX -= c.pickerAllWidth - 3 - s.arrow_up.width;
        if (this._selectY < heightHalf)
          arrowY += s.arrow_up.height + 3;
        else
          arrowY -= c.pickerAllHeight + 3;
          
        Widgets.Transitions.show($("#id_pick_all")).relocate(arrowX, arrowY);
      } else {
        // Show trample selection.
        Widgets.Transitions.hide($("#id_pick_all"));
        
        if (this._selectX < widthHalf)
          arrowX -= 3;
        else
          arrowX -= c.pickerTrampleWidth - 3 - s.arrow_up.width;
        if (this._selectY < heightHalf)
          arrowY += s.arrow_up.height + 3;
        else
          arrowY -= c.pickerTrampleHeight + 3;
          
        this._trampleElemental = State.channel.channelData().trample(elemental);
        
        Widgets.FormBuilder.setImage($("#id_pick_trample_selector"), State.channel.channelData().map(this._trampleElemental));
          
        Widgets.Transitions.show($("#id_pick_trample")).relocate(arrowX, arrowY);
      }
      
      this.setCooldownState();
    }
  },
  setCooldownState: function() {
    // Enable color selectors depending on spell cooldown time.
    if (this._parent.channelData().spellReady()) {
      $("#id_pick_all_content > img").css("opacity", "1");
      $("#id_pick_trample_content > img").css("opacity", "1");
    } else {
      $("#id_pick_all_content > img").css("opacity", "0.3");
      $("#id_pick_trample_content > img").css("opacity", "0.3");
      
      var This = this;
      this._cooldownTimer = setTimeout(function() {
        This.setCooldownState();
      }, 250);
    }
  },
  hideSelectors: function() {
    Widgets.Transitions.hide($("#id_pick_all"));
    Widgets.Transitions.hide($("#id_pick_trample"));
    Widgets.Transitions.hide($("#id_pick_arrow_up"));
    Widgets.Transitions.hide($("#id_pick_arrow_down"));
    
    if (this._cooldownTimer) {
      clearTimeout(this._cooldownTimer);
      this._cooldownTimer = null;
    }
  }
};

////////////////////////////////////////////////////
// Typing chat message.

State.channel.chat = { _parent: State.channel,
  enter: function() {
    Widgets.Transitions.show($("#id_chat"));
    Widgets.Transitions.show($("#frame_menu"))
    
    $("#id_chat-message").focus();
  },
  leave: function() {
    Widgets.Transitions.hide($("#id_chat"));
    Widgets.Transitions.hide($("#frame_menu"))
  },
  send: function() {
    // Fire-n-forget.
    $.ajax({type: "POST", url: Utility.formatUrl("/frontend/chat/"), data: $("#id_chat").coreSerialize("action_chat", { "chat-channel": this._parent.channelData().channel() }),
      success: function(json) { /* Do not care about result. */ }, error: function() { /* Do not care about result. */ }});
    
    // Clear current message.
    $("#id_chat-message").val("");
    
    State.go("channel.connected");
  }
};

////////////////////////////////////////////////////
// Main game menu.

State.channel.menu = { _parent: State.channel,
  enter: function() {
    Widgets.Transitions.show($("#id_menu"));
    Widgets.Transitions.show($("#frame_menu"))
  },
  leave: function() {
    Widgets.Transitions.hide($("#id_menu"));
    Widgets.Transitions.hide($("#frame_menu"))
  }
}

////////////////////////////////////////////////////
// Game menu online users listing.

State.channel.online = { _parent: State.channel, _lastOnline: null,
  enter: function() {
    var This = this;
    
    Widgets.Transitions.hide($("#action_online_health"));
    Widgets.Transitions.show($("#action_online_experience"));
    
    Widgets.Transitions.show($("#id_online"));
    Widgets.Transitions.show($("#frame_menu"))
    
    $("#id_online_names").html("");
    
    $.ajax({type: "POST", url: Utility.formatUrl("/frontend/listonline/"), data: { channel: this._parent.channelData().channel() },
      success: function(json) {
        var response = JSON.parse(json);
        if (response.errors) {          
        } else {
          if (response.online && $.isArray(response.online)) {            
            This._lastOnline = response;
            
            This.sortHealth();
          }
        }
      },
      error: function() {
        Widgets.MessageBox.error("Server is busy. Please try again.");
      }});
  },
  leave: function() {
    Widgets.Transitions.hide($("#id_online"));
    Widgets.Transitions.hide($("#frame_menu"))
  },
  sortHealth: function() {
    Widgets.Transitions.hide($("#action_online_health"));
    Widgets.Transitions.show($("#action_online_experience"));
    
    this.sort('vitals', function(a, b){
      var ea = parseInt(a.h);
      var eb = parseInt(b.h);
      
      if (ea < eb) return 1;
      if (ea == eb) return 0;
      
      return -1;
    });
  },
  sortExperience: function() {
    Widgets.Transitions.show($("#action_online_health"));
    Widgets.Transitions.hide($("#action_online_experience"));
    
    this.sort('experience', function(a, b){
      var ea = parseInt(a.e);
      var eb = parseInt(b.e);
      
      if (ea < eb) return 1;
      if (ea == eb) return 0;
      
      return -1;
    });
  },
  sort: function(type, sortFunction) {
    var response = this._lastOnline;
    
    var sb = new StringBuilder();
            
    var e = Widgets.Sprites.star_small;
    var h = Widgets.Sprites.heart_small;
    var s = Widgets.Sprites.shield_small;
    
    response.online.sort(sortFunction);
    
    for (var i = 0; i < response.online.length; ++i) {
      var user = response.online[i];
      
      var cssClass = "chat-online";
      if (user.u == User.name)
        cssClass = "chat-online-self";
      else
        if (user.u.startsWith("__Guest"))
          cssClass = "chat-online-guest";
      
      sb.appendList(
        '<span class="', cssClass, '">', $.encode(Utility.formatName(user.u)).replace(/\s/g, "&nbsp;"));
      
      if (type == "vitals") {
        sb.appendList('&nbsp;<span class="chat-online-score stats-heart">', $.encode(user.h));
      } else if (type == "experience") {
        sb.appendList(
          '&nbsp;<span class="chat-online-score stats-star">', $.encode(user.e));
      }
      sb.appendList('</span></span>&nbsp;&nbps; ');
    }
    
    $("#id_online_names").html(sb.toString())
  }  
}

////////////////////////////////////////////////////
// Game disconnecting state.

State.channel.disconnecting = { _parent: State.channel,
  enter: function() {    
    Widgets.Transitions.fadeOut($("#id_channel"));
  },
  leave: function() {    
  }
};

////////////////////////////////////////////////////
// Meteor communications layer.
Client = { _connected: false, _process: null, _statusChanged: null,
  connected: function() { return this._connected; },
  launch: function() {
    if (!Widgets.isApple)
      Meteor.debugmode = false;
    
    var host = location.hostname;
    if (host.substr(0, 4) == "www.")
      host = host.substring(4);
    Meteor.host = "data." + host;    
    
    Meteor.registerEventCallback("process", function(data) {
      Client.process(data);
    });
    Meteor.registerEventCallback("statuschanged", function(data) {
      Client.statusChanged(data);
    });
  },
  id: function() {
    return Meteor.hostid;
  },
  connect: function(channel, messages, processCallback, statusChangedCallback) {
    this._process = processCallback;
    this._statusChanged = statusChangedCallback;
    
    Meteor.hostid = Meteor.time() + "" + Math.floor(Math.random()*1000000)
    
    Meteor.joinChannel(channel, messages);
    Meteor.mode = "stream";
    Meteor.connect();
  },
  disconnect: function(channel) {
    Meteor.disconnect();
    Meteor.leaveChannel(channel);
  },
  process: function(data) {
    // Processes data from the server.
    if (this._process)
      this._process(data);
  },
  statusChanged: function(status) {    
    // Processes streaming status changes.
    this._connected = status == 5;
    
    if (this._statusChanged)
      this._statusChanged(status);
  }
};

////////////////////////////////////////////////////
// Widgets extension.

// Creates a progress bar.
Widgets.FormBuilder.prototype.channelProgressBar = function(name, click) {
  assert(!!this.options.container);
  
  var o = this.options; var s = Widgets.Sprites;
  
  o.container.append(
    StringBuilder.toStringList(
      '<div id="', name, '" style="cursor: pointer; position: absolute; left: ', o.x, 'px; top: ', o.y, 'px; width: ', s.progress_bar_empty.width, 'px;">',
        '<div id="', name, '_text" class="form-text-tiny" style="height: 16px;"></div>',
        '<div style="position: relative;">',
          '<img src="/static/dot.gif" style="position: absolute; left: 0px; top: 0px; width: ', s.progress_bar_empty.width, 'px; height: ', s.progress_bar_empty.height, 'px; background: transparent url(', Widgets.images.atlas, ') no-repeat ', s.progress_bar_empty.style, ';" />',
          '<div id="', name, '_progress" style="position: absolute; left: 0px; top: 0px; width: ', 5, 'px; height: ', s.progress_bar_empty.height, 'px; background: transparent url(', Widgets.images.atlas, ') no-repeat ', s.progress_bar_full.style, ';"><!-- Empty. --></div>',
        '</div>',
      '</div>'
    ));
  
  if (click)
    $("#" + name).click(click);
  
  return this;
};

// Sets progress bar value.
Widgets.FormBuilder.setChannelProgressBar = function(element, text, value, maxValue) {
  var ratio = value / maxValue;
  if (ratio < 0) ratio = 0;
  if (ratio > 1) ratio = 1;
  var percentage = Math.floor(ratio * 97) + 5;
  
  $("#" + element.attr("id") + "_text").html(text);
  
  $("#" + element.attr("id") + "_progress").css("width", percentage + "px");
};

// Default button map.
Widgets.default_button_map = {};

// Maps a state to a default button.
Widgets.map_default_button = function(stateName, buttonName) {
  Widgets.default_button_map[stateName] = buttonName;
}

// Dispatches submit depending on the current state.
Widgets.dispatch_submit = function() {
  // Handle default button only when there is no message box.
  if ($("#message").css("opacity") == "0") {
    var button = Widgets.default_button_map[State.name];
  
    if (button) {
      $("#action_no_focus_input").css("display", "inline").focus().css("display", "none");
      
      $(button).click();
    }
  }
}

////////////////////////////////////////////////////
// Utilities.

var Utility = {
  formatUrl: function(requestUrl) {
    var location = window.location + "";
    
    if (location.substring(location.length - 1) == "/")
      location = location.substring(0, location.length - 1);
      
    return location + SITE_PATH + requestUrl.substring(1);
  },
  formatName: function(name, anonymizeGuest) {
    if (name == null)
      return "";
    if (anonymizeGuest)
      return name.startsWith("__Guest") ? "Guest" : name;
    else
      if (name.startsWith("__"))
        return name.substring(2);
        
    return name;
  },
  unpackMessage: function(message) {
    return message.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
  }
};

////////////////////////////////////////////////////
// Initialization.

$(function() {  
  // Kick off preloader.
  Widgets.load(function() {
    // Map default buttons for states.
    Widgets.map_default_button("intro", "#action_play");
    Widgets.map_default_button("forgot", "#action_reset_password");
    Widgets.map_default_button("register", "#action_register");
    Widgets.map_default_button("welcome.my_account", "#action_my_account_save");
    Widgets.map_default_button("channel.chat", "#action_chat");
    Widgets.map_default_button("welcome.support", "#action_support_save");
    
    // Patch up images after preloading.
    Widgets.FormBuilder.setBackground($("#content"), Widgets.backgrounds.generic);
    
    ////////////////////////////////////////////////////
    // Main frame.
    (new Widgets.FormBuilder({x: 4, y: 4, width: 312, height: 348 }))
      .visible(false).frame($("#frame_lobby")).offset(0, -25).logo();
    
    ////////////////////////////////////////////////////  
    // Login form.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 272, height: 308, container: $("#id_login") }))
      .sizeContainer()
      .size(76, 69).position(5, 45).place($("#id_login-intro_image"))
      .size(177, 69).position(92, 45).multiline($("#id_login-intro_text"))      
      .size(130, 28).position(5, 170).text($("#id_login-login_label"), "main", "left")
      .offset(135, 0).text($("#id_login-password_label"), "main", "left")
      .offset(-135, 28).input($("#id_login-login"))
      .offset(135, 0).input($("#id_login-password"))
      .offset(-135, 30).checkbox($("#id_login-remember"))      
      .offset(0, 30).button($("#action_play"), function(e) {
        State.intro.login();
      })      
      .offset(135, 0).button($("#action_forgot"), function(e) {
        State.go("forgot");
      })
      .offset(-135, -130).button($("#action_play_as_guest"), function(e) {
        // Login with guest credentials.
        State.intro.loginAsGuest();
      })
      .offset(135, 0).button($("#action_sign_up"), function(e) {
        State.go("register");
      })
      .size(284, 20).position(0, 295).text($("#id_login-copyright"), "main", "center")
    ;
    
    ////////////////////////////////////////////////////
    // Registration form.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 272, height: 308, container: $("#id_register") }))
      .sizeContainer()
      .size(270, 28).offset(0, 88).entry($("#id_register-login"), 100, "main", "right")
      .offset(0, 30).entry($("#id_register-email"), 100, "main", "right")
      .offset(0, 30).entry($("#id_register-password"), 100, "main", "right")
      .offset(0, 30).entry($("#id_register-password_again"), 100, "main", "right")
      .resize(-100, 0).offset(100, 30).checkbox($("#id_register-terms"))
      .offset(0, 35).button($("#action_register"), function(e) {
        State.register.register();
      })
      .offset(0, 35).button($("#action_register_back"), function(e) {
        State.go("intro");        
      })
    ;
    
    ////////////////////////////////////////////////////
    // Reset password form.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 272, height: 308, container: $("#id_reset_password") }))
      .sizeContainer()
      .size(270, 28).offset(0, 88).entry($("#id_reset_password-login"), 100, "main", "right")
      .resize(-100, 0).offset(100, 35).button($("#action_reset_password"), function(e) {
        State.forgot.resetPassword();
      })
      .offset(0, 35).button($("#action_reset_password_back"), function(e) {
        State.go("intro");        
      })
    ;
      
    ////////////////////////////////////////////////////
    // Game frame.
    (new Widgets.FormBuilder({x: -2, y: -2, width: 324, height: 360 }))
      .visible(false).frame($("#frame_game")).offset(0, 326).logoTiny();
      
    ////////////////////////////////////////////////////
    // Login welcome screen.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 284, height: 320, container: $("#id_welcome") }))
      .sizeContainer()
      .size(111, 28).offset(0, 0).button($("#action_my_account"), function(e) {
        State.go("welcome.my_account");
      })
      .button($("#action_become_member"), function(e) {
        State.welcome.welcome.becomeMember();
      })
      .offset(116, 0).size(82, 28).button($("#action_help"), function(e) {
        State.go("welcome.help");
      })
      .offset(87, 0).button($("#action_logoff"), function(e) {
        State.welcome.welcome.logoff();
      })
      .position(0, 39).size(53, 85).image("rankings", function(e) {
        State.go("welcome.rankings");
      })
      .offset(58, 0).image("locked", function(e) {
      })
      .size(170, 90).position(116, 39).container($("#id_welcome_news")).sizeContainer()
      .position(0, 0).size(284, 170).offset(0, 135).container($("#id_welcome_loading"))
        .sizeContainer()
        .offset(142, 70).ticker($("#id_welcome_loading"))
      .position(0, 0).size(284, 170).offset(0, 135).container($("#id_welcome_server_status"))
        .sizeContainer()
        .size(190, 20).offset(0, -135).text($("#id_welcome_server_status > .user"), "small", "left")
        .size(94, 20).offset(190, 0).text($("#id_welcome_server_status > .online"), "small", "right")
        .position(0, 0).offset(20, 24).channelProgressBar("id_channel_0", function(e) {
          State.welcome.welcome.join(0);
        })
        .offset(140, 0).channelProgressBar("id_channel_3", function(e) {
          State.welcome.welcome.join(3);
        })
        .offset(-140, 48).channelProgressBar("id_channel_1", function(e) {
          State.welcome.welcome.join(1);
        })
        .offset(140, 0).channelProgressBar("id_channel_4", function(e) {
          State.welcome.welcome.join(4);
        })
        .offset(-140, 48).channelProgressBar("id_channel_2", function(e) {
          State.welcome.welcome.join(2);
        })
        .offset(140, 0).channelProgressBar("id_channel_5", function(e) {
          State.welcome.welcome.join(5);
        })
        .size(284, 20).position(0, 305).text($("#id_welcome-copyright"), "main", "center")
    ;
    
    ////////////////////////////////////////////////////  
    // My account.    
    (new Widgets.FormBuilder({x: 0, y: 0, width: 284, height: 320, container: $("#id_my_account") }))
      .sizeContainer()      
      .size(270, 28).offset(12, 0).text($("#id_my_account-change_password"), "main", "left")
      .offset(0, 30).entry($("#id_my_account-old_password"), 100, "main", "right")
      .offset(0, 30).entry($("#id_my_account-password"), 100, "main", "right")
      .offset(0, 30).entry($("#id_my_account-password_again"), 100, "main", "right")
      .resize(-100, 0).offset(100, 35).button($("#action_my_account_save"), function(e) {
        State.welcome.my_account.save();
      })
      .offset(0, 35).button($("#action_my_account_back"), function(e) {
        State.go("welcome.welcome");
      })    
    ;
    
    ////////////////////////////////////////////////////  
    // Help.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 284, height: 320, container: $("#id_help") }))
      .sizeContainer()
      .size(130, 28).offset(10, 290).button($("#action_more_help"), function(e) {
        State.go("welcome.support");
      })
      .offset(135, 0).button($("#action_help_back"), function(e) {
        State.go("welcome.welcome");
      })
      .size(180, 28).position(57, 230).button($("#action_help_rules"), function(e) {
        State.welcome.help.next();
      })
      .size(275, 15).position(5, 267).horizontalDivider()
      
      .size(285, 205).position(0, 0).container($("#id_help_intro")).sizeContainer()
      .size(285, 75).position(0, 0).multiline($("#id_help_intro_intro"), "main", "left")
      .size(200, 75).position(85, 75).multiline($("#id_help_intro_battleground"), "main", "left")
      .size(76, 69).position(0, 85).place($("#id_help_intro_board"))
      
      .size(285, 205).position(0, 0).container($("#id_help_gems")).sizeContainer()
      .size(102, 124).position(0, 0).place($("#id_help_gems_balance"))
      .size(178, 125).position(107, 0).multiline($("#id_help_gems_trample"), "main", "left")
      .size(129, 79).position(156, 145).place($("#id_help_gems_spell"))
      .size(151, 75).position(0, 145).multiline($("#id_help_gems_match"))
      
      .size(285, 205).position(0, 0).container($("#id_help_spells")).sizeContainer()
      .size(34, 34).position(0, 0).image("block_water").size(246, 30).position(39, 0).multiline($("#id_help_spells_heal"))
      .size(34, 34).position(251, 38).image("block_fire").size(246, 30).position(0, 38).multiline($("#id_help_spells_fireball"))
      .size(34, 34).position(0, 76).image("block_earth").size(246, 30).position(39, 76).multiline($("#id_help_spells_shield"))
      .size(34, 34).position(251, 114).image("block_air").size(246, 30).position(0, 114).multiline($("#id_help_spells_airblast"))
      .size(34, 34).position(0, 152).image("block_void").size(246, 30).position(39, 152).multiline($("#id_help_spells_leech"))
      .size(34, 34).position(251, 190).image("block_aether").size(246, 30).position(0, 190).multiline($("#id_help_spells_spawn"))
      
      .size(285, 205).position(0, 0).container($("#id_help_experience")).sizeContainer()
      .size(70, 105).position(210, 0).place($("#id_help_experience_delay"))
      .size(200, 105).position(0, 0).multiline($("#id_help_experience_place"), "main", "left")
      .size(60, 60).position(0, 120).place($("#id_help_experience_stars"))
      .size(210, 100).position(70, 115).multiline($("#id_help_experience_experience"), "main", "left")
    ;
    
    ////////////////////////////////////////////////////  
    // Rankings.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 284, height: 320, container: $("#id_rankings") }))
      .sizeContainer()
      .size(130, 28).offset(78, 290).button($("#action_rankings_back"), function(e) {
        State.go("welcome.welcome");
      })
      .size(275, 15).position(5, 267).horizontalDivider()
      .size(275, 15).position(5, 100).text($("#id_rankings-label"), "main", "center")
      .size(275, 260).position(5, 5).container($("#id_rankings-names")).sizeContainer()
    ;
    
    ////////////////////////////////////////////////////  
    // Support message.
    (new Widgets.FormBuilder({x: 0, y: 0, width: 284, height: 320, container: $("#id_support") }))
      .sizeContainer()      
      .size(270, 50).offset(12, 0).multiline($("#id_support-label"), "main", "left")
      .size(270, 28).offset(0, 52).entry($("#id_support-name"), 100, "main", "right")
      .offset(0, 30).entry($("#id_support-email"), 100, "main", "right")
      .size(270, 125).offset(0, 30).textarea($("#id_support-message"))
      .size(170, 28).offset(100, 135).button($("#action_support_save"), function(e) {
        State.welcome.support.save();
      })
      .offset(0, 35).button($("#action_support_back"), function(e) {
        State.go("welcome.help");
      })    
    ;
    
    ////////////////////////////////////////////////////  
    // Game screen.
    (new Widgets.FormBuilder({x: -8, y: -8, width: 300, height: 336, container: $("#id_channel")}))
      .sizeContainer()
      .position(275, 7).image("button_menu", function(e) {
        State.go("channel.menu");
      }, "action_play_menu")
      .position(275, 304).image("button_chat", function(e) {
        State.go("channel.chat");
      }, "action_play_chat")
      .position(-7, 258).image("top_3")
      .position(275, 260).image("button_list", function(e) {
        State.go("channel.online");
      }, "action_play_view_all")
      .size(121, 18).position(89, -10).place($("#id_round_timer")).background($("#id_round_timer"), "round_left")
      .size(264, 16).position(4, 8).place($("#id_dashboard"))
      .container($("#id_dashboard"))
      .size(105, 16).position(0, 0).text($("#id_dashboard_nickname"), "tiny-clipped", "left")
      .offset(105, 2).image("heart")
      .size(29, 16).offset(13, -2).text($("#id_dashboard_health"), "tiny-clipped", "left")
      .offset(29, 2).image("shield")
      .size(29, 16).offset(13, -2).text($("#id_dashboard_protection"), "tiny-clipped", "left")
      .offset(29, 2).image("star")
      .size(62, 16).offset(13, -2).text($("#id_dashboard_experience"), "tiny-clipped", "left")
      .size(264, 16).position(4, 22).place($("#id_challenge"))
      .container($("#id_channel"))
      .size(270, 32).position(0, 257).sizeElement($("#id_top"))
      .size(274, 48).position(0, 295).sizeElement($("#id_chat_messages"))
      .visible(false).image("arrow_up", null, "id_pick_arrow_up")
      .image("arrow_down", null, "id_pick_arrow_down")
      .size(129, 94).frame($("#id_pick_all"))         
      .visible(true).position(-8, -8).image("block_water", function(e) {
        State.channel.connected.pickColor(1);
      })
      .offset(35, 0).image("block_fire", function(e) {
        State.channel.connected.pickColor(4);
      })
      .offset(35, 0).image("block_void", function(e) {
        State.channel.connected.pickColor(6);
      })
      .offset(-70, 35).image("block_earth", function(e) {
        State.channel.connected.pickColor(2);
      })
      .offset(35, 0).image("block_air", function(e) {
        State.channel.connected.pickColor(3);
      })
      .offset(35, 0).image("block_aether", function(e) {
        State.channel.connected.pickColor(5);
      })
      .visible(false).size(59, 59).position(0, 0).frame($("#id_pick_trample"))
      .visible(true).position(-8, -8).image("block_water", function(e) {
        State.channel.connected.pickColor();
      }, "id_pick_trample_selector")
      .container($("#id_channel_canvas"))
      .size(315, 210).position(-7, 40).sizeContainer()
      .container($("#id_channel_events"))
      .size(315, 210).position(-7, 40).sizeContainer()      
      .container($("#id_channel_ko"))
      .size(320, 356).position(-18, -18).sizeContainer()      
      .container($("#id_channel_interactive"), function(e) {
        var pt = Widgets.mousePosition(this, e);
        
        State.channel.connected.eventClick(pt);
      })
      .size(315, 210).position(-7, 40).sizeContainer()
    ;
    
    ////////////////////////////////////////////////////  
    // Chat input.
    (new Widgets.FormBuilder({x: 8, y: 250, width: 304, height: 52 }))
      .visible(false).frame($("#id_chat"))
      .visible(true).size(216, 28).offset(-8, -8).input($("#id_chat-message"))
      .offset(225, 2).image("button_chat", function(e) {
        State.channel.chat.send();
      })
      .offset(31, 0).image("button_close", function(e) {
        State.go("channel.connected");
      })
      .visible(false).button($("#action_chat"), function(e) {
        State.channel.chat.send();
      })
    ;
    
    Widgets.Transitions.hideButton($("#action_chat"));
    
    ////////////////////////////////////////////////////  
    // Game menu frame.
    (new Widgets.FormBuilder({x:60, y: 55, width: 200, height: 260}))
      .visible(false).frame($("#id_menu"))      
      .visible(true).size(140, 28).offset(10, 15).button($("#action_menu_resume"), function(e) {
        State.go("channel.connected");
      })
      .offset(0, 40).button($("#action_menu_online"), function(e) {
        State.go("channel.online");
      })
      .offset(0, 138).button($("#action_menu_leave"), function(e) {
        State.go("welcome.launch");
      })
    ;
    
    ////////////////////////////////////////////////////  
    // Game online list.
    (new Widgets.FormBuilder({x:20, y: 3, width: 280, height: 350 }))
      .visible(false).frame($("#id_online"))
      .visible(true).size(140, 28).offset(105, 285).button($("#action_online_resume"), function(e) {
        State.go("channel.connected");
      })
      .size(100, 28).offset(-105, 0).button($("#action_online_experience"), function(e) {
        State.channel.online.sortExperience();
      })
      .docImage($("#action_online_experience"), "star")
      .button($("#action_online_health"), function(e) {
        State.channel.online.sortHealth();
      })
      .docImage($("#action_online_health"), "heart")
    ;
    
    ////////////////////////////////////////////////////  
    // Game menu.
    Widgets.Transitions.hide($("#frame_menu"));
    
    // Initialize streaming services.
    Client.launch();
    
    // Begin game.
    State.go("launch");
  });
});
