<commit_before>/*
  Note that the main run() loop will be executed once per frame as specified by WORLD_UPDATE_INTERVAL in worker.js.
  Behind the scenes, the engine just keeps on building up a cellData tree of all different
  state objects that are present within our current grid cell.
  The tree is a simple JSON object and needs to be in the format:

    {
      // player is a state type.
      player: {
        // ...
      },
      // You can add other custom state types.
      someType: {
        // Use the id as the key (in the place of someId).
        // It is recommended that you use a random uuid as the state id. See https://www.npmjs.com/package/uuid
        someId: {
          // All properties listed here are required.
          // You can add additional ones.
          id: someId,
          type: someType,
          x: someXCoordinate,
          y: someYCoordinate,
        },
        anotherId: {
          // ...
        }
      }
    }

  You can add new type subtrees, new states and new properties to the cellData
  as you like. So long as you follow the structure above, the items will show
  up on the front end in the relevant cell in our world (see the handleCellData function in index.html).

  Adding new items to the cell is easy.
  For example, to add a new coin to the cell, you just need to add a state object to the coin subtree (E.g):
  cellData.coin[coin.id] = coin;

  See how coinManager.addCoin is used below (and how it's implemented) for more details.

  Note that states which are close to our current cell (based on WORLD_CELL_OVERLAP_DISTANCE)
  but not exactly inside it will still be visible within this cell (they will have an additional
  'external' property set to true).

  External states should not be modified because they belong to a different cell and the change will be ignored.
*/

var _ = require('lodash');
var rbush = require('rbush');
var SAT = require('sat');
var config = require('./config');
var BotManager = require('./bot-manager').BotManager;
var CoinManager = require('./coin-manager').CoinManager;

// This controller will be instantiated once for each
// cell in our world grid.

var CellController = function (options, util) {
  var self = this;

  this.options = options;
  this.cellIndex = options.cellIndex;
  this.util = util;

  // You can use the exchange object to publish data to global channels which you
  // can watch on the front end (in index.html).
  // The API for the exchange object is here: http://socketcluster.io/#!/docs/api-exchange
  // For info on how to watch channels on the front end, read about subscribe and watch
  // functions here: http://socketcluster.io/#!/docs/basic-usage
  this.exchange = options.worker.exchange;

  this.worldColCount = Math.ceil(config.WORLD_WIDTH / config.WORLD_CELL_WIDTH);
  this.worldRowCount = Math.ceil(config.WORLD_HEIGHT / config.WORLD_CELL_HEIGHT);
  this.worldCellCount = this.worldColCount * this.worldRowCount;
  this.workerCount = options.worker.options.workers;

  this.coinMaxCount = Math.round(config.COIN_MAX_COUNT / this.worldCellCount);
  this.coinDropInterval = config.COIN_DROP_INTERVAL * this.worldCellCount;
  this.botCount = Math.round(config.BOT_COUNT / this.worldCellCount);

  var cellData = options.cellData;

  this.botManager = new BotManager({
    worldWidth: config.WORLD_WIDTH,
    worldHeight: config.WORLD_HEIGHT,
    botDefaultDiameter: config.BOT_DEFAULT_DIAMETER,
    botMoveSpeed: config.BOT_MOVE_SPEED,
    botMass: config.BOT_MASS,
    botChangeDirectionProbability: config.BOT_CHANGE_DIRECTION_PROBABILITY
  });

  if (!cellData.player) {
    cellData.player = {};
  }

  for (var b = 0; b < this.botCount; b++) {
    var bot = this.botManager.addBot();
    cellData.player[bot.id] = bot;
  }

  this.botMoves = [
    {u: 1},
    {d: 1},
    {r: 1},
    {l: 1}
  ];

  this.coinManager = new CoinManager({
    cellData: options.cellData,
    cellBounds: options.cellBounds,
    playerNoDropRadius: config.COIN_PLAYER_NO_DROP_RADIUS,
    coinMaxCount: this.coinMaxCount,
    coinDropInterval: this.coinDropInterval
  });

  this.lastCoinDrop = 0;

  config.COIN_TYPES.sort(function (a, b) {
    if (a.probability < b.probability) {
      return -1;
    }
    if (a.probability > b.probability) {
      return 1;
    }
    return 0;
  });

  this.coinTypes = [];
  var probRangeStart = 0;
  config.COIN_TYPES.forEach(function (coinType) {
    var coinTypeClone = _.cloneDeep(coinType);
    coinTypeClone.prob = probRangeStart;
    self.coinTypes.push(coinTypeClone);
    probRangeStart += coinType.probability;
  });

  this.playerCompareFn = function (a, b) {
    if (a.id < b.id) {
      return -1;
    }
    if (a.id > b.id) {
      return 1;
    }
    return 0;
  };

  this.diagonalSpeedFactor = Math.sqrt(1 / 2);
};

/*
  The main run loop for our cell controller.
*/
CellController.prototype.run = function (cellData) {
  if (!cellData.player) {
    cellData.player = {};
  }
  if (!cellData.coin) {
    cellData.coin = {};
  }
  var players = cellData.player;
  var coins = cellData.coin;

  // Sorting is important to achieve consistency across cells.
  var playerIds = Object.keys(players).sort(this.playerCompareFn);

  this.findPlayerOverlaps(playerIds, players, coins);
  this.dropCoins(coins);
  this.generateBotOps(playerIds, players);
  this.applyPlayerOps(playerIds, players, coins);
};

CellController.prototype.dropCoins = function (coins) {
  var now = Date.now();

  if (now - this.lastCoinDrop >= this.coinManager.coinDropInterval &&
    this.coinManager.coinCount < this.coinManager.coinMaxCount) {

    this.lastCoinDrop = now;

    var rand = Math.random();
    var chosenCoinType;

    var numTypes = this.coinTypes.length;
    for (var i = numTypes - 1; i >= 0; i--) {
      var curCoinType = this.coinTypes[i];
      if (rand >= curCoinType.prob) {
        chosenCoinType = curCoinType;
        break;
      }
    }

    if (!chosenCoinType) {
      throw new Error('There is something wrong with the coin probability distribution. ' +
        'Check that probabilities add up to 1 in COIN_TYPES config option.');
    }

    var coin = this.coinManager.addCoin(chosenCoinType.value, chosenCoinType.type, chosenCoinType.radius);
    if (coin) {
      coins[coin.id] = coin;
    }
  }
};

CellController.prototype.generateBotOps = function (playerIds, players, coins) {
  var self = this;

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    // States which are external are managed by a different cell, therefore changes made to these
    // states are not saved unless they are grouped with one or more internal states from the current cell.
    // See util.groupStates() method near the bottom of this file for details.
    if (player.subtype == 'bot' && !player.external) {
      var radius = Math.round(player.diam / 2);
      var isBotOnEdge = player.x <= radius || player.x >= config.WORLD_WIDTH - radius ||
        player.y <= radius || player.y >= config.WORLD_HEIGHT - radius;

      if (Math.random() <= player.changeDirProb || isBotOnEdge) {
        var randIndex = Math.floor(Math.random() * self.botMoves.length);
        player.repeatOp = self.botMoves[randIndex];
      }
      if (player.repeatOp) {
        player.op = player.repeatOp;
      }
    }
  });
};

CellController.prototype.keepPlayerOnGrid = function (player) {
  var radius = Math.round(player.diam / 2);

  var leftX = player.x - radius;
  var rightX = player.x + radius;
  var topY = player.y - radius;
  var bottomY = player.y + radius;

  if (leftX < 0) {
    player.x = radius;
  } else if (rightX > config.WORLD_WIDTH) {
    player.x = config.WORLD_WIDTH - radius;
  }
  if (topY < 0) {
    player.y = radius;
  } else if (bottomY > config.WORLD_HEIGHT) {
    player.y = config.WORLD_HEIGHT - radius;
  }
};

CellController.prototype.applyPlayerOps = function (playerIds, players, coins) {
  var self = this;

  playerIds.forEach(function (playerId) {
    var player = players[playerId];

    var playerOp = player.op;
    var moveSpeed;
    if (player.subtype == 'bot') {
      moveSpeed = player.speed;
    } else {
      moveSpeed = config.PLAYER_DEFAULT_MOVE_SPEED;
    }

    if (playerOp) {
      var movementVector = {x: 0, y: 0};
      var movedHorizontally = false;
      var movedVertically = false;

      if (playerOp.u) {
        movementVector.y = -moveSpeed;
        player.direction = 'up';
        movedVertically = true;
      }
      if (playerOp.d) {
        movementVector.y = moveSpeed;
        player.direction = 'down';
        movedVertically = true;
      }
      if (playerOp.r) {
        movementVector.x = moveSpeed;
        player.direction = 'right';
        movedHorizontally = true;
      }
      if (playerOp.l) {
        movementVector.x = -moveSpeed;
        player.direction = 'left';
        movedHorizontally = true;
      }

      if (movedHorizontally && movedVertically) {
        movementVector.x *= self.diagonalSpeedFactor;
        movementVector.y *= self.diagonalSpeedFactor;
      }

      player.x += movementVector.x;
      player.y += movementVector.y;
    }

    if (player.playerOverlaps) {
      player.playerOverlaps.forEach(function (otherPlayer) {
        self.resolvePlayerCollision(player, otherPlayer);
        self.keepPlayerOnGrid(otherPlayer);
      });
      delete player.playerOverlaps;
    }

    if (player.coinOverlaps) {
      player.coinOverlaps.forEach(function (coin) {
        if (self.testCircleCollision(player, coin).collided) {
          player.score += coin.v;
          self.coinManager.removeCoin(coin.id);
        }
      });
      delete player.coinOverlaps;
    }

    self.keepPlayerOnGrid(player);
  });
};

