Source: objects.js

/**
 * Enforces the abstract class paradigm
 */
class AbstractClass {
  /**
   * Ensures the class being created is a derived class not a base class
   *  @param {Object} BaseClass - The baseclass that is abstract
   */
  constructor(BaseClass) {
    this._baseClass = BaseClass;
    if (this.constructor === AbstractClass) {
      throw new TypeError('Abstract class "AbstractClass" cannot be instantiated directly');
    }
    if (this.constructor === this._baseClass) {
      throw new TypeError(`Abstract class "${this._baseClass.name}" cannot be instantiated directly`);
    }
  }
  /**
   * Raises an error if an abstract base class method is called
   *  @param {string} methodName - The name of the method that is abstract
   */
  AbstractMethod(methodName) {
    throw new TypeError(
      `${this._baseClass.name}: Abstract method "${methodName}" not overridden by derived class "${this.constructor.name}".`
    );
  }
}

/**
 * Defines the base functionality of interactive game objects
 *  @extends AbstractClass
 */
class GameObject extends AbstractClass {
  /**
   * Creates a base GameObject
   *  @param {Object} game - The game manager class
   *  @param {Object} boundingBox - Initial size and position of this GameObject
   */
  constructor(game, boundingBox) {
    super(GameObject);
    this._game = game;
    this._box = boundingBox;
  }
  /**
   * Gets the bounding box dimensions of this Object
   *  @return {Object} This GameObject's BoundingBox
   */
  get dimensions() {
    return this._box;
  }
  /**
   * Abstract interface definition for the draw method.
   * Must be overridden in derived classes.
   */
  draw(ctx) {this.AbstractMethod("draw");}
  /**
   * Simple collision test between this GameObject and another
   *  @param {Object} object - The GameObject to check for intersection
   *  @return {boolean} true/false if colliding
   */
  collision(object) {
    return this._box.collides(object._box);
  }
  /**
   * Performs a comprehensive collision test that checks where the two objects are
   * overlapping and indicates the closest point to move them out of collision.
   *  @param {Object} object - The object to check for intersection
   *  @return {Object} a dictionary with side: axis of intersection and
   *                     pos: closest point of non-intersection
   */
  intersects(object) {
    return this._box.intersects(object._box);
  }
}

/**
 * Defines a destroyable block
 *  @extends GameObject
 */
class Block extends GameObject {
  /**
   * Creates a Block
   *  @param {Object} game - The game manager class
   *  @param {Object} boundingBox - Initial size and position of this Block
   */
  constructor(game, boundingBox) {
    super(game, boundingBox);
    this._alive = true;
  }
  /**
   * Gets whether this Block is still alive
   *  @return {boolean} True/False if the Block is still alive
   */
  get isAlive() {
    return this._alive;
  }
  /**
   * Draws this block on the screen
   *  @param {Object} ctx - The Canvas rendering context to draw to.
   */
  draw(ctx) {
    if (this._alive) {
      ctx.beginPath();
      ctx.rect(this._box.x, this._box.y, this._box.width, this._box.height);
      ctx.fill();
    }
  }
  /**
   * Simple collision test between this GameObject and another
   *  @param {Object} object - The GameObject to check for intersection
   *  @return {boolean} true/false if colliding
   */
  collision(object) {
    let collision = false;
    // Only check collisions if the block is still alive
    if (this._alive) {
      collision = super.collision(object);
      this._alive = !collision;
    }
    return collision;
  }
  /**
   * Performs a comprehensive collision test that checks where the two objects are
   * overlapping and indicates the closest point to move them out of collision.
   *  @param {Object} object - The object to check for intersection
   *  @return {Object} a dictionary with side: axis of intersection and
   *                     pos: closest point of non-intersection
   */
  intersects(object) {
    let collision = false;
    // Only check collisions if the block is still alive
    if (this._alive) {
      collision = super.intersects(object);
      if (collision) this._alive = false;
    }
    return collision;
  }
}

/**
 * Defines a player controlled paddle
 *  @extends GameObject
 */
