Lesterpig/openparty

View on GitHub
lib/rooms.js

Summary

Maintainability
D
2 days
Test Coverage
var utils   = require('./utils.js');
var players = require('./players.js');
var Player  = players.Player;


var Room = function(name, password, gameplay) {

  this.isRoom = true;
  this.id = utils.randomString(20); // TODO make it better...
  this.players = [];
  this.name = name;
  this.size = gameplay.minPlayers;
  this.creationDate = new Date();
  this.password = password;

  this.gameplay = gameplay;
  this.gameplay.room = this; // circular

  // Stages
  this.started = false;
  this.timeout = null;
  this.currentStage = null;

  if(this.gameplay.init) {
    this.gameplay.init(this);
  }

};

/**
 * Broadcast a message to several sockets
 * @param  {String} [channel]
 * @param  {String} event
 * @param  {*}      [data]
 */
Room.prototype.broadcast = function(channel, event, data) {

  if(data === undefined && channel) {
    data = event;
    event = channel;
    channel = '';
  } else if(channel !== '') {
    channel = '_' + channel;
  }
  __app.io.to('room_' + this.id + channel).emit(event, data);
};

/**
 * Send more information about a player.
 * Client-side, it will be displayed in players list.
 *
 * @param  {String} [channel]
 * @param  {Object} player (socket or Player object)
 * @param  {String} value
 */
Room.prototype.playerInfo = function(channel, player, value) {

  if(!value) {
    if(channel instanceof Player) {
      channel = channel.socket;
    }
    this.broadcast('playerInfo', {username: channel.username, value: player});
  } else {
    if(player instanceof Player) {
      player = player.socket;
    }
    this.broadcast(channel, 'playerInfo', {username: player.username, value: value});
  }
};

/**
 * Send a chat message to players (system message)
 * @param  {String} [channel]
 * @param  {String} message
 */
Room.prototype.message = function(channel, message) {
  if(!message) {
    this.broadcast('chatMessage', {message:channel});
  }
  else {
    this.broadcast(channel, 'chatMessage', {message:message});
  }
};

/**
 * Get public information about the room and its players
 * @return {Object}
 */
Room.prototype.getPublicInfo = function() {
  var output = {};

  output.id       = this.id;
  output.isRoom   = true;
  output.name     = this.name;
  output.players  = [];
  output.size     = this.size;
  output.started  = this.started;
  output.password = !!this.password;

  for(var i = 0; i < this.players.length; i++) {
    output.players.push({
      username: this.players[i].username,
      authentication: this.players[i].authentication,
    });
  }

  var parameters = [];
  this.gameplay.parameters.forEach(function(p) {
    parameters.push({
      name      : p.name,
      value     : p.value,
      help      : p.help,
      isBoolean : p.type === Boolean
    });
  });

  output.gameplay = {
    name        : this.gameplay.name,
    description : this.gameplay.description,
    parameters  : parameters,
    maxPlayers  : this.gameplay.maxPlayers,
    minPlayers  : this.gameplay.minPlayers,
    sounds      : this.gameplay.sounds
  };

  return output;

};

/**
 * Set room size
 * @param {Number} size
 */
Room.prototype.setSize = function(size) {
  if(size >= this.gameplay.minPlayers && size <= this.gameplay.maxPlayers) {
    this.size = size;
    sendUpdateRoom(this);
  }
};

/**
 * Set room parameter
 * @param {}       parameter
 * @param {*}      value
 * @param {Socket} socket
 */
Room.prototype.setParameter = function(parameter, value, socket) {
  this.gameplay.parameters.forEach(function(e) {
    if(e.name === parameter) {
      e.value = e.type(value); //deal with it.
      sendUpdateRoom(this, socket);
      return;
    }
  }.bind(this));
};

Room.prototype.sendMessage = function(channel, message, socket) {
  var allowed = false;

  if(channel === 'preChat') {
    if(this.started) {
      return;
    }
    allowed = true;
    channel = '';
  } else {
    if(!this.started) {
      return;
    }
    var channels = socket.player.getWriteChannels();
    allowed = (channels[channel] ? (true === channels[channel].w) : false);
  }

  if(allowed) {
    socket.emit('messageSent');
    if(this.gameplay.processMessage && this.started) {
      message = this.gameplay.processMessage(channel, message, socket.player);
      if(!message) {
        return;
      }
    }
    this.broadcast(channel, 'chatMessage', {message: message, sender: socket.username});
  }
};

Room.prototype.start = function() {
  players.init(this);
  this.gameplay.start(this, function(err) {

    if(err) {
      return this.broadcast('chatMessage', { message: err });
    }

    this.started = true;
    sendUpdateRoom(this);
    this.broadcast('gameStarted');

    if(this.gameplay.firstStage) {
      this.nextStage(this.gameplay.firstStage);
    }

  }.bind(this));
};

/**
 * End the current stage and start another one
 * @param  {String}   stage    The new stage name
 * @param  {Function} [callback]
 */
Room.prototype.nextStage = function(stage, callback) {
  this.currentStage = stage;
  this.gameplay.stages[stage].start(this, function(err, duration) {
    this.setStageDuration(duration);

    // Send actions to players

    this.players.forEach(function(player) {
      player.player.sendAvailableActions();
    });

    if(callback) {
      callback(null);
    }
  }.bind(this));
};

