Source: models/character.class.js

/**
 * Represents the main character in the game, extending the MovableObject class.
 * Manages animations, movement, collisions, and idle states (including a long idle with snoring).
 *
 * @class Character
 * @extends MovableObject
 */
class Character extends MovableObject {
  /**
   * The height of the character.
   * @type {number}
   */
  height = 280;

  /**
   * The y-coordinate of the character.
   * @type {number}
   */
  y = 150;

  /**
   * The x-coordinate of the character.
   * @type {number}
   */
  x = 100;

  /**
   * The current image index for animations.
   * @type {number}
   */
  currentImage = 0;

  /**
   * The movement speed of the character.
   * @type {number}
   */
  speed = 10;

  /**
   * Indicates whether the character is in a long idle state.
   * @type {boolean}
   */
  longIdle = false;

  /**
   * Timeout reference for triggering the long idle state.
   * @type {number|null}
   */
  timeoutLongIdle = null;

  /**
   * Indicates if the character is facing the opposite direction (left).
   * @type {boolean}
   */
  otherDirektion = false;

  /**
   * Reference to the interval handling character animations.
   * @type {number}
   */
  characterInterval;

  /**
   * Reference to the interval handling character movement.
   * @type {number}
   */
  moveIntervall;

  /**
   * An array of image paths for the default idle animation.
   * @type {string[]}
   */
  IMAGES_IDLE = [
    "img/2_character_pepe/1_idle/idle/I-1.png",
    "img/2_character_pepe/1_idle/idle/I-2.png",
    "img/2_character_pepe/1_idle/idle/I-3.png",
    "img/2_character_pepe/1_idle/idle/I-4.png",
    "img/2_character_pepe/1_idle/idle/I-5.png",
    "img/2_character_pepe/1_idle/idle/I-6.png",
    "img/2_character_pepe/1_idle/idle/I-7.png",
    "img/2_character_pepe/1_idle/idle/I-8.png",
    "img/2_character_pepe/1_idle/idle/I-9.png",
    "img/2_character_pepe/1_idle/idle/I-10.png",
  ];

  /**
   * An array of image paths for the walking animation.
   * @type {string[]}
   */
  IMAGES_WALKING = [
    "img/2_character_pepe/2_walk/W-21.png",
    "img/2_character_pepe/2_walk/W-22.png",
    "img/2_character_pepe/2_walk/W-23.png",
    "img/2_character_pepe/2_walk/W-24.png",
    "img/2_character_pepe/2_walk/W-25.png",
    "img/2_character_pepe/2_walk/W-26.png",
  ];

  /**
   * An array of image paths for the jumping animation.
   * @type {string[]}
   */
  IMAGES_JUMPING = [
    "img/2_character_pepe/3_jump/J-31.png",
    "img/2_character_pepe/3_jump/J-32.png",
    "img/2_character_pepe/3_jump/J-33.png",
    "img/2_character_pepe/3_jump/J-34.png",
    "img/2_character_pepe/3_jump/J-35.png",
    "img/2_character_pepe/3_jump/J-36.png",
    "img/2_character_pepe/3_jump/J-37.png",
    "img/2_character_pepe/3_jump/J-38.png",
    "img/2_character_pepe/3_jump/J-39.png",
  ];

  /**
   * An array of image paths for the dead (game over) animation.
   * @type {string[]}
   */
  IMAGES_DEAD = [
    "img/2_character_pepe/5_dead/D-51.png",
    "img/2_character_pepe/5_dead/D-52.png",
    "img/2_character_pepe/5_dead/D-53.png",
    "img/2_character_pepe/5_dead/D-54.png",
    "img/2_character_pepe/5_dead/D-55.png",
    "img/2_character_pepe/5_dead/D-56.png",
    "img/2_character_pepe/5_dead/D-57.png",
  ];

  /**
   * An array of image paths for the hurt animation.
   * @type {string[]}
   */
  IMAGES_HURT = [
    "img/2_character_pepe/4_hurt/H-41.png",
    "img/2_character_pepe/4_hurt/H-42.png",
    "img/2_character_pepe/4_hurt/H-43.png",
  ];

  /**
   * An array of image paths for the long idle animation (snoring).
   * @type {string[]}
   */
  IMAGES_LONG_IDLE = [
    "img/2_character_pepe/1_idle/long_idle/I-11.png",
    "img/2_character_pepe/1_idle/long_idle/I-12.png",
    "img/2_character_pepe/1_idle/long_idle/I-13.png",
    "img/2_character_pepe/1_idle/long_idle/I-14.png",
    "img/2_character_pepe/1_idle/long_idle/I-15.png",
    "img/2_character_pepe/1_idle/long_idle/I-16.png",
    "img/2_character_pepe/1_idle/long_idle/I-17.png",
    "img/2_character_pepe/1_idle/long_idle/I-18.png",
    "img/2_character_pepe/1_idle/long_idle/I-19.png",
    "img/2_character_pepe/1_idle/long_idle/I-20.png",
  ];

  /**
   * Reference to the game world object.
   * @type {object}
   */
  world;