CellController.prototype.findPlayerOverlaps = function (playerIds, players, coins) {
  var self = this;

  var playerTree = new rbush();
  var hitAreaList = [];

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    player.hitArea = self.generateHitArea(player);
    hitAreaList.push(player.hitArea);
  });

  playerTree.load(hitAreaList);

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    playerTree.remove(player.hitArea);
    var hitList = playerTree.search(player.hitArea);
    playerTree.insert(player.hitArea);

    hitList.forEach(function (hit) {
      if (!player.playerOverlaps) {
        player.playerOverlaps = [];
      }
      player.playerOverlaps.push(hit.target);
    });
  });

  var coinIds = Object.keys(coins);
  coinIds.forEach(function (coinId) {
    var coin = coins[coinId];
    var coinHitArea = self.generateHitArea(coin);
    var hitList = playerTree.search(coinHitArea);

    if (hitList.length) {
      // If multiple players hit the coin, give it to a random one.
      var randomIndex = Math.floor(Math.random() * hitList.length);
      var coinWinner = hitList[randomIndex].target;

      if (!coinWinner.coinOverlaps) {
        coinWinner.coinOverlaps = [];
      }
      coinWinner.coinOverlaps.push(coin);
    }
  });

  playerIds.forEach(function (playerId) {
    delete players[playerId].hitArea;
  });
};

CellController.prototype.generateHitArea = function (target) {
  var targetRadius = target.r || Math.round(target.diam / 2);
  return {
    target: target,
    minX: target.x - targetRadius,
    minY: target.y - targetRadius,
    maxX: target.x + targetRadius,
    maxY: target.y + targetRadius
  };
};

CellController.prototype.testCircleCollision = function (a, b) {
  var radiusA = a.r || Math.round(a.diam / 2);
  var radiusB = b.r || Math.round(b.diam / 2);

  var circleA = new SAT.Circle(new SAT.Vector(a.x, a.y), radiusA);
  var circleB = new SAT.Circle(new SAT.Vector(b.x, b.y), radiusB);

  var response = new SAT.Response();
  var collided = SAT.testCircleCircle(circleA, circleB, response);

  return {
    collided: collided,
    overlapV: response.overlapV
  };
};

CellController.prototype.resolvePlayerCollision = function (player, otherPlayer) {
  var result = this.testCircleCollision(player, otherPlayer);

  if (result.collided) {
    var olv = result.overlapV;

    var totalMass = player.mass + otherPlayer.mass;
    var playerBuff = player.mass / totalMass;
    var otherPlayerBuff = otherPlayer.mass / totalMass;

    player.x -= olv.x * otherPlayerBuff;
    player.y -= olv.y * otherPlayerBuff;
    otherPlayer.x += olv.x * playerBuff;
    otherPlayer.y += olv.y * playerBuff;

    /*
      Whenever we have one state affecting the (x, y) coordinates of
      another state, we should group them together using the util.groupStates() function.
      Otherwise we will may get flicker when the two states interact across
      a cell boundary.
      In this case, if we don't use groupStates(), there will be flickering when you
      try to push another player across to a different cell.
    */
    this.util.groupStates([player, otherPlayer]);
  }
};

module.exports = CellController;
<commit_msg>Improved comment<commit_after>/*
  Note that the main run() loop will be executed once per frame as specified by WORLD_UPDATE_INTERVAL in worker.js.
  Behind the scenes, the engine just keeps on building up a cellData tree of all different
  state objects that are present within our current grid cell.
  The tree is a simple JSON object and needs to be in the format:

    {
      // player is a state type.
      player: {
        // ...
      },
      // You can add other custom state types.
      someType: {
        // Use the id as the key (in the place of someId).
        // It is recommended that you use a random uuid as the state id. See https://www.npmjs.com/package/uuid
        someId: {
          // All properties listed here are required.
          // You can add additional ones.
          id: someId,
          type: someType,
          x: someXCoordinate,
          y: someYCoordinate,
        },
        anotherId: {
          // ...
        }
      }
    }

  You can add new type subtrees, new states and new properties to the cellData
  as you like. So long as you follow the structure above, the items will show
  up on the front end in the relevant cell in our world (see the handleCellData function in index.html).

  Adding new items to the cell is easy.
  For example, to add a new coin to the cell, you just need to add a state object to the coin subtree (E.g):
  cellData.coin[coin.id] = coin;

  See how coinManager.addCoin is used below (and how it's implemented) for more details.

  Note that states which are close to our current cell (based on WORLD_CELL_OVERLAP_DISTANCE)
  but not exactly inside it will still be visible within this cell (they will have an additional
  'external' property set to true).

  External states should not be modified because they belong to a different cell and the change will be ignored.
*/

var _ = require('lodash');
var rbush = require('rbush');
var SAT = require('sat');
var config = require('./config');
var BotManager = require('./bot-manager').BotManager;
var CoinManager = require('./coin-manager').CoinManager;

// This controller will be instantiated once for each
// cell in our world grid.

var CellController = function (options, util) {
  var self = this;

  this.options = options;
  this.cellIndex = options.cellIndex;
  this.util = util;

  // You can use the exchange object to publish data to global channels which you
  // can watch on the front end (in index.html).
  // The API for the exchange object is here: http://socketcluster.io/#!/docs/api-exchange
  // To receive channel data on the front end, you can read about the subscribe and watch
  // functions here: http://socketcluster.io/#!/docs/basic-usage
  this.exchange = options.worker.exchange;

  this.worldColCount = Math.ceil(config.WORLD_WIDTH / config.WORLD_CELL_WIDTH);
  this.worldRowCount = Math.ceil(config.WORLD_HEIGHT / config.WORLD_CELL_HEIGHT);
  this.worldCellCount = this.worldColCount * this.worldRowCount;
  this.workerCount = options.worker.options.workers;

  this.coinMaxCount = Math.round(config.COIN_MAX_COUNT / this.worldCellCount);
  this.coinDropInterval = config.COIN_DROP_INTERVAL * this.worldCellCount;
  this.botCount = Math.round(config.BOT_COUNT / this.worldCellCount);

  var cellData = options.cellData;

  this.botManager = new BotManager({
    worldWidth: config.WORLD_WIDTH,
    worldHeight: config.WORLD_HEIGHT,
    botDefaultDiameter: config.BOT_DEFAULT_DIAMETER,
    botMoveSpeed: config.BOT_MOVE_SPEED,
    botMass: config.BOT_MASS,
    botChangeDirectionProbability: config.BOT_CHANGE_DIRECTION_PROBABILITY
  });

  if (!cellData.player) {
    cellData.player = {};
  }

  for (var b = 0; b < this.botCount; b++) {
    var bot = this.botManager.addBot();
    cellData.player[bot.id] = bot;
  }

  this.botMoves = [
    {u: 1},
    {d: 1},
    {r: 1},
    {l: 1}
  ];

  this.coinManager = new CoinManager({
    cellData: options.cellData,
    cellBounds: options.cellBounds,
    playerNoDropRadius: config.COIN_PLAYER_NO_DROP_RADIUS,
    coinMaxCount: this.coinMaxCount,
    coinDropInterval: this.coinDropInterval
  });

  this.lastCoinDrop = 0;

  config.COIN_TYPES.sort(function (a, b) {
    if (a.probability < b.probability) {
      return -1;
    }
    if (a.probability > b.probability) {
      return 1;
    }
    return 0;
  });

  this.coinTypes = [];
  var probRangeStart = 0;
  config.COIN_TYPES.forEach(function (coinType) {
    var coinTypeClone = _.cloneDeep(coinType);
    coinTypeClone.prob = probRangeStart;
    self.coinTypes.push(coinTypeClone);
    probRangeStart += coinType.probability;
  });

  this.playerCompareFn = function (a, b) {
    if (a.id < b.id) {
      return -1;
    }
    if (a.id > b.id) {
      return 1;
    }
    return 0;
  };

  this.diagonalSpeedFactor = Math.sqrt(1 / 2);
};

/*
  The main run loop for our cell controller.
*/
CellController.prototype.run = function (cellData) {
  if (!cellData.player) {
    cellData.player = {};
  }
  if (!cellData.coin) {
    cellData.coin = {};
  }
  var players = cellData.player;
  var coins = cellData.coin;

  // Sorting is important to achieve consistency across cells.
  var playerIds = Object.keys(players).sort(this.playerCompareFn);

  this.findPlayerOverlaps(playerIds, players, coins);
  this.dropCoins(coins);
  this.generateBotOps(playerIds, players);
  this.applyPlayerOps(playerIds, players, coins);
};

CellController.prototype.dropCoins = function (coins) {
  var now = Date.now();

  if (now - this.lastCoinDrop >= this.coinManager.coinDropInterval &&
    this.coinManager.coinCount < this.coinManager.coinMaxCount) {

    this.lastCoinDrop = now;

    var rand = Math.random();
    var chosenCoinType;

    var numTypes = this.coinTypes.length;
    for (var i = numTypes - 1; i >= 0; i--) {
      var curCoinType = this.coinTypes[i];
      if (rand >= curCoinType.prob) {
        chosenCoinType = curCoinType;
        break;
      }
    }

    if (!chosenCoinType) {
      throw new Error('There is something wrong with the coin probability distribution. ' +
        'Check that probabilities add up to 1 in COIN_TYPES config option.');
    }

    var coin = this.coinManager.addCoin(chosenCoinType.value, chosenCoinType.type, chosenCoinType.radius);
    if (coin) {
      coins[coin.id] = coin;
    }
  }
};

