core/animation-manager.ts

import { Board } from "./board.ts";
import { GameEvent } from "./game-event.ts";
import { Game } from "./game.ts";
import { Player } from "./player.ts";

const FRAME_MS = 80; // ~12.5 fps, matching Java's 80 ms period

/**
 * AnimationManager — drives all visual effects at ~12.5 fps using rAF.
 *
 * Each tick:
 *   1. Calls player.onFrame(cell) so damage/heal flashes fade
 *   2. Calls onFrame() on every animated piece via the board's AnimationProxy
 *      list (terrain, agents, items, effects including InFlightItem projectiles)
 *
 * The board and player references are set by the Game controller just before
 * the game loop starts via setContext().
 */
export class AnimationManager {
  #board!: Board;
  #player!: Player;
  #game!: Game;
  #running: boolean = false;
  #rafId: number | null = null;
  lastTime: number = 0;

  constructor() {
    this.loop = this.loop.bind(this);
  }

  loop(now: number) {
    if (!this.#running) {
      return;
    }
    this.#rafId = requestAnimationFrame(this.loop);

    if (now - this.lastTime < FRAME_MS) {
      return;
    }
    this.lastTime = now;
    this.tick();
  }

  /**
   * Inject game context. Called by the Game controller before start().
   */
  setContext(board: Board, player: Player, game: Game) {
    this.#board = board;
    this.#player = player;
    this.#game = game;
  }

  start() {
    if (this.#running) {
      return;
    }
    this.#running = true;
    this.lastTime = performance.now();
    this.#rafId = requestAnimationFrame(this.loop);
  }

  stop() {
    this.#running = false;
    if (this.#rafId !== null) {
      cancelAnimationFrame(this.#rafId);
      this.#rafId = null;
    }
  }

  isRunning(): boolean {
    return this.#rafId != null;
  }

  tick() {
    const board = this.#board;
    const player = this.#player;
    const event = new GameEvent(player, board, this.#game);

    // Advance player flash counters
    const playerCell = board.getCurrentCell();
    player.onFrame(playerCell);

    // Dispatch onFrame to all animated pieces (terrain, agents, items, effects).
    // Walking backwards mirrors Java: animations can remove themselves mid-tick
    // without corrupting the forward iteration.
    const animated = board.animated;
    for (let i = animated.length - 1; i >= 0; i--) {
      animated[i].tick(event, board);
    }
  }
}

export const animationManager = new AnimationManager();