core/effect.js

import { Piece } from "./piece.js";
import { TRANSIENT } from "./flags.js";

/**
 * Base class for all effect types.
 *
 * Effects are transient pieces (not saved with the board) used to animate
 * thrown items, projectiles, fire, clouds, etc. Unlike terrain/items/agents,
 * effects are NOT immutable — they maintain frame state as they animate.
 *
 * An effect has an array of animation frames (Symbols). The AnimationManager
 * calls onTick() each frame to advance the effect.
 */
export class Effect extends Piece {
  /**
   * @param {string} name
   * @param {Symbol[]} frames - animation frame symbols
   * @param {string} color
   */
  constructor(name, frames, color) {
    if (!frames || frames.length === 0)
      throw new Error("Effect must have at least one frame");
    // Effects always have the TRANSIENT flag
    super(name, TRANSIENT, color, frames[0]);
    this.frames = frames;
    this.frameIndex = 0;
    this.done = false;
  }

  /** Current animation frame symbol. */
  get currentSymbol() {
    return this.frames[this.frameIndex];
  }

  /** True if this effect has finished animating and should be removed. */
  get isExpired() {
    return this.done;
  }

  /**
   * Should this effect render above an agent that occupies the same cell?
   * Override to return true for effects that should appear on top.
   * @returns {boolean}
   */
  isAboveAgent() {
    return false;
  }

  /**
   * Advance the effect by one tick. Called by AnimationManager each animation frame.
   * Override in subclasses to implement movement, spreading, damage, etc.
   * @param {GameEvent} event
   * @param {Board} board
   * @param {Cell} cell
   */
  onTick(event, board, cell) {
    this.frameIndex = (this.frameIndex + 1) % this.frames.length;
  }
}