CellController.prototype.generateBotOps = function (playerIds, players, coins) {
  var self = this;

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    // States which are external are managed by a different cell, therefore changes made to these
    // states are not saved unless they are grouped with one or more internal states from the current cell.
    // See util.groupStates() method near the bottom of this file for details.
    if (player.subtype == 'bot' && !player.external) {
      var radius = Math.round(player.diam / 2);
      var isBotOnEdge = player.x <= radius || player.x >= config.WORLD_WIDTH - radius ||
        player.y <= radius || player.y >= config.WORLD_HEIGHT - radius;

      if (Math.random() <= player.changeDirProb || isBotOnEdge) {
        var randIndex = Math.floor(Math.random() * self.botMoves.length);
        player.repeatOp = self.botMoves[randIndex];
      }
      if (player.repeatOp) {
        player.op = player.repeatOp;
      }
    }
  });
};

CellController.prototype.keepPlayerOnGrid = function (player) {
  var radius = Math.round(player.diam / 2);

  var leftX = player.x - radius;
  var rightX = player.x + radius;
  var topY = player.y - radius;
  var bottomY = player.y + radius;

  if (leftX < 0) {
    player.x = radius;
  } else if (rightX > config.WORLD_WIDTH) {
    player.x = config.WORLD_WIDTH - radius;
  }
  if (topY < 0) {
    player.y = radius;
  } else if (bottomY > config.WORLD_HEIGHT) {
    player.y = config.WORLD_HEIGHT - radius;
  }
};

CellController.prototype.applyPlayerOps = function (playerIds, players, coins) {
  var self = this;

  playerIds.forEach(function (playerId) {
    var player = players[playerId];

    var playerOp = player.op;
    var moveSpeed;
    if (player.subtype == 'bot') {
      moveSpeed = player.speed;
    } else {
      moveSpeed = config.PLAYER_DEFAULT_MOVE_SPEED;
    }

    if (playerOp) {
      var movementVector = {x: 0, y: 0};
      var movedHorizontally = false;
      var movedVertically = false;

      if (playerOp.u) {
        movementVector.y = -moveSpeed;
        player.direction = 'up';
        movedVertically = true;
      }
      if (playerOp.d) {
        movementVector.y = moveSpeed;
        player.direction = 'down';
        movedVertically = true;
      }
      if (playerOp.r) {
        movementVector.x = moveSpeed;
        player.direction = 'right';
        movedHorizontally = true;
      }
      if (playerOp.l) {
        movementVector.x = -moveSpeed;
        player.direction = 'left';
        movedHorizontally = true;
      }

      if (movedHorizontally && movedVertically) {
        movementVector.x *= self.diagonalSpeedFactor;
        movementVector.y *= self.diagonalSpeedFactor;
      }

      player.x += movementVector.x;
      player.y += movementVector.y;
    }

    if (player.playerOverlaps) {
      player.playerOverlaps.forEach(function (otherPlayer) {
        self.resolvePlayerCollision(player, otherPlayer);
        self.keepPlayerOnGrid(otherPlayer);
      });
      delete player.playerOverlaps;
    }

    if (player.coinOverlaps) {
      player.coinOverlaps.forEach(function (coin) {
        if (self.testCircleCollision(player, coin).collided) {
          player.score += coin.v;
          self.coinManager.removeCoin(coin.id);
        }
      });
      delete player.coinOverlaps;
    }

    self.keepPlayerOnGrid(player);
  });
};

CellController.prototype.findPlayerOverlaps = function (playerIds, players, coins) {
  var self = this;

  var playerTree = new rbush();
  var hitAreaList = [];

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    player.hitArea = self.generateHitArea(player);
    hitAreaList.push(player.hitArea);
  });

  playerTree.load(hitAreaList);

  playerIds.forEach(function (playerId) {
    var player = players[playerId];
    playerTree.remove(player.hitArea);
    var hitList = playerTree.search(player.hitArea);
    playerTree.insert(player.hitArea);

    hitList.forEach(function (hit) {
      if (!player.playerOverlaps) {
        player.playerOverlaps = [];
      }
      player.playerOverlaps.push(hit.target);
    });
  });

  var coinIds = Object.keys(coins);
  coinIds.forEach(function (coinId) {
    var coin = coins[coinId];
    var coinHitArea = self.generateHitArea(coin);
    var hitList = playerTree.search(coinHitArea);

    if (hitList.length) {
      // If multiple players hit the coin, give it to a random one.
      var randomIndex = Math.floor(Math.random() * hitList.length);
      var coinWinner = hitList[randomIndex].target;

      if (!coinWinner.coinOverlaps) {
        coinWinner.coinOverlaps = [];
      }
      coinWinner.coinOverlaps.push(coin);
    }
  });

  playerIds.forEach(function (playerId) {
    delete players[playerId].hitArea;
  });
};

CellController.prototype.generateHitArea = function (target) {
  var targetRadius = target.r || Math.round(target.diam / 2);
  return {
    target: target,
    minX: target.x - targetRadius,
    minY: target.y - targetRadius,
    maxX: target.x + targetRadius,
    maxY: target.y + targetRadius
  };
};

CellController.prototype.testCircleCollision = function (a, b) {
  var radiusA = a.r || Math.round(a.diam / 2);
  var radiusB = b.r || Math.round(b.diam / 2);

  var circleA = new SAT.Circle(new SAT.Vector(a.x, a.y), radiusA);
  var circleB = new SAT.Circle(new SAT.Vector(b.x, b.y), radiusB);

  var response = new SAT.Response();
  var collided = SAT.testCircleCircle(circleA, circleB, response);

  return {
    collided: collided,
    overlapV: response.overlapV
  };
};

CellController.prototype.resolvePlayerCollision = function (player, otherPlayer) {
  var result = this.testCircleCollision(player, otherPlayer);

  if (result.collided) {
    var olv = result.overlapV;

    var totalMass = player.mass + otherPlayer.mass;
    var playerBuff = player.mass / totalMass;
    var otherPlayerBuff = otherPlayer.mass / totalMass;

    player.x -= olv.x * otherPlayerBuff;
    player.y -= olv.y * otherPlayerBuff;
    otherPlayer.x += olv.x * playerBuff;
    otherPlayer.y += olv.y * playerBuff;

    /*
      Whenever we have one state affecting the (x, y) coordinates of
      another state, we should group them together using the util.groupStates() function.
      Otherwise we will may get flicker when the two states interact across
      a cell boundary.
      In this case, if we don't use groupStates(), there will be flickering when you
      try to push another player across to a different cell.
    */
    this.util.groupStates([player, otherPlayer]);
  }
};

module.exports = CellController;
<|endoftext|>


<commit_before>//      CLog.js 1.1.0
//      https://github.com/CVarisco/CLog.js
//      http://www.christianvarisco.com
//      Copyright (c) 2016 Christian Varisco

(function (w) {

    // Creates the CLog object function
    var CLog = function (options) {

        //On input it assigns a custom color, otherwise it sets the default color
        this.options = {
            group: options.group || null,
            colors: options.colors || null
        };

        this.options.colors.default = "#000000";
        /*
         Here You can write another options
         */

    };

    // Returns an array where argument[0] is always a color.
    // Color is set based on input (id or hex) or is set to default (black)
    var _normalizeArguments = function (args) {

        var colors = this.options.colors; // Get colors from options
        var color = ''; // Init color
        var argumentsArray = [].slice.apply(args); // Convert arguments into an Array

        // Search in options.colors or set color
        if (!!colors && typeof argumentsArray[0] === "string") {
            if (argumentsArray[0][0] === "#") {
                color = argumentsArray[0];
            } else {
                for (var c in colors) {
                    if (argumentsArray[0] === c) color = colors[argumentsArray[0]];
                }
            }
        }

        //If color in the arguments doesn't exist, unshift the default color
        if (color === '') argumentsArray.unshift(this.options.colors.default);
        else argumentsArray[0] = color;

        return argumentsArray;

    };

    // Formats the output
    var _createOutput = function (args) {
        var output = '%c ';
        var outputArgs = [];
        var isGrouped = this.options.group;

        for (var i = 1; i < args.length; i++) {
            switch (typeof args[i]) {
                case 'string':
                case 'number':
                case 'boolean':
                    output += args[i] + ' ';
                    break;
                case 'undefined':
                    output += "'undefined' ";
                    break;
                case 'object':
                case 'array':
                case 'function':
                    outputArgs.push(args[i]);
                    output += (isGrouped) ? '' : '(%O) ';
                    break;
                default:
                    output += args[i] + ' ';
                    break;
            }

        }

        //If group isn't active, slice the arguments output
        if (!isGrouped) {
            outputArgs.splice(0, 0, output);
            outputArgs.splice(1, 0, "color:" + args[0] + ";");
        }

        return {
            color: args[0],
            message: output,
            args: outputArgs
        }
    };

    // Prints to console
    var _print = function (output) {
        var isGrouped = this.options.group;

        //If group is active, create group in console
        if (isGrouped) {
            console.groupCollapsed(output.message, "color:" + output.color + ";");

            output.args.forEach(function (arg) {
                console.dir(arg);
            });

            console.groupEnd();
        } else {
            console.log.apply(console, output.args);
        }
    };

    //Here's comes the Magic!
    CLog.prototype.log = function () {

        // Check if Browser is Chrome
        if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
            var input = _normalizeArguments(arguments); //Returns a normalized array (with [0] being a color)
            var output = _createOutput(input);
            _print(output);

        } else {
            console.log.apply(console, arguments);
        }

    };

    //Clear console if called
    CLog.prototype.clear = function () {
        console.clear();
    };

    w.CLog = CLog;

}(window));

var options = {
    group: false,            // The console.group mode of Google Chrome is set to false by default
    colors: {               // Define the color. (Default color: Black)
        log: "#31f095",
        home: "#e86024",
        error: "#e22f2f",
        network: "#08bce9"
    }
};

var CLog = new CLog(options);

