Source: models/drawable-object.class.js

// drawable-object.class.js

/**
 * @class DrawableObject
 *
 * Base class for all objects that can be drawn onto the canvas.
 * Handles image management, orientation and collision offsets.
 */
class DrawableObject {
  /**
   * Defines the hitbox offset for the object.
   * Useful for shrinking or expanding collision boundaries relative to the image.
   * @type {{ top: number, bottom: number, left: number, right: number }}
   */
  offset = {
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  };

  /**
   * The currently displayed image of the object.
   * Set by loadImage or animation logic.
   * @type {HTMLImageElement | undefined}
   */
  img;

  /**
   * A cache of preloaded images for animation or performance.
   * Keys are image paths, values are HTMLImageElement instances.
   * @type {Object.<string, HTMLImageElement>}
   */
  imageCache = {};

  /**
   * Index of the current frame in the animation image sequence.
   * Used by playAnimation.
   * @type {number}
   */
  currentImage = 0;

  /**
   * Whether the object is facing left instead of the default right.
   * Used for mirroring the image in rendering logic.
   * @type {boolean}
   */
  otherDirection = false;

  /**
   * Loads a single image and assigns it to the `img` property.
   * Used to display a static image or initialize an animation frame.
   *
   * @param {string} path - The path to the image file.
   */
  loadImage(path) {
    this.img = new Image();
    this.img.src = path;
  }

  /**
   * Preloads an array of image paths and stores them in the image cache.
   * Used for animations or quick access to different states.
   * Calls the global `countLoadedImages` function on each load.
   *
   * @param {string[]} arr - Array of image file paths to preload.
   */
  loadImages(arr) {
    arr.forEach((path) => {
      let img = new Image();
      img.src = path;
      this.imageCache[path] = img;
      img.onload = handleImageLoad;
    });
  }

  /**
   * Draws the current image of the object onto the canvas.
   * This is the standard rendering function used by all drawable game objects.
   *
   * @param {CanvasRenderingContext2D} ctx - The canvas 2D rendering context.
   */
  draw(ctx) {
    try {
      ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
    } catch (error) {
      console.warn(error);
      debugger;
    }
  }

  /**
   * Draws a red outline representing the object's collision hitbox.
   * Only applies to certain object types (Character, Chicken, etc.).
   * Useful for debugging and visualizing hitboxes.
   *
   * @param {CanvasRenderingContext2D} ctx - The canvas 2D rendering context.
   */
  drawFrame(ctx) {
    if (
      this instanceof Character ||
      this instanceof Chicken ||
      this instanceof Endboss ||
      this instanceof CollectibleObject ||
      this instanceof Obstacle
    ) {
      ctx.beginPath();
      ctx.lineWidth = "2";
      ctx.strokeStyle = "red";

      ctx.rect(
        this.x + this.offset.left,
        this.y + this.offset.top,
        this.width - this.offset.left - this.offset.right,
        this.height - this.offset.top - this.offset.bottom
      );

      ctx.stroke();
    }
  }

  /**
   * Cycles through the given image array to create an animation effect.
   * Updates the `img` property to the next frame in the sequence.
   *
   * @param {string[]} images - An array of image paths used for animation frames.
   */
  playAnimation(images) {
    let i = this.currentImage % images.length;
    let path = images[i];
    this.img = this.imageCache[path];
    this.currentImage++;
  }

  /**
   * Checks whether this object is colliding with another object.
   * Uses axis-aligned bounding box (AABB) collision detection with hitbox offsets.
   *
   * @param {DrawableObject} other - The object to check collision against.
   * @returns {boolean} True if this object overlaps with the other object.
   */
  isColliding(other) {
    return (
      this.getHitboxBorderRight() >= other.getHitboxBorderLeft() &&
      this.getHitboxBorderLeft() <= other.getHitboxBorderRight() &&
      this.getHitboxBorderBottom() >= other.getHitboxBorderTop() &&
      this.getHitboxBorderTop() <= other.getHitboxBorderBottom()
    );
  }

  /**
   * Calculates the right boundary of the object's hitbox.
   * @returns {number} Right edge including offset.
   */
  getHitboxBorderRight() {
    return this.x + (this.width - this.offset.right);
  }

  /**
   * Calculates the left boundary of the object's hitbox.
   * @returns {number} Left edge including offset.
   */
  getHitboxBorderLeft() {
    return this.x + this.offset.left;
  }

  /**
   * Calculates the bottom boundary of the object's hitbox.
   * @returns {number} Bottom edge including offset.
   */
  getHitboxBorderBottom() {
    return this.y + (this.height - this.offset.bottom);
  }

  /**
   * Calculates the top boundary of the object's hitbox.
   * @returns {number} Top edge including offset.
   */
  getHitboxBorderTop() {
    return this.y + this.offset.top;
  }
}