class Paddle extends GameObject {
  /**
   * Creates a Paddle
   *  @param {Object} game - The game manager class
   *  @param {Object} boundingBox - Initial size and position of this Paddle
   *  @param {number} speed - The movement speed of this paddle in pixels a second
   */
  constructor(game, boundingBox, speed) {
    super(game, boundingBox);
    // Speed the paddle can move in pixels a second
    this._speed = speed;
  }
  /**
   * Gets this Paddles current movement speed
   *  @return {number} The movement speed of this Paddle
   */
  get speed() {
    return this._speed;
  }
  /**
   * Sets the Paddle's movement speed
   *  @param {number} val - The new movement speed
   */
  set speed(val) {
    this._speed = val;
  }
  /**
   * Draws this Paddle on the screen
   *  @param {Object} ctx - The Canvas rendering context to draw to.
   */
  draw(ctx) {
    ctx.beginPath();
    ctx.rect(this._box.x, this._box.y, this._box.width, this._box.height);
    ctx.fill();
  }
  /**
   * Sets the x position of this Paddle while keeping it within the boundary passed
   *  @param {object} bounds - BoundingBox the Paddle should stay within
   *  @param {number} x - The new x coordinate
   */
  setPosInBounds(bounds, x) {
    // Clamp to the left side of the screen
    if (x < bounds.x) {
      x = bounds.x;
    }
    // Clamp to the right side of the screen
    if (x + this._box.width > bounds.x + bounds.width) {
      x = bounds.width - this._box.width;
    }
    this._box.x = x;
  }
  // Generalised movement function, for internal use
  _move(bounds, timeDelta, direction) {
    this.setPosInBounds(bounds, this._box.x + ((this._speed * timeDelta) * direction));
  }
  /**
   * Moves the paddle left based on it's speed and the time since the last update
   *  @param {Objects} bounds - BoundingBox the Paddle should stay within
   *  @param {number} timeDelta - The time in seconds since the last update
   */
  moveLeft(bounds, timeDelta) {
    this._move(bounds, timeDelta, -1);
  }
  /**
   * Moves the paddle right based on it's speed and the time since the last update
   *  @param {Objects} bounds - BoundingBox the Paddle should stay within
   *  @param {number} timeDelta - The time in seconds since the last update
   */
  moveRight(bounds, timeDelta) {
    this._move(bounds, timeDelta, 1);
  }
}

/**
 * Defines a basic ball
 *  @extends GameObject
 */
class Ball extends GameObject {
  /**
   * Creates a Ball
   *  @param {Object} game - The game manager class
   *  @param {Object} boundingBox - Initial size and position of this Ball
   *  @param {Object} initalVector - Initial movement vector of the Ball
   */
  constructor(game, boundingBox, initalVector) {
    super(game, boundingBox);
    this._vector = initalVector;
  }
  /**
   * Gets this Ball's current movement vector
   *  @return {Object} The current movement Vector2D
   */
  get vector() {
    return this._vector;
  }
  /**
   * Draws this Ball on the screen
   *  @param {Object} ctx - The Canvas rendering context to draw to.
   */
  draw(ctx) {
    // Calculate the center point from the BoundingBox
    let cX = this._box.x + this._box.width / 2;
    let cY = this._box.y + this._box.height / 2;
    // Draw the circle. Assumes height === width
    ctx.beginPath();
    ctx.arc(cX, cY, this._box.width / 2, 0, 2 * Math.PI);
    ctx.fill();
  }
  /**
   * Moves the ball based on it's movement vector and the time since the last update
   *  @param {Object} bounds - BoundingBox the Ball should stay within
   *  @param {number} timeDelta - The time in seconds since the last update
   */
  move(bounds, timeDelta) {
    // Calculate new position
    this._box.x = this._box.x + (this.vector.x * timeDelta);
    this._box.y = this._box.y + (this.vector.y * timeDelta);

    // Check the ball will still be in bounds
    if (this._box.x < bounds.x) {
      // Hit the left side of the screen
      this._box.x = bounds.x;
      this._vector.x = -this._vector.x;
    }
    if (this._box.x + this._box.width > bounds.x + bounds.width) {
      // Hit the right side of the screen
      this._box.x = bounds.x + (bounds.width - this._box.width);
      this._vector.x = -this._vector.x;
    }
    if (this._box.y < bounds.y) {
      // Hit the top of the screen
      this._box.y = bounds.y;
      this._vector.y = -this._vector.y;
    }
    if (this._box.y + this._box.height > bounds.y + bounds.height) {
      // Hit the bottom of the screen
      game.loseALife();
    }

    // Check for collision with the paddle
    let collision = game.paddle.intersects(this);
    if (collision) {
      this._box[collision.side] = collision.pos;
      this._vector[collision.side] = -this._vector[collision.side];
    }
    // Check for collisions with the blocks
    const blocks = game.blocks;
    for (let row = 0; row < blocks.length; row++) {
      for (let column = 0; column < blocks[row].length; column++) {
        collision = blocks[row][column].intersects(this);
        if (collision) {
          this._box[collision.side] = collision.pos;
          this._vector[collision.side] = -this._vector[collision.side];
          // Increment the score counter
          game.increaseScore();
        }
      }
    }
  }
}