CLog.log('log', 'String log', 12345, 'Another String', {id: '1234'}, ['a', 'b', 'c'], true, false);
CLog.log('home', 'String log', 12345, 'Another String', {id: '1234'}, ['a', 'b', 'c'], true, false);
CLog.log('network', 'String log', 12345, 'Another String', {id: '1234'}, ['a', 'b', 'c'], true, false);<commit_msg>init support to UMD<commit_after>//      CLog.js 1.1.0
//      https://github.com/CVarisco/CLog.js
//      http://www.christianvarisco.com
//      Copyright (c) 2016 Christian Varisco

(function (w) {

    // Creates the CLog object function
    var CLog = function (options) {

        //On input it assigns a custom color, otherwise it sets the default color
        this.options = {
            group: options.group || null,
            colors: options.colors || null
        };

        this.options.colors.default = "#000000";
        /*
         Here You can write another options
         */

    };

    // Returns an array where argument[0] is always a color.
    // Color is set based on input (id or hex) or is set to default (black)
    var _normalizeArguments = function (args) {

        var colors = this.options.colors; // Get colors from options
        var color = ''; // Init color
        var argumentsArray = [].slice.apply(args); // Convert arguments into an Array

        // Search in options.colors or set color
        if (!!colors && typeof argumentsArray[0] === "string") {
            if (argumentsArray[0][0] === "#") {
                color = argumentsArray[0];
            } else {
                for (var c in colors) {
                    if (argumentsArray[0] === c) color = colors[argumentsArray[0]];
                }
            }
        }

        //If color in the arguments doesn't exist, unshift the default color
        if (color === '') argumentsArray.unshift(this.options.colors.default);
        else argumentsArray[0] = color;

        return argumentsArray;

    };

    // Formats the output
    var _createOutput = function (args) {
        var output = '%c ';
        var outputArgs = [];
        var isGrouped = this.options.group;

        for (var i = 1; i < args.length; i++) {
            switch (typeof args[i]) {
                case 'string':
                case 'number':
                case 'boolean':
                    output += args[i] + ' ';
                    break;
                case 'undefined':
                    output += "'undefined' ";
                    break;
                case 'object':
                case 'array':
                case 'function':
                    outputArgs.push(args[i]);
                    output += (isGrouped) ? '' : '(%O) ';
                    break;
                default:
                    output += args[i] + ' ';
                    break;
            }

        }

        //If group isn't active, slice the arguments output
        if (!isGrouped) {
            outputArgs.splice(0, 0, output);
            outputArgs.splice(1, 0, "color:" + args[0] + ";");
        }

        return {
            color: args[0],
            message: output,
            args: outputArgs
        }
    };

    // Prints to console
    var _print = function (output) {
        var isGrouped = this.options.group;

        //If group is active, create group in console
        if (isGrouped) {
            console.groupCollapsed(output.message, "color:" + output.color + ";");

            output.args.forEach(function (arg) {
                console.dir(arg);
            });

            console.groupEnd();
        } else {
            console.log.apply(console, output.args);
        }
    };

    //Here's comes the Magic!
    CLog.prototype.log = function () {

        // Check if Browser is Chrome
        if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
            var input = _normalizeArguments(arguments); //Returns a normalized array (with [0] being a color)
            var output = _createOutput(input);
            _print(output);

        } else {
            console.log.apply(console, arguments);
        }

    };

    //Clear console if called
    CLog.prototype.clear = function () {
        console.clear();
    };

    // Compatibility for the UMD API. If we’re in the browser, add CLog as a global object.
    if (typeof define === 'function' && define.amd) {
        define('CLog', [], function() {
            return CLog;
        });
    }else{
        w.CLog = CLog;
    }

}(this));
<|endoftext|>


