Source: model/Game.js

/**
 * Model of a game that takes advantage of Box2D.
 * @module
 */
define(['box2d'], function(Box2D) {
  'use strict';

  /**
   * @constructor
   * @this {module:model/Game}
   */
  var exports = function() {
    // Multiply all variables with 2 to make them more suitable.
    // See section `1.7 Units` on http://www.box2d.org/manual.html for an
    // explanation of why.
    this.MULTIPLIER = 2;

    // Where to store the walls
    this.walls = [];

    // Where to store the obstacles
    this.obstacles = [];

    // Create the world
    var gravity = new Box2D.b2Vec2(0.0, 0.0);
    this.world = new Box2D.b2World(gravity);

    // Start with 0 tilt.
    this.horizontalTilt = 0;
    this.verticalTilt = 0;
  };

  /**
   * Create container edges.
   *
   * @this {module:model/Game}
   * @param {Object} width
   * @param {Object} height
   */
  exports.prototype.createContainer = function(width, height) {
    var edges = [
      {
        v1: new Box2D.b2Vec2(0, 0),
        v2: new Box2D.b2Vec2(0, this.MULTIPLIER * height)
      },
      {
        v1: new Box2D.b2Vec2(0, this.MULTIPLIER * height),
        v2: new Box2D.b2Vec2(this.MULTIPLIER * width, this.MULTIPLIER * height)
      },
      {
        v1: new Box2D.b2Vec2(this.MULTIPLIER * width, this.MULTIPLIER * height),
        v2: new Box2D.b2Vec2(this.MULTIPLIER * width, 0)
      },
      {
        v1: new Box2D.b2Vec2(this.MULTIPLIER * width, 0),
        v2: new Box2D.b2Vec2(0, 0)
      }
    ];
    _.forEach(edges, function(edge) {
      var shape = new Box2D.b2EdgeShape();
      shape.Set(edge.v1, edge.v2);

      var bd = new Box2D.b2BodyDef();
      bd.set_type(Box2D.b2_staticBody);

      var body = this.world.CreateBody(bd);
      body.CreateFixture(shape, 0);
    }.bind(this));
  };

  /**
   * Add a wall to the model.
   *
   * @this {module:model/Game}
   * @param {Object} wall
   * @param {number} wall.x X coordinate of the wall's center.
   * @param {number} wall.y Y coordinate of the wall's center.
   * @param {number} wall.width Width of the wall.
   * @param {number} wall.height Height of the wall.
   * @returns {b2Body} The Box2D body that was created.
   */
  exports.prototype.createWall = function(wall) {
    var shape = new Box2D.b2PolygonShape();
    // Divide with 2 because Box2d's method wants the half-width and the
    // half-height, see:
    // http://hodade.adam.ne.jp/box2d/API/html/classb2_polygon_shape.html#a6bb90df8b4a40d1c53b64cc352a855dd
    shape.SetAsBox(
      this.MULTIPLIER * wall.width / 2, this.MULTIPLIER * wall.height / 2
    );

    var bd = new Box2D.b2BodyDef();
    bd.set_type(Box2D.b2_staticBody);
    bd.set_position(
      new Box2D.b2Vec2(this.MULTIPLIER * wall.x, this.MULTIPLIER * wall.y)
    );

    var body = this.world.CreateBody(bd);
    body.CreateFixture(shape, 0);

    this.walls.push({
      body: body,
      width: this.MULTIPLIER * wall.width,
      height: this.MULTIPLIER * wall.height
    });

    return body;
  };

  /**
   * Add a ball.
   *
   * @this {module:model/Game}
   * @param {Object} ball
   * @param {number} ball.x X coordinate.
   * @param {number} ball.y Y coordinate.
   * @returns {b2Body} The Box2D body that was created.
   */
  exports.prototype.createBall = function(ball) {
    var shape = new Box2D.b2CircleShape();
    // Make the ball to a fourth the size of a wall cell
    var radius = this.MULTIPLIER / 8;
    shape.set_m_radius(radius);

    var bd = new Box2D.b2BodyDef();
    bd.set_type(Box2D.b2_dynamicBody);
    // Add 0.5 to place it in the center of the cell
    bd.set_position(new Box2D.b2Vec2(
      this.MULTIPLIER * (ball.x + 0.5),
      this.MULTIPLIER * (ball.y + 0.5)
    ));

    var body = this.world.CreateBody(bd);
    var fixtureDef = new Box2D.b2FixtureDef();
    fixtureDef.set_shape(shape);
    fixtureDef.set_density(5.0);
    fixtureDef.set_restitution(0.1);
    body.CreateFixture(fixtureDef);
    body.SetLinearDamping(0.4);

    this.ball = {body: body, radius: radius};

    return body;
  };

  /**
   * Add the exit path.
   *
   * @this {module:model/Game}
   * @param {Array.<Object>} exitPath
   * @param {number} exitPath.x X coordinate of the cell's center.
   * @param {number} exitPath.y Y coordinate of the cell's center.
   */
  exports.prototype.addExitPath = function(exitPath) {
    this.exitPath = _.map(exitPath, function(pathCell) {
      // Adjust the exit path coordinates with the model's multiplier
      return {
        x: pathCell.x * this.MULTIPLIER,
        y: pathCell.y * this.MULTIPLIER
      };
    }.bind(this));
  };

  /**
   * Create an obstacle.
   *
   * @this {module:model/Game}
   * @param {Object} obstacle
   * @param {number} obstacle.x X coordinate of the cell the obstacle will be
   *   placed in.
   * @param {number} obstacle.y Y coordinate of the cell the obstacle will be
   *   placed in.
   * @returns {b2Body} The Box2D body that was created.
   */
  exports.prototype.createObstacle = function(obstacle) {
    var shape = new Box2D.b2CircleShape();
    // Make the obstacle to half the size of a wall cell
    var radius = this.MULTIPLIER / 4;
    shape.set_m_radius(radius);

    var bd = new Box2D.b2BodyDef();
    bd.set_type(Box2D.b2_staticBody);
    // Randomly place the pbstacle in one of the cell's corners
    bd.set_position(new Box2D.b2Vec2(
      this.MULTIPLIER * (obstacle.x + 0.25 + _.random(0, 1) * 0.5),
      this.MULTIPLIER * (obstacle.y + 0.25 + _.random(0, 1) * 0.5)
    ));

    var body = this.world.CreateBody(bd);
    var fixtureDef = new Box2D.b2FixtureDef();
    fixtureDef.set_shape(shape);
    body.CreateFixture(fixtureDef);

    this.obstacles.push({body: body, radius: radius});

    return body;
  };

  /**
   * Step the game model.
   *
   * @this {module:model/Game}
   * @param {number} deltaTime Time step that the world should progress.
   */
  exports.prototype.step = function(deltaTime) {
    this.ball.body.ApplyForceToCenter(new Box2D.b2Vec2(
      // Multiply with the multiplier to the power of 2 because the area
      // of the ball increases x^2 times when the side indreases x times.
      Math.pow(this.MULTIPLIER, 2) * this.horizontalTilt * 1,
      Math.pow(this.MULTIPLIER, 2) * this.verticalTilt * 1
    ));
    this.world.Step(deltaTime, 3, 3);
  };

  /**
   * Tilt the labyrinth to the left.
   */
  exports.prototype.tiltLeft = function() {
    this.horizontalTilt = -1;
  };

  /**
   * Tilt the labyrinth to the right.
   *
   * @this {module:model/Game}
   */
  exports.prototype.tiltRight = function() {
    this.horizontalTilt = 1;
  };

  /**
   * Reset the horizontal labyrinth tilt.
   *
   * @this {module:model/Game}
   */
  exports.prototype.releaseHorizontalTilt = function() {
    this.horizontalTilt = 0;
  };

  /**
   * Tilt the labyrinth downwards.
   *
   * @this {module:model/Game}
   */
  exports.prototype.tiltDown = function() {
    this.verticalTilt = -1;
  };

  /**
   * Tilt the labyrinth upwards.
   *
   * @this {module:model/Game}
   */
  exports.prototype.tiltUp = function() {
    this.verticalTilt = 1;
  };

  /**
   * Reset the vertical labyrinth tilt.
   *
   * @this {module:model/Game}
   */
  exports.prototype.releaseVerticalTilt = function() {
    this.verticalTilt = 0;
  };

  return exports;
});