/**
 * End the current stage, without starting another one
 */
Room.prototype.endStage = function() {
  clearTimeout(this.timeout);
  var endFn = this.gameplay.stages[this.currentStage].end;
  if(endFn)
    endFn(this, function() {});
};

/**
 * Change current stage duration
 * @param {Number} duration (sec)
 */
Room.prototype.setStageDuration = function(duration) {
  clearTimeout(this.timeout);

  if(duration < 0) {
    this.broadcast('clearTimer');
    this.currentStageEnd = Infinity;
    return;
  }

  this.currentStageEnd = new Date().getTime() + duration * 1000;
  this.broadcast('setTimer', this.currentStageEnd);
  this.timeout = setTimeout(this.endStage.bind(this), duration * 1000);
};

/**
 * @return Number Milliseconds before next stage. Can be 'Infinity'.
 */
Room.prototype.getRemainingTime = function() {
  return this.currentStageEnd - new Date().getTime();
};

/**
 * Get player object from username
 * @param  {String} username
 * @return {Socket}
 */
Room.prototype.resolveUsername = function(username) {
  for(var i = 0; i < this.players.length; i++) {
    if(this.players[i].username === username) {
      return this.players[i];
    }
  }
  return null;
};

module.exports = {

  rooms: [],

  getRooms: function() {

    var output = [];
    for(var i = 0; i < this.rooms.length; i++) {
      if(!this.rooms[i].started) {
        output.push(this.rooms[i].getPublicInfo());
      }
    }

    return output;
  },

  createRoom: function(name, password, type, socket) {

    if(__conf.maxRooms && this.rooms.length >= __conf.maxRooms) {
      return;
    }

    if(!type || type === 'default') {
      for(var i in __gametypes) {
        type = i;
        break;
      }
    }

    if(!__gametypes[type]) {
      return;
    }

    var gameplay = new __gametypes[type]();

    // Set sounds path
    if(gameplay.sounds) {
      var sounds = [];
      gameplay.sounds.forEach(function(s){
        if(!s.distant)
          sounds.push({
            id   : s.id,
            path : '/' + type + '/' + s.path
          });
        else
          sounds.push(s);
      });
      gameplay.sounds = sounds;
    }

    var room = new Room(name, password, gameplay);

    this.rooms.push(room);
    __app.io.to('lobby').emit('roomCreated', room.getPublicInfo());
    this.joinRoom(room.id, password, socket);
  },

  getRoom: function(id) {

    for(var i = 0; i < this.rooms.length; i++) {
      if(this.rooms[i].id === id) {
        return this.rooms[i];
      }
    }

    return null;

  },

  joinRoom: function(id, password, socket) {

    var room = this.getRoom(id);
    if(!room) {
      return;
    }

    if(room.players.length >= room.size) {
      var d = (new Date()).getTime();
      if(!room.lastFullNotice || room.lastFullNotice < (d - 60*1000)) {
        room.lastFullNotice = d;
        room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-user"></span> <strong>Someone</strong> would like to join this room. Could you add some slots?'});
      }
      return;
    }

    if(room.password && room.password !== password) {
      socket.emit('invalidRoomPassword');
      return;
    }

    if(room.players.indexOf(socket) < 0) {
      room.players.push(socket);
    }

    sendUpdateRoom(room);
    socket.emit('roomJoined', room.getPublicInfo());
    socket.join('room_' + id);
    socket.currentRoom = room;

    room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-ok-circle"></span> <strong>' + socket.username + '</strong> has joined the room'});

  },

  leaveRoom: function(socket) {

    var room = socket.currentRoom;
    if(!room) {
      return;
    }

    var i = room.players.indexOf(socket);
    if(i < 0) {
      return;
    }

    room.players.splice(i, 1);

    if(room.gameplay.onDisconnect && socket.player) {
      room.gameplay.onDisconnect(room, socket.player);
    }

    if(!room.started && socket.username) {
      room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-remove-circle"></span> <strong>' + socket.username + '</strong> has left the room'});
    }

    if(room)

    if(room.players.length === 0) {
      this.removeRoom(room);
    } else {
      sendUpdateRoom(room);
    }

    socket.player = null;
    socket.currentRoom = null;

    // Leave concerned socket rooms
    // Buffered, because socket.rooms is dynamically updated by socket.io
    var regex  = /^room\_/;
    var buffer = [];
    for(var r in socket.rooms) {
      if(regex.test(socket.rooms[r])) {
        buffer.push(r);
      }
    }

    try {
      buffer.forEach(function(r) {
        socket.leave(r);
      });
      socket.emit('roomLeft');
    } catch(e) {}

  },

  removeRoom: function(room) {

    for(var i = 0; i < this.rooms.length; i++) {
      if(this.rooms[i].id === room.id) {
        clearTimeout(this.rooms[i].timeout);
        this.rooms.splice(i, 1);
        __app.io.to('lobby').emit('roomRemoved', room.id);
        return;
      }
    }

  }

};

/** PRIVATE FUNCTIONS **/

function sendUpdateRoom(room, socket) {
  if(!socket) {
    __app.io.to('lobby').emit('roomUpdated', room.getPublicInfo());
  }
  else {
    socket.broadcast.to('lobby').emit('roomUpdated', room.getPublicInfo());
  }
}