<commit_before>(function(global) {
  'use strict';

  var helper = {};

  helper.inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    return ctor;
  };

  helper.diffObj = function(newObj, oldObj) {
    var diff = {};
    for (var key in newObj) {
      if (newObj[key] !== oldObj[key])
        diff[key] = newObj[key];
    }
    return diff;
  };

  var dom = {};

  dom.el = function(selector) {
    if (selector[0] === '<') {
      selector = selector.match(/<(.+)>/)[1];
      return document.createElement(selector);
    }
  };

  dom.attr = function(el, props) {
    for (var prop in props) {
      el.setAttribute(prop, props[prop]);
    }
  };

  dom.css = function(el, props) {
    var style = el.style;
    for (var prop in props) {
      style[prop] = props[prop];
    }
  };

  dom.append = function(parent, el) {
    parent.appendChild(el);
  };

  dom.remove = function(el) {
    el.parentNode.removeChild(el);
  };

  dom.text = function(el, s) {
    el.textContent = s;
  };

  dom.html = function(el, s) {
    el.innerHTML = s;
  };

  dom.animate = function(callback) {
    return window.requestAnimationFrame(callback);
  };

  var Component = function() {};

  Component.prototype.prop = function(initialValue) {
    var cache = initialValue;
    return function(value) {
      if (typeof value === 'undefined')
        return cache;
      cache = value;
      this.markDirty();
    };
  };

  Component.prototype.markDirty = (function() {
    var dirtyComponents = [];
    var requestId = null;
    var callback = function() {
      dirtyComponents.forEach(function(component) {
        component.redraw();
      });
      dirtyComponents = [];
      requestId = null;
    };
    return function() {
      if (typeof document === 'undefined')
        return;
      if (dirtyComponents.indexOf(this) === -1)
        dirtyComponents.push(this);
      if (requestId !== null)
        return;
      requestId = dom.animate(callback);
    };
  })();

  var Node = helper.inherits(function(option) {
    this.content = this.prop(option.content || '');
    this.contentType = this.prop(option.contentType || Node.CONTENT_TYPE_TEXT);
    this.x = this.prop(option.x || 0);
    this.y = this.prop(option.y || 0);
    this.width = this.prop(option.width || 75);
    this.height = this.prop(option.height || 30);
    this.backgroundColor = this.prop(option.backgroundColor || '#a7cbe6');
    this.borderColor = this.prop(option.borderColor || '#333');
    this.borderWidth = this.prop(option.borderWidth || 2);
    this.textColor = this.prop(option.textColor || '#333');
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
    this.cache = this.prop({});
  }, Component);

  Node.prototype.text = function(text) {
    if (typeof text === 'undefined')
      return this.content();
    this.content(text);
    this.contentType(Node.CONTENT_TYPE_TEXT);
  };

  Node.prototype.html = function(html) {
    if (typeof html === 'undefined')
      return this.content();
    this.content(html);
    this.contentType(Node.CONTENT_TYPE_HTML);
  };

  Node.prototype.style = function() {
    var contentType = this.contentType();
    var lineHeight = (contentType === Node.CONTENT_TYPE_TEXT) ? this.height() : 14;
    var textAlign = (contentType === Node.CONTENT_TYPE_TEXT) ? 'center' : 'left';
    var translate = 'translate(' + this.x() + 'px, ' + this.y() + 'px)';
    var borderWidthOffset = this.borderWidth() * 2;
    return {
      backgroundColor: this.backgroundColor(),
      border: this.borderWidth() + 'px solid ' + this.borderColor(),
      borderRadius: '4px',
      color: this.textColor(),
      height: (this.height() - borderWidthOffset) + 'px',
      lineHeight: (lineHeight - borderWidthOffset) + 'px',
      MozTransform: translate,
      msTransform: translate,
      overflow: 'hidden',
      pointerEvents: 'none',
      position: 'absolute',
      textAlign: textAlign,
      textOverflow: 'ellipsis',
      transform: translate,
      webkitTransform: translate,
      whiteSpace: 'nowrap',
      width: (this.width() - borderWidthOffset) + 'px'
    };
  };

  Node.prototype.redraw = function() {
    var content = this.content();
    var contentType = this.contentType();
    var style = this.style();
    var element = this.element();
    var parentElement = this.parentElement();
    var cache = this.cache();

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      this.cache({});
      return;
    }

    if (content !== cache.content) {
      if (contentType === Node.CONTENT_TYPE_TEXT)
        dom.text(element, content);
      else if (contentType === Node.CONTENT_TYPE_HTML)
        dom.html(element, content);
      cache.content = content;
    }

    if (cache.style)
      style = helper.diffObj(style, cache.style);

    dom.css(element, style);
    cache.style = style;
  };

  Node.CONTENT_TYPE_TEXT = 'text';
  Node.CONTENT_TYPE_HTML = 'html';

  var Link = helper.inherits(function(option) {
    this.content = this.prop(option.content || '');
    this.contentType = this.prop(option.contentType || Link.CONTENT_TYPE_TEXT);
    this.x = this.prop(option.x || 100);
    this.y = this.prop(option.y || 40);
    this.width = this.prop(option.width || 50);
    this.height = this.prop(option.height || 20);
    this.backgroundColor = this.prop(option.backgroundColor || 'white');
    this.borderColor = this.prop(option.borderColor || '#333');
    this.borderWidth = this.prop(option.borderWidth || 2);
    this.textColor = this.prop(option.textColor || '#333');
    this.sourceX = this.prop(option.sourceX || option.x - 70 || 30);
    this.sourceY = this.prop(option.sourceY || option.y || 40);
    this.targetX = this.prop(option.targetX || option.x + 70 || 170);
    this.targetY = this.prop(option.targetY || option.y || 40);
    this.lineColor = this.prop(option.lineColor || '#333');
    this.lineWidth = this.prop(option.lineWidth || 2);
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
    this.cache = this.prop({});
  }, Component);

  Link.prototype.text = function(text) {
    if (typeof text === 'undefined')
      return this.content();
    this.content(text);
    this.contentType(Link.CONTENT_TYPE_TEXT);
  };

  Link.prototype.html = function(html) {
    if (typeof html === 'undefined')
      return this.content();
    this.content(html);
    this.contentType(Link.CONTENT_TYPE_HTML);
  };

  Link.prototype.pathContainerStyle = function() {
    return {
      overflow: 'visible',
      position: 'absolute'
    };
  };

  Link.prototype.pathAttributes = function() {
    var d = ['M', this.sourceX(), this.sourceY(),
             'L', this.x(), this.y(),
             'L', this.targetX(), this.targetY()].join(' ');
    return {
      d: d,
      fill: 'none',
      stroke: this.lineColor(),
      'stroke-linecap': 'round',
      'stroke-width': this.lineWidth()
    };
  };

  Link.prototype.contentStyle = function() {
    var contentType = this.contentType();
    var lineHeight = (contentType === Link.CONTENT_TYPE_TEXT) ? this.height() : 14;
    var textAlign = (contentType === Link.CONTENT_TYPE_TEXT) ? 'center' : 'left';
    var x = this.x() - this.width() / 2;
    var y = this.y() - this.height() / 2;
    var translate = 'translate(' + x + 'px, ' + y + 'px)';
    var borderWidthOffset = this.borderWidth() * 2;
    return {
      backgroundColor: this.backgroundColor(),
      border: this.borderWidth() + 'px solid ' + this.borderColor(),
      borderRadius: '4px',
      color: this.textColor(),
      height: (this.height() - borderWidthOffset) + 'px',
      lineHeight: (lineHeight - borderWidthOffset) + 'px',
      MozTransform: translate,
      msTransform: translate,
      overflow: 'hidden',
      position: 'absolute',
      textAlign: textAlign,
      textOverflow: 'ellipsis',
      transform: translate,
      webkitTransform: translate,
      whiteSpace: 'nowrap',
      width: (this.width() - borderWidthOffset) + 'px'
    };
  };

  Link.prototype.redraw = function() {
    var content = this.content();
    var contentType = this.contentType();
    var pathAttributes = this.pathAttributes();
    var contentStyle = this.contentStyle();
    var element = this.element();
    var parentElement = this.parentElement();
    var cache = this.cache();

    var pathContainerElement;
    var pathElement;
    var contentElement;

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      dom.css(element, {pointerEvents: 'none'});
      dom.html(element, '<svg><path></path></svg>

');
      pathContainerElement = element.children[0];
      dom.css(pathContainerElement, this.pathContainerStyle());
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      this.cache({});
      return;
    }

    // update path element
    pathElement = element.children[0].childNodes[0];

    if (cache.pathAttributes)
      pathAttributes = helper.diffObj(pathAttributes, cache.pathAttributes);

    dom.attr(pathElement, pathAttributes);
    cache.pathAttributes = pathAttributes;

    // update content element
    contentElement = element.children[1];

    if (content !== cache.content) {
      if (contentType === Link.CONTENT_TYPE_TEXT)
        dom.text(contentElement, content);
      else if (contentType === Link.CONTENT_TYPE_HTML)
        dom.html(contentElement, content);
      cache.content = content;
    }

    if (cache.contentStyle)
      contentStyle = helper.diffObj(contentStyle, cache.contentStyle);

    dom.css(contentElement, contentStyle);
    cache.contentStyle = contentStyle;
  };

  Link.CONTENT_TYPE_TEXT = 'text';
  Link.CONTENT_TYPE_HTML = 'html';

  var Connector = helper.inherits(function(option) {
    this.x = this.prop(option.x || 0);
    this.y = this.prop(option.y || 0);
    this.r = this.prop(option.r || 16);
    this.color = this.prop(option.color || Connector.COLOR_UNCONNECTED);
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
  }, Component);

  Connector.prototype.style = function() {
    var x = this.x() - this.r();
    var y = this.y() - this.r();
    var translate = 'translate(' + x + 'px, ' + y + 'px)';
    return {
      backgroundColor: this.color(),
      borderRadius: this.r() + 'px',
      height: this.r() * 2 + 'px',
      MozTransform: translate,
      msTransform: translate,
      opacity: 0.6,
      pointerEvents: 'none',
      position: 'absolute',
      transform: translate,
      webkitTransform: translate,
      width: this.r() * 2 + 'px'
    };
  };

  Connector.prototype.redraw = function() {
    var style = this.style();
    var element = this.element();
    var parentElement = this.parentElement();

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      return;
    }

    dom.css(element, style);
  };

  Connector.COLOR_CONNECTED = 'lightgreen';
  Connector.COLOR_UNCONNECTED = 'pink';

  var Cmap = helper.inherits(function(element) {
    if (!(this instanceof Cmap))
      return new Cmap(element);

    this.element = this.prop(element || null);

    this.markDirty();
  }, Component);

  Cmap.prototype.createNode = function(option) {
    return new Node(option || {});
  };

  Cmap.prototype.createLink = function(option) {
    return new Link(option || {});
  };

  Cmap.prototype.createConnector = function(option) {
    return new Connector(option || {});
  };

  Cmap.prototype.add = function(child) {
    child.parentElement(this.element());
  };

  Cmap.prototype.remove = function(child) {
    child.parentElement(null);
  };

  Cmap.prototype.style = function() {
    return {
      color: '#333',
      cursor: 'default',
      fontFamily: 'sans-serif',
      fontSize: '14px',
      MozUserSelect: 'none',
      msUserSelect: 'none',
      position: 'relative',
      userSelect: 'none',
      webkitUserSelect: 'none'
    };
  };

  Cmap.prototype.redraw = function() {
    var element = this.element();

    if (!element) {
      element = dom.el('
');
      this.element(element);
    }

    dom.css(element, this.style());
  };

  if (typeof module !== 'undefined' && module.exports)
    module.exports = Cmap;
  else
    global.Cmap = Cmap;
})(this);<commit_msg>Revert "Remove component list"<commit_after>(function(global) {
  'use strict';

  var helper = {};

  helper.inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    return ctor;
  };

  helper.diffObj = function(newObj, oldObj) {
    var diff = {};
    for (var key in newObj) {
      if (newObj[key] !== oldObj[key])
        diff[key] = newObj[key];
    }
    return diff;
  };

  var dom = {};

  dom.el = function(selector) {
    if (selector[0] === '<') {
      selector = selector.match(/<(.+)>/)[1];
      return document.createElement(selector);
    }
  };

  dom.attr = function(el, props) {
    for (var prop in props) {
      el.setAttribute(prop, props[prop]);
    }
  };

  dom.css = function(el, props) {
    var style = el.style;
    for (var prop in props) {
      style[prop] = props[prop];
    }
  };

  dom.append = function(parent, el) {
    parent.appendChild(el);
  };

  dom.remove = function(el) {
    el.parentNode.removeChild(el);
  };

  dom.text = function(el, s) {
    el.textContent = s;
  };

  dom.html = function(el, s) {
    el.innerHTML = s;
  };

  dom.animate = function(callback) {
    return window.requestAnimationFrame(callback);
  };

  var Component = function() {};

  Component.prototype.prop = function(initialValue) {
    var cache = initialValue;
    return function(value) {
      if (typeof value === 'undefined')
        return cache;
      cache = value;
      this.markDirty();
    };
  };

  Component.prototype.markDirty = (function() {
    var dirtyComponents = [];
    var requestId = null;
    var callback = function() {
      dirtyComponents.forEach(function(component) {
        component.redraw();
      });
      dirtyComponents = [];
      requestId = null;
    };
    return function() {
      if (typeof document === 'undefined')
        return;
      if (dirtyComponents.indexOf(this) === -1)
        dirtyComponents.push(this);
      if (requestId !== null)
        return;
      requestId = dom.animate(callback);
    };
  })();

  var Node = helper.inherits(function(option) {
    this.content = this.prop(option.content || '');
    this.contentType = this.prop(option.contentType || Node.CONTENT_TYPE_TEXT);
    this.x = this.prop(option.x || 0);
    this.y = this.prop(option.y || 0);
    this.width = this.prop(option.width || 75);
    this.height = this.prop(option.height || 30);
    this.backgroundColor = this.prop(option.backgroundColor || '#a7cbe6');
    this.borderColor = this.prop(option.borderColor || '#333');
    this.borderWidth = this.prop(option.borderWidth || 2);
    this.textColor = this.prop(option.textColor || '#333');
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
    this.cache = this.prop({});
  }, Component);

  Node.prototype.text = function(text) {
    if (typeof text === 'undefined')
      return this.content();
    this.content(text);
    this.contentType(Node.CONTENT_TYPE_TEXT);
  };

  Node.prototype.html = function(html) {
    if (typeof html === 'undefined')
      return this.content();
    this.content(html);
    this.contentType(Node.CONTENT_TYPE_HTML);
  };

  Node.prototype.style = function() {
    var contentType = this.contentType();
    var lineHeight = (contentType === Node.CONTENT_TYPE_TEXT) ? this.height() : 14;
    var textAlign = (contentType === Node.CONTENT_TYPE_TEXT) ? 'center' : 'left';
    var translate = 'translate(' + this.x() + 'px, ' + this.y() + 'px)';
    var borderWidthOffset = this.borderWidth() * 2;
    return {
      backgroundColor: this.backgroundColor(),
      border: this.borderWidth() + 'px solid ' + this.borderColor(),
      borderRadius: '4px',
      color: this.textColor(),
      height: (this.height() - borderWidthOffset) + 'px',
      lineHeight: (lineHeight - borderWidthOffset) + 'px',
      MozTransform: translate,
      msTransform: translate,
      overflow: 'hidden',
      pointerEvents: 'none',
      position: 'absolute',
      textAlign: textAlign,
      textOverflow: 'ellipsis',
      transform: translate,
      webkitTransform: translate,
      whiteSpace: 'nowrap',
      width: (this.width() - borderWidthOffset) + 'px'
    };
  };

  Node.prototype.redraw = function() {
    var content = this.content();
    var contentType = this.contentType();
    var style = this.style();
    var element = this.element();
    var parentElement = this.parentElement();
    var cache = this.cache();

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      this.cache({});
      return;
    }

    if (content !== cache.content) {
      if (contentType === Node.CONTENT_TYPE_TEXT)
        dom.text(element, content);
      else if (contentType === Node.CONTENT_TYPE_HTML)
        dom.html(element, content);
      cache.content = content;
    }

    if (cache.style)
      style = helper.diffObj(style, cache.style);

    dom.css(element, style);
    cache.style = style;
  };

  Node.CONTENT_TYPE_TEXT = 'text';
  Node.CONTENT_TYPE_HTML = 'html';

  var Link = helper.inherits(function(option) {
    this.content = this.prop(option.content || '');
    this.contentType = this.prop(option.contentType || Link.CONTENT_TYPE_TEXT);
    this.x = this.prop(option.x || 100);
    this.y = this.prop(option.y || 40);
    this.width = this.prop(option.width || 50);
    this.height = this.prop(option.height || 20);
    this.backgroundColor = this.prop(option.backgroundColor || 'white');
    this.borderColor = this.prop(option.borderColor || '#333');
    this.borderWidth = this.prop(option.borderWidth || 2);
    this.textColor = this.prop(option.textColor || '#333');
    this.sourceX = this.prop(option.sourceX || option.x - 70 || 30);
    this.sourceY = this.prop(option.sourceY || option.y || 40);
    this.targetX = this.prop(option.targetX || option.x + 70 || 170);
    this.targetY = this.prop(option.targetY || option.y || 40);
    this.lineColor = this.prop(option.lineColor || '#333');
    this.lineWidth = this.prop(option.lineWidth || 2);
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
    this.cache = this.prop({});
  }, Component);

  Link.prototype.text = function(text) {
    if (typeof text === 'undefined')
      return this.content();
    this.content(text);
    this.contentType(Link.CONTENT_TYPE_TEXT);
  };

  Link.prototype.html = function(html) {
    if (typeof html === 'undefined')
      return this.content();
    this.content(html);
    this.contentType(Link.CONTENT_TYPE_HTML);
  };

  Link.prototype.pathContainerStyle = function() {
    return {
      overflow: 'visible',
      position: 'absolute'
    };
  };

  Link.prototype.pathAttributes = function() {
    var d = ['M', this.sourceX(), this.sourceY(),
             'L', this.x(), this.y(),
             'L', this.targetX(), this.targetY()].join(' ');
    return {
      d: d,
      fill: 'none',
      stroke: this.lineColor(),
      'stroke-linecap': 'round',
      'stroke-width': this.lineWidth()
    };
  };

  Link.prototype.contentStyle = function() {
    var contentType = this.contentType();
    var lineHeight = (contentType === Link.CONTENT_TYPE_TEXT) ? this.height() : 14;
    var textAlign = (contentType === Link.CONTENT_TYPE_TEXT) ? 'center' : 'left';
    var x = this.x() - this.width() / 2;
    var y = this.y() - this.height() / 2;
    var translate = 'translate(' + x + 'px, ' + y + 'px)';
    var borderWidthOffset = this.borderWidth() * 2;
    return {
      backgroundColor: this.backgroundColor(),
      border: this.borderWidth() + 'px solid ' + this.borderColor(),
      borderRadius: '4px',
      color: this.textColor(),
      height: (this.height() - borderWidthOffset) + 'px',
      lineHeight: (lineHeight - borderWidthOffset) + 'px',
      MozTransform: translate,
      msTransform: translate,
      overflow: 'hidden',
      position: 'absolute',
      textAlign: textAlign,
      textOverflow: 'ellipsis',
      transform: translate,
      webkitTransform: translate,
      whiteSpace: 'nowrap',
      width: (this.width() - borderWidthOffset) + 'px'
    };
  };

  Link.prototype.redraw = function() {
    var content = this.content();
    var contentType = this.contentType();
    var pathAttributes = this.pathAttributes();
    var contentStyle = this.contentStyle();
    var element = this.element();
    var parentElement = this.parentElement();
    var cache = this.cache();

    var pathContainerElement;
    var pathElement;
    var contentElement;

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      dom.css(element, {pointerEvents: 'none'});
      dom.html(element, '<svg><path></path></svg>

');
      pathContainerElement = element.children[0];
      dom.css(pathContainerElement, this.pathContainerStyle());
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      this.cache({});
      return;
    }

    // update path element
    pathElement = element.children[0].childNodes[0];

    if (cache.pathAttributes)
      pathAttributes = helper.diffObj(pathAttributes, cache.pathAttributes);

    dom.attr(pathElement, pathAttributes);
    cache.pathAttributes = pathAttributes;

    // update content element
    contentElement = element.children[1];

    if (content !== cache.content) {
      if (contentType === Link.CONTENT_TYPE_TEXT)
        dom.text(contentElement, content);
      else if (contentType === Link.CONTENT_TYPE_HTML)
        dom.html(contentElement, content);
      cache.content = content;
    }

    if (cache.contentStyle)
      contentStyle = helper.diffObj(contentStyle, cache.contentStyle);

    dom.css(contentElement, contentStyle);
    cache.contentStyle = contentStyle;
  };

  Link.CONTENT_TYPE_TEXT = 'text';
  Link.CONTENT_TYPE_HTML = 'html';

  var Connector = helper.inherits(function(option) {
    this.x = this.prop(option.x || 0);
    this.y = this.prop(option.y || 0);
    this.r = this.prop(option.r || 16);
    this.color = this.prop(option.color || Connector.COLOR_UNCONNECTED);
    this.element = this.prop(null);
    this.parentElement = this.prop(null);
  }, Component);

  Connector.prototype.style = function() {
    var x = this.x() - this.r();
    var y = this.y() - this.r();
    var translate = 'translate(' + x + 'px, ' + y + 'px)';
    return {
      backgroundColor: this.color(),
      borderRadius: this.r() + 'px',
      height: this.r() * 2 + 'px',
      MozTransform: translate,
      msTransform: translate,
      opacity: 0.6,
      pointerEvents: 'none',
      position: 'absolute',
      transform: translate,
      webkitTransform: translate,
      width: this.r() * 2 + 'px'
    };
  };

  Connector.prototype.redraw = function() {
    var style = this.style();
    var element = this.element();
    var parentElement = this.parentElement();

    if (!parentElement && !element)
      return;

    // add element
    if (parentElement && !element) {
      element = dom.el('
');
      this.element(element);
      this.redraw();
      dom.append(parentElement, element);
      return;
    }

    // remove element
    if (!parentElement && element) {
      dom.remove(element);
      this.element(null);
      return;
    }

    dom.css(element, style);
  };

  Connector.COLOR_CONNECTED = 'lightgreen';
  Connector.COLOR_UNCONNECTED = 'pink';

  var ComponentList = function() {
    this.data = [];
  };

  ComponentList.prototype.add = function(component) {
    var data = this.data;
    if (data.indexOf(component) === -1)
      data.push(component);
  };

  ComponentList.prototype.remove = function(component) {
    var data = this.data;
    var index = data.indexOf(component);
    if (index !== -1)
      data.splice(index, 1);
  };

  ComponentList.prototype.each = function(callback) {
    return this.data.forEach(callback);
  };

  var Cmap = helper.inherits(function(element) {
    if (!(this instanceof Cmap))
      return new Cmap(element);

    this.componentList = this.prop(new ComponentList());
    this.element = this.prop(element || null);

    this.markDirty();
  }, Component);

  Cmap.prototype.createNode = function(option) {
    return new Node(option || {});
  };

  Cmap.prototype.createLink = function(option) {
    return new Link(option || {});
  };

  Cmap.prototype.createConnector = function(option) {
    return new Connector(option || {});
  };

  Cmap.prototype.add = function(child) {
    child.parentElement(this.element());
    this.componentList().add(child);
  };

  Cmap.prototype.remove = function(child) {
    child.parentElement(null);
    this.componentList().remove(child);
  };

  Cmap.prototype.style = function() {
    return {
      color: '#333',
      cursor: 'default',
      fontFamily: 'sans-serif',
      fontSize: '14px',
      MozUserSelect: 'none',
      msUserSelect: 'none',
      position: 'relative',
      userSelect: 'none',
      webkitUserSelect: 'none'
    };
  };

  Cmap.prototype.redraw = function() {
    var componentList = this.componentList();
    var element = this.element();

    if (!element) {
      element = dom.el('
');
      this.element(element);
      componentList.each(function(component) {
        component.parentElement(element);
      });
    }

    dom.css(element, this.style());
  };

  if (typeof module !== 'undefined' && module.exports)
    module.exports = Cmap;
  else
    global.Cmap = Cmap;
})(this);<|endoftext|>