  /**
   * Constructs the Character, loads images, applies gravity, and starts the animation loops.
   */
  constructor() {
    super().loadImage(this.IMAGES_IDLE[0]);
    this.offset.top = 130;
    this.offset.bottom = 10;
    this.offset.left = 30;
    this.offset.right = 30;

    this.loadImages(this.IMAGES_IDLE);
    this.loadImages(this.IMAGES_WALKING);
    this.loadImages(this.IMAGES_JUMPING);
    this.loadImages(this.IMAGES_DEAD);
    this.loadImages(this.IMAGES_HURT);
    this.loadImages(this.IMAGES_LONG_IDLE);
    this.applyGravity();
    this.animate();
  }

  /**
   * Starts the timer that triggers the long idle animation if there is no interaction for 15 seconds.
   */
  startTimer() {
    if (this.timeoutLongIdle) return;
    this.longIdle = false;
    this.timeoutLongIdle = setTimeout(() => {
      this.longIdle = true;
    }, 15000);
  }

  /**
   * Stops the long idle timer and resets its state.
   */
  stopTimer() {
    clearTimeout(this.timeoutLongIdle);
    this.timeoutLongIdle = null;
    this.longIdle = false;
    this.world.audioManager.snoreSound.stop();
  }

  /**
   * Starts the movement and animation intervals, updating the character's position and state.
   */
  animate() {
    this.moveIntervall = setInterval(() => this.handleMovement(), 1000 / 60);
    this.characterInterval = setInterval(() => this.handleAnimation(), 150);
  }

  /**
   * Handles the character's movement on each frame, detecting directional input and collisions.
   */
  handleMovement() {
    const oldX = this.x;
    if (this.world.startGame && this.energy > 0) {
      this.handleDirection();
      this.handleJump();
      this.handleCollision(oldX);
    }
    this.world.camera_x = -this.x + 100;
  }

  /**
   * Handles horizontal movement based on keyboard input.
   */
  handleDirection() {
    if (this.world.keyboard.RIGHT && this.x < this.world.level.level_end_x) {
      this.moveRight();
      this.otherDirektion = false;
    }
    if (this.world.keyboard.LEFT && this.x > 0) {
      this.moveLeft();
      this.otherDirektion = true;
    }
  }

  /**
   * Allows the character to jump if the UP or SPACE key is pressed and the character is on the ground.
   */
  handleJump() {
    if (
      (this.world.keyboard.UP && !this.isAboveGround()) ||
      (this.world.keyboard.SPACE && !this.isAboveGround())
    ) {
      this.jump();
      this.world.audioManager.jumpSound.play();
    }
  }

  /**
   * Checks for collisions with enemies and reverts the character's x-position if a collision is detected.
   *
   * @param {number} oldX - The character's x-position before movement.
   */
  handleCollision(oldX) {
    this.world.level.enemies.forEach((enemy) => {
      if (!enemy.enemyIsDead && this.isColliding(enemy)) {
        this.x = oldX;
      }
    });
  }

  /**
   * Controls the character's animation state on each frame based on conditions like death, hurt, jumping, or idle.
   */
  handleAnimation() {
    let frameIndex = 0;
    if (this.isDead()) {
      this.handleDeadAnimation(frameIndex);
    } else if (this.isHurt()) {
      this.handleHurtAnimation();
    } else if (this.isAboveGround()) {
      this.handleJumpingAnimation();
    } else {
      this.handleIdleAnimation();
    }
  }

  /**
   * Plays the dead animation frame-by-frame, then stops character updates once the sequence is complete.
   *
   * @param {number} frameIndex - The current frame index in the dead animation sequence.
   */
  handleDeadAnimation(frameIndex) {
    this.playAnimation([this.IMAGES_DEAD[frameIndex]]);
    frameIndex++;
    if (frameIndex >= this.IMAGES_DEAD.length) {
      clearInterval(this.characterInterval);
      this.stopTimer();
      this.world.snoreSound.stop();
    }
  }

  /**
   * Plays the hurt animation frames, stops the long idle timer, and stops the snoring sound.
   */
  handleHurtAnimation() {
    this.playAnimation(this.IMAGES_HURT);
    this.stopTimer();
    this.world.audioManager.snoreSound.stop();
  }

  /**
   * Plays the jumping animation frames, stops the long idle timer, and stops the snoring sound.
   */
  handleJumpingAnimation() {
    this.playAnimation(this.IMAGES_JUMPING);
    this.stopTimer();
    this.world.audioManager.snoreSound.stop();
  }

  /**
   * Determines whether to play the walking animation, long idle (snoring) animation, or default idle animation
   * based on keyboard input and the long idle timer.
   */
  handleIdleAnimation() {
    if (this.world.keyboard.RIGHT || this.world.keyboard.LEFT) {
      this.playAnimation(this.IMAGES_WALKING);
      this.stopTimer();
      this.world.audioManager.snoreSound.stop();
    } else if (this.longIdle && this.world.startGame) {
      this.playAnimation(this.IMAGES_LONG_IDLE);
      this.world.audioManager.snoreSound.play();
    } else {
      this.playAnimation(this.IMAGES_IDLE);
      this.startTimer();
      this.world.audioManager.snoreSound.stop();
    }
  }

  /**
   * Resets the character to its default state for a new game or after certain conditions,
   * restoring position, energy, and other state values.
   */
  resetCharacter() {
    this.x = 100;
    this.energy = 100;
    this.enemyIsDead = false;
    this.otherDirektion = false;
    this.timeoutLongIdle = null;
    this.longIdle = false;
    this.animate();
  }
}