pieces/terrain/triggers.js

import { Decorator } from "./decorators.js";
import { Registry } from "../../core/registry.js";
import { TerrainUtils } from "../../core/terrain-utils.js";
import { events } from "../../core/events.js";
import { NONE, STEELBLUE, colorByName } from "../../core/color.js";
import { BaseSerializer } from "../../core/serializer.js";
import { getFlag } from "../../core/flags.js";

// ── Trigger base ──────────────────────────────────────────────────────────────

/**
 * Fires a color event (and optional message) when the player steps on it.
 * Persistent — fires every time.
 */
export class Trigger extends Decorator {
  constructor(terrain, color, message) {
    super(terrain, null, 0, color, null);
    this.message = message;
  }
  onEnterInternal(event, _player, cell, _dir) {
    if (event.isCancelled) {
      return;
    }
    event.board.fireColorEvent(event, this.color, cell);
    if (this.message) {
      events.fireModalMessage(this.message);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return new Trigger(
        _resolveTerrain(args[0]),
        colorByName(args[1]) ?? NONE,
        args[2] ?? null,
      );
    }
    store(t) {
      const base = `Trigger|${this.esc(t.terrain)}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new Trigger(Registry.get("Floor"), STEELBLUE, null);
    }
    template(_id) {
      return "Trigger|{terrain}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

/**
 * Fires only if the player has the specified flag or item.
 */
export class TriggerIf extends Trigger {
  constructor(terrain, test, color, message) {
    super(terrain, color, message);
    this.test = test;
  }
  onEnterInternal(event, player, cell, dir) {
    if (event.isCancelled) {
      return;
    }
    if (_playerHas(player, this.test)) {
      super.onEnterInternal(event, player, cell, dir);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return new TriggerIf(
        _resolveTerrain(args[0]),
        args[1],
        colorByName(args[2]) ?? NONE,
        args[3] ?? null,
      );
    }
    store(t) {
      const base = `TriggerIf|${this.esc(t.terrain)}|${t.test}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new TriggerIf(Registry.get("Floor"), "poisoned", STEELBLUE, null);
    }
    template(_id) {
      return "TriggerIf|{terrain}|{testValue}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

/**
 * Fires unless the player has the specified flag or item.
 */
export class TriggerIfNot extends Trigger {
  constructor(terrain, test, color, message) {
    super(terrain, color, message);
    this.test = test;
  }
  onEnterInternal(event, player, cell, dir) {
    if (event.isCancelled) {
      return;
    }
    if (!_playerHas(player, this.test)) {
      super.onEnterInternal(event, player, cell, dir);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return new TriggerIfNot(
        _resolveTerrain(args[0]),
        args[1],
        colorByName(args[2]) ?? NONE,
        args[3] ?? null,
      );
    }
    store(t) {
      const base = `TriggerIfNot|${this.esc(t.terrain)}|${t.test}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new TriggerIfNot(
        Registry.get("Floor"),
        "poisoned",
        STEELBLUE,
        null,
      );
    }
    template(_id) {
      return "TriggerIfNot|{terrain}|{testValue}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

// ── TriggerOnce ───────────────────────────────────────────────────────────────

/**
 * Fires once, then removes itself (replaces with the wrapped terrain).
 */
export class TriggerOnce extends Trigger {
  onEnterInternal(event, player, cell, dir) {
    if (event.isCancelled) {
      return;
    }
    super.onEnterInternal(event, player, cell, dir);
    TerrainUtils.removeDecorator(event, this);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([terrain, color, message]) {
      return new TriggerOnce(
        _resolveTerrain(terrain),
        colorByName(color) ?? NONE,
        message ?? null,
      );
    }
    store(t) {
      const base = `TriggerOnce|${this.esc(t.terrain)}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new TriggerOnce(Registry.get("Floor"), STEELBLUE, null);
    }
    template(_id) {
      return "TriggerOnce|{terrain}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

/**
 * Fires once if player has the flag/item, then removes itself.
 */
export class TriggerOnceIf extends TriggerOnce {
  constructor(terrain, test, color, message) {
    super(terrain, color, message);
    this.test = test;
  }
  onEnterInternal(event, player, cell, dir) {
    if (event.isCancelled || !_playerHas(player, this.test)) {
      return;
    }
    super.onEnterInternal(event, player, cell, dir);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return new TriggerOnceIf(
        _resolveTerrain(args[0]),
        args[1],
        colorByName(args[2]) ?? NONE,
        args[3] ?? null,
      );
    }
    store(t) {
      const base = `TriggerOnceIf|${this.esc(t.terrain)}|${t.test}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new TriggerOnceIf(
        Registry.get("Floor"),
        "poisoned",
        STEELBLUE,
        null,
      );
    }
    template(_id) {
      return "TriggerOnceIf|{terrain}|{testValue}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

/**
 * Fires once unless player has the flag/item, then removes itself.
 */
export class TriggerOnceIfNot extends TriggerOnce {
  constructor(terrain, test, color, message) {
    super(terrain, color, message);
    this.test = test;
  }
  onEnterInternal(event, player, cell, dir) {
    if (event.isCancelled || _playerHas(player, this.test)) {
      return;
    }
    super.onEnterInternal(event, player, cell, dir);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return new TriggerOnceIfNot(
        _resolveTerrain(args[0]),
        args[1],
        colorByName(args[2]) ?? NONE,
        args[3] ?? null,
      );
    }
    store(t) {
      const base = `TriggerOnceIfNot|${this.esc(t.terrain)}|${t.test}|${t.color.name}`;
      return t.message ? `${base}|${t.message}` : base;
    }
    example() {
      return new TriggerOnceIfNot(
        Registry.get("Floor"),
        "poisoned",
        STEELBLUE,
        null,
      );
    }
    template(_id) {
      return "TriggerOnceIfNot|{terrain}|{testValue}|{color}|{message?}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

// ── Drop / Pickup triggers ────────────────────────────────────────────────────

/**
 * Fires once when a specific item (matching flagStr/name) is dropped here.
 */
export class TriggerOnceOnDrop extends Decorator {
  constructor(terrain, color, itemName) {
    super(terrain, null, 0, color, null);
    this.itemName = itemName;
  }
  onDropInternal(event, cell, item) {
    if (event.isCancelled) {
      return;
    }
    if (item.name === this.itemName) {
      event.board.fireColorEvent(event, this.color, cell);
      TerrainUtils.removeDecorator(event, this);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([terrain, color, itemName]) {
      return new TriggerOnceOnDrop(
        _resolveTerrain(terrain),
        colorByName(color) ?? NONE,
        itemName,
      );
    }
    store(t) {
      return `TriggerOnceOnDrop|${this.esc(t.terrain)}|${t.color.name}|${t.itemName}`;
    }
    example() {
      return new TriggerOnceOnDrop(
        Registry.get("Floor"),
        STEELBLUE,
        "GoldCoin",
      );
    }
    template(_id) {
      return "TriggerOnceOnDrop|{terrain}|{color}|{itemName}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

/**
 * Fires once when a specific item is picked up from here.
 */
export class TriggerOnceOnPickup extends Decorator {
  constructor(terrain, color, flagStr) {
    super(terrain, null, 0, color, null);
    this.flagStr = flagStr;
  }
  onPickupInternal(event, cell, _agent, item) {
    if (event.isCancelled) {
      return;
    }
    if (item.name === this.flagStr || _itemMatchesFlag(item, this.flagStr)) {
      event.board.fireColorEvent(event, this.color, cell);
      TerrainUtils.removeDecorator(event, this);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([terrain, color, flagStr]) {
      return new TriggerOnceOnPickup(
        _resolveTerrain(terrain),
        colorByName(color) ?? NONE,
        flagStr,
      );
    }
    store(t) {
      return `TriggerOnceOnPickup|${this.esc(t.terrain)}|${t.color.name}|${t.flagStr}`;
    }
    example() {
      return new TriggerOnceOnPickup(
        Registry.get("Floor"),
        STEELBLUE,
        "Gold Coin",
      );
    }
    template(_id) {
      return "TriggerOnceOnPickup|{terrain}|{color}|{flag}";
    }
    tag() {
      return "Utility Terrain";
    }
  })();
}

// ── Registry ──────────────────────────────────────────────────────────────────

export function registerTriggers() {
  Registry.register("Trigger", Trigger.SERIALIZER);
  Registry.register("TriggerIf", TriggerIf.SERIALIZER);
  Registry.register("TriggerIfNot", TriggerIfNot.SERIALIZER);
  Registry.register("TriggerOnce", TriggerOnce.SERIALIZER);
  Registry.register("TriggerOnceIf", TriggerOnceIf.SERIALIZER);
  Registry.register("TriggerOnceIfNot", TriggerOnceIfNot.SERIALIZER);
  Registry.register("TriggerOnceOnDrop", TriggerOnceOnDrop.SERIALIZER);
  Registry.register("TriggerOnceOnPickup", TriggerOnceOnPickup.SERIALIZER);
}

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Resolve a terrain arg that may be either a plain string key or an already-resolved Piece. */
function _resolveTerrain(arg) {
  if (typeof arg === "string") {
    return Registry.get(arg);
  }
  return arg;
}

function _playerHas(player, test) {
  if (!player || !test) {
    return false;
  }
  const flag = getFlag(test);
  if (flag !== -1 && player.is(flag)) {
    return true;
  }
  return player.bag.find((i) => i.name === test) != null;
}

function _itemMatchesFlag(item, flagStr) {
  return item.name === flagStr;
}