<commit_before>var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
var Game = {}

/***********/

Game.score = {}

Game.score.score = 0
Game.score.lengthMult = 100
Game.score.comboMult = 100

/***********/

Game.world = {}

Game.world.mapSize = [20,20] //rows then columns

Game.world.tileSize = 32 //in px

Game.world.map = []

Game.world.createMap = function() {

	/* remember
		x is rows
		y is columns
		array has [x][y] format

		I'll have to reverse this prob even by ugly hack, but for now it is working, so...
	*/

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			/*test if loop works as intended (useful, don't delete (plz(or i will kill you(i'm dead seious))))

			ctx.fillStyle = "red"
			ctx.fillRect(y*Game.world.tileSize , x*Game.world.tileSize, Game.world.tileSize, Game.world.tileSize)

			*/

			Game.world.map = Helper.array.newZeroedArray([Game.world.mapSize[0],Game.world.mapSize[1]])

		}
	}

	//it is a good idea to spawn our snake, right? (I know, s**t implementation, but who cares)
	Game.world.map[3][3] = 1
	Game.world.spawnEatable()

	//obligatory redraw
	Game.screen.redraw()

}

Game.world.spawnEatable = function(){

	daLoop:
	while(true){

		var foo = Math.floor(Math.random()*Game.world.mapSize[0])
		var bar = Math.floor(Math.random()*Game.world.mapSize[1])

		if (Game.world.map[foo][bar] == 0) {

			Game.world.map[foo][bar] = -1
			break daLoop

		}

	}

	Game.screen.redraw()

}

Game.world.player = {}

Game.world.player.length = 5
Game.world.player.direction = "down"
Game.world.player.newDirection = "down"

Game.world.player.directionSet = function(direction) {

	//temporary stupid variables
	var foo = 0
	var bar = 0

	switch (Game.world.player.direction) {

		case "left" :
			foo = 0
			break

		case "right" :
			foo = 3
			break

		case "up" :
			foo = 1
			break

		case "down" :
			foo = 2
			break

	}

	switch (direction) {

		case "left" :
			bar = 0
			break

		case "right" :
			bar = 3
			break

		case "up" :
			bar = 1
			break

		case "down" :
			bar = 2
			break

	}

	if (foo + bar == 3) {

		console.log("wrong direction")

	} else {

		Game.world.player.newDirection = direction
		//Game.world.player.move() //line for testing purposes

	}

}

Game.world.player.getHeadpos = function(){

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			if (Game.world.map[x][y] == 1) { return [x,y] }

		}
	}

	console.error("Snake's head couldn't be found")

}

Game.world.player.move = function(){

	//the most crucial of all functions
	//done in (kinda) bad way
	//because i suck
	//seriously

	var headpos = Game.world.player.getHeadpos() //we need to do this. srsly.
	var newHeadpos = [0,0]

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			if (Game.world.map[x][y] >= 1) {

				Game.world.map[x][y]++

			}

		}
	}

	switch (Game.world.player.newDirection) {

		case "down":
			newHeadpos = [headpos[0]+1,headpos[1]]
			break

		case "up":
			newHeadpos = [headpos[0]-1,headpos[1]]
			break

		case "left":
			newHeadpos = [headpos[0],headpos[1]-1]
			break

		case "right":
			newHeadpos = [headpos[0],headpos[1]+1]
			break

		default:
			console.error("Unexpected direction of snake's movement - conclusion: js hates developer.")

	}

	switch (true) {

		//check if new position isn't out of bounds using this ugliness
		case (
			newHeadpos[0] >= Game.world.mapSize[0] ||
			newHeadpos[0] < 0 ||
			newHeadpos[1] >= Game.world.mapSize[1]	||
			newHeadpos[1] < 0
		):
			clearInterval(Game.world.player.moveLoop)
			break

		//in case of nothing
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] == 0 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			break

		//in case of cannibalism
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] >= 1 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			clearInterval(Game.world.player.moveLoop)
			break

		//eat the eatable
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] == -1 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			Game.world.player.length++
			Game.world.spawnEatable()
			//and then add score
			Game.score.score += Math.floor(Game.score.lengthMult*Game.score.comboMult/100)
			Game.score.lengthMult += 5
			Game.score.comboMult += 20
			break

		//default just in case
		default :
			clearInterval(Game.world.player.moveLoop)
			console.error("why the hell there is something else than numbers on map array?!")
			break
	}

	for (x=0 ; x < Game.world.mapSize[0] ; x++) {
		for (y=0 ; y < Game.world.mapSize[1] ; y++){

			if (Game.world.map[x][y] > Game.world.player.length) {

				Game.world.map[x][y] = 0

			}

		}
	}

	Game.world.player.direction = Game.world.player.newDirection
	Game.screen.redraw()
}

/***********/

Game.screen = {}

Game.screen.redraw = function() {

	//I made this a bit ugly :(

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			switch(true) {

				case Game.world.map[x][y] == 1 : //head of snake
					Helper.canvas.drawRect(x,y,"blue")
					break

				case Game.world.map[x][y] > 1 : //body of snake
					Helper.canvas.drawRect(x,y,"black")
					break

				case Game.world.map[x][y] == -1 : //eatable thing
					Helper.canvas.drawRect(x,y,"red")
					break

				default : //in case of nothing or some stupid value
					Helper.canvas.drawRect(x,y,"white")
					break

			}

		}
	}

	//score and multiplers
	document.getElementById("score").innerHTML = Game.score.score
	document.getElementById("lengthMult").innerHTML = Game.score.lengthMult
	document.getElementById("comboMult").innerHTML = Game.score.comboMult

}

//it is a good idea for now to create map instantly to not clog console.

Game.world.player.moveLoop = setInterval(function(){

	Game.world.player.move()

	if (Game.score.comboMult > 100) { Game.score.comboMult -- }

}, 100)

Game.world.createMap()

Game.reset = function() {

	Game.world.player.direction = "down"
	Game.world.player.newDirection = "down"

	Game.world.createMap()

	clearInterval(Game.world.player.moveLoop)

	Game.world.player.moveLoop = setInterval(function(){

		Game.world.player.move()
		if (Game.score.comboMult > 100) { Game.score.comboMult -- }

	}, 100)

}<commit_msg>minor score balance change<commit_after>var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
var Game = {}

/***********/

Game.score = {}

Game.score.score = 0
Game.score.lengthMult = 100
Game.score.comboMult = 100

/***********/

Game.world = {}

Game.world.mapSize = [20,20] //rows then columns

Game.world.tileSize = 32 //in px

Game.world.map = []

Game.world.createMap = function() {

	/* remember
		x is rows
		y is columns
		array has [x][y] format

		I'll have to reverse this prob even by ugly hack, but for now it is working, so...
	*/

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			/*test if loop works as intended (useful, don't delete (plz(or i will kill you(i'm dead seious))))

			ctx.fillStyle = "red"
			ctx.fillRect(y*Game.world.tileSize , x*Game.world.tileSize, Game.world.tileSize, Game.world.tileSize)

			*/

			Game.world.map = Helper.array.newZeroedArray([Game.world.mapSize[0],Game.world.mapSize[1]])

		}
	}

	//it is a good idea to spawn our snake, right? (I know, s**t implementation, but who cares)
	Game.world.map[3][3] = 1
	Game.world.spawnEatable()

	//obligatory redraw
	Game.screen.redraw()

}

Game.world.spawnEatable = function(){

	daLoop:
	while(true){

		var foo = Math.floor(Math.random()*Game.world.mapSize[0])
		var bar = Math.floor(Math.random()*Game.world.mapSize[1])

		if (Game.world.map[foo][bar] == 0) {

			Game.world.map[foo][bar] = -1
			break daLoop

		}

	}

	Game.screen.redraw()

}

Game.world.player = {}

Game.world.player.length = 5
Game.world.player.direction = "down"
Game.world.player.newDirection = "down"

Game.world.player.directionSet = function(direction) {

	//temporary stupid variables
	var foo = 0
	var bar = 0

	switch (Game.world.player.direction) {

		case "left" :
			foo = 0
			break

		case "right" :
			foo = 3
			break

		case "up" :
			foo = 1
			break

		case "down" :
			foo = 2
			break

	}

	switch (direction) {

		case "left" :
			bar = 0
			break

		case "right" :
			bar = 3
			break

		case "up" :
			bar = 1
			break

		case "down" :
			bar = 2
			break

	}

	if (foo + bar == 3) {

		console.log("wrong direction")

	} else {

		Game.world.player.newDirection = direction
		//Game.world.player.move() //line for testing purposes

	}

}

Game.world.player.getHeadpos = function(){

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			if (Game.world.map[x][y] == 1) { return [x,y] }

		}
	}

	console.error("Snake's head couldn't be found")

}

Game.world.player.move = function(){

	//the most crucial of all functions
	//done in (kinda) bad way
	//because i suck
	//seriously

	var headpos = Game.world.player.getHeadpos() //we need to do this. srsly.
	var newHeadpos = [0,0]

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			if (Game.world.map[x][y] >= 1) {

				Game.world.map[x][y]++

			}

		}
	}

	switch (Game.world.player.newDirection) {

		case "down":
			newHeadpos = [headpos[0]+1,headpos[1]]
			break

		case "up":
			newHeadpos = [headpos[0]-1,headpos[1]]
			break

		case "left":
			newHeadpos = [headpos[0],headpos[1]-1]
			break

		case "right":
			newHeadpos = [headpos[0],headpos[1]+1]
			break

		default:
			console.error("Unexpected direction of snake's movement - conclusion: js hates developer.")

	}

	switch (true) {

		//check if new position isn't out of bounds using this ugliness
		case (
			newHeadpos[0] >= Game.world.mapSize[0] ||
			newHeadpos[0] < 0 ||
			newHeadpos[1] >= Game.world.mapSize[1]	||
			newHeadpos[1] < 0
		):
			clearInterval(Game.world.player.moveLoop)
			break

		//in case of nothing
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] == 0 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			break

		//in case of cannibalism
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] >= 1 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			clearInterval(Game.world.player.moveLoop)
			break

		//eat the eatable
		case Game.world.map[newHeadpos[0]][newHeadpos[1]] == -1 :
			Game.world.map[newHeadpos[0]][newHeadpos[1]] = 1
			Game.world.player.length++
			Game.world.spawnEatable()
			//and then add score
			Game.score.score += Math.floor(Game.score.lengthMult*Game.score.comboMult/100)
			Game.score.lengthMult += 2
			Game.score.comboMult += 20
			break

		//default just in case
		default :
			clearInterval(Game.world.player.moveLoop)
			console.error("why the hell there is something else than numbers on map array?!")
			break
	}

	for (x=0 ; x < Game.world.mapSize[0] ; x++) {
		for (y=0 ; y < Game.world.mapSize[1] ; y++){

			if (Game.world.map[x][y] > Game.world.player.length) {

				Game.world.map[x][y] = 0

			}

		}
	}

	Game.world.player.direction = Game.world.player.newDirection
	Game.screen.redraw()
}

/***********/

Game.screen = {}

Game.screen.redraw = function() {

	//I made this a bit ugly :(

	for (x=0; x < Game.world.mapSize[0] ;x++) {
		for (y=0; y < Game.world.mapSize[1] ;y++){

			switch(true) {

				case Game.world.map[x][y] == 1 : //head of snake
					Helper.canvas.drawRect(x,y,"blue")
					break

				case Game.world.map[x][y] > 1 : //body of snake
					Helper.canvas.drawRect(x,y,"black")
					break

				case Game.world.map[x][y] == -1 : //eatable thing
					Helper.canvas.drawRect(x,y,"red")
					break

				default : //in case of nothing or some stupid value
					Helper.canvas.drawRect(x,y,"white")
					break

			}

		}
	}

	//score and multiplers
	document.getElementById("score").innerHTML = Game.score.score
	document.getElementById("lengthMult").innerHTML = Game.score.lengthMult
	document.getElementById("comboMult").innerHTML = Game.score.comboMult

}

//it is a good idea for now to create map instantly to not clog console.

Game.world.player.moveLoop = setInterval(function(){

	Game.world.player.move()

	if (Game.score.comboMult > 100) { Game.score.comboMult -- }

}, 100)

Game.world.createMap()

Game.reset = function() {

	Game.world.player.direction = "down"
	Game.world.player.newDirection = "down"

	Game.world.createMap()

	clearInterval(Game.world.player.moveLoop)

	Game.world.player.moveLoop = setInterval(function(){

		Game.world.player.move()
		if (Game.score.comboMult > 100) { Game.score.comboMult -- }

	}, 100)

}
<|endoftext|>


<commit_before>//========================================
// Keyed collection _$  1.0.2
//========================================

function _$(obj, quick, keys, fns) {

  _$.VERSION = "1.0.2";

  if(quick) {
    this._back = quick;
    _.each(obj, function(el, idx) {
      this[idx] = el;
    }, this);
  } else {
    this.length = obj.length;//size
    this._back = this._build(obj, keys, fns);
    this._use(0);//first index
  }
  this._esc = false;
  return new Proxy(this, {
    set: function(obj, prop, val) {
      if(obj._esc == true) {
        obj[prop] = val;
        return;
      }
      var old = obj._back[0][obj[prop]];
      obj._back[0][obj[prop]] = val;
      //rebuild quick!
      var vals = obj._back[0];
      _.each(obj._back[2], function(v, key) {
        if(old[key] !== val[key]) { //only fix damaged keys
          var cur = _.range(obj.length);
          cur.sort(function(a, b) {
            var x = 0;
            while(x == 0 && key < keys.length) {
               x = fns[key] && fns[key++](vals[a], vals[b]);
            }
          });
          obj._back[1][key] = cur;//store new sort
          if(key == obj._back[4]) obj._use(key);//retore valid index
        }
      });
    }
    get: function(obj, prop) {
      return obj._back[0][obj[prop]];//return indexed
    }
  });

  function _use(idx) {
    var x = this._back[4] = idx;
    this._esc = true;//escape proxy
    _.each(this, function(el, key) {//has right count
      this[key] = this._back[1][x][key];//indexes, index used, element
    }, this);
    this._esc = false;
  }

  function _build(obj, keys, fns) {
    var vals = [];
    _.each(obj, function(el) {
      vals.push(el);
    });
    var len = vals.length;
    var idx = [];
    _.each(keys, function(val, key) {
      var cur = _.range(len);
      cur.sort(function(a, b) {
        var x = 0;
        while(x == 0 && key < keys.length) {
           x = fns[key] && fns[key++](vals[a], vals[b]);
        }
      });
      idx.push(cur);//add an index
    });
    return [vals, idx, keys, fns, 0];//master shared structure of collection state
  }

  function use(name) {
    this._use(this._back[2].findIndex(name));
  }

  function concat(all) {// or ONE _$ to make a collection of both
    var res;
    if(all instanceof _$) {// N.B.
      res = new _$(super.concat(all), this._idx.concat(all._idx));// super === this.prototype ...
    } else {
      res = new _$(this);
      _.each(arguments, function(el) {
        res.push(el);
      });
    }
    return res;
  }

  function fill() {
    error(fill);
  }

  function copyWithin() {
    error(copyWithin);
  }

  function push(el) {
    this._idx.push(el._i);//becomes finable
    return super.push(el);
  }

  function pop() {
    this._idx.pop();//make not found
    return super.pop();
  }

  function shift() {//synchronized auto
    this._idx.shift();
    return super.shift();
  }

  function unshift(el) {
    this._idx.unshift(el._i);
    return super.unshift(el);
  }

  function slice() {
    return new _$(super.slice(arguments), this._back);//quick
  }

  function splice() {
    var x = Array.from(arguments);
    _.map(x, function(el, key) {
      if(key < 2) return el;
      return el._i;
    });
    this._idx.splice(x);
    return super.splice(arguments);
  }

  function reverse() {
    error(reverse);
  }

  function filter(callback, thisArg) {
    var it = new _$();
    _.each(this, function(el, key, arr) {
      if(callback.call(thisArg, el, key, arr)) {
        it.push(el);
      }
    });
    return it;
  }

  function map() {
    error(map, 'consider a library like underscore. This is not an efficient operation.');
  }

  function sort() {
    error(sort);
  }

  function error(func, suggest) {
    suggest = suggest || 'understand this is an indexed sorted collection.';
    throw 'Library _$.' + func.name + '() is not available. Suggest you ' + suggest;
  }

  //any more functions?
}

_$.prototype = Array;
<commit_msg>just slice, push and concat to do<commit_after>//========================================
// Keyed collection _$  1.0.2
//========================================

function _$(obj, quick, keys, fns) {

  _$.VERSION = "1.0.2";

  if(quick) {
    this._back = quick;
    _.each(obj, function(el, idx) {
      this[idx] = el;
    }, this);
  } else {
    this.length = obj.length;//size
    this._back = this._build(obj, keys, fns);
    this._use(0);//first index
  }
  this._esc = false;
  return new Proxy(this, {
    set: function(obj, prop, val) {
      if(obj._esc == true) {
        obj[prop] = val;
        return;
      }
      var old = obj._back[0][obj[prop]];
      obj._back[0][obj[prop]] = val;
      //rebuild quick!
      var vals = obj._back[0];
      _.each(obj._back[2], function(v, key) {
        if(old[key] !== val[key]) { //only fix damaged keys
          var cur = _.range(obj.length);
          cur.sort(function(a, b) {
            var x = 0;
            while(x == 0 && key < keys.length) {
               x = fns[key] && fns[key++](vals[a], vals[b]);
            }
          });
          obj._back[1][key] = cur;//store new sort
          if(key == obj._back[4]) obj._use(key);//retore valid index
        }
      });
    }
    get: function(obj, prop) {
      return obj._back[0][obj[prop]];//return indexed
    }
  });

  function _use(idx) {
    var x = this._back[4] = idx;
    this._esc = true;//escape proxy
    _.each(this, function(el, key) {//has right count
      this[key] = this._back[1][x][key];//indexes, index used, element
    }, this);
    this._esc = false;
  }

  function _build(obj, keys, fns) {
    var vals = [];
    _.each(obj, function(el) {
      vals.push(el);
    });
    var len = vals.length;
    var idx = [];
    _.each(keys, function(val, key) {
      var cur = _.range(len);
      cur.sort(function(a, b) {
        var x = 0;
        while(x == 0 && key < keys.length) {
           x = fns[key] && fns[key++](vals[a], vals[b]);
        }
      });
      idx.push(cur);//add an index
    });
    return [vals, idx, keys, fns, 0];//master shared structure of collection state
  }

  function use(name) {
    if(this.length != this._back[0].length) error(use, 'use use() before slice().');
    this._use(this._back[2].findIndex(name));
  }

  function concat(all) {// or ONE _$ to make a collection of both
    var res;
    if(all instanceof _$) {// N.B.
      res = new _$(super.concat(all), this._idx.concat(all._idx));// super === this.prototype ...
    } else {
      res = new _$(this);
      _.each(arguments, function(el) {
        res.push(el);
      });
    }
    return res;
  }

  function fill() {
    error(fill);
  }

  function copyWithin() {
    error(copyWithin);
  }

  function push(el) {
    this._idx.push(el._i);//becomes finable
    return super.push(el);
  }

  function pop() {
    return this._back[0][super.pop()];
  }

  function shift() {
    return this._back[0][super.shift()];
  }

  function unshift(el) {
    return this.push(el);//it's the same for this collection
  }

  function slice() {
    return new _$(super.slice(arguments), this._back);//quick
  }

  function splice() {
    var x = Array.from(arguments);
    _.map(x, function(el, key) {
      if(key < 2) return el;
      return el._i;
    });
    this._idx.splice(x);
    return super.splice(arguments);
  }

  function reverse() {
    error(reverse);
  }

  function filter(callback, thisArg) {
    var it = new _$();
    _.each(this, function(el, key, arr) {
      if(callback.call(thisArg, el, key, arr)) {
        it.push(el);
      }
    });
    return it;
  }

  function map() {
    error(map, 'consider a library like underscore. This is not an efficient operation.');
  }

  function sort() {
    error(sort);
  }

  function error(func, suggest) {
    suggest = suggest || 'understand this is an indexed sorted collection.';
    throw 'Library _$.' + func.name + '() is not available. Suggest you ' + suggest;
  }

  //any more functions?
}

_$.prototype = Array;
<|endoftext|>