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.
*
* @extends Decorator
*/
export class Trigger extends Decorator {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string|null} message - Optional modal message shown to the player.
*/
constructor(terrain, color, message) {
super(terrain, null, 0, color, null);
this.message = message;
}
/**
* Fires the color event and, if set, shows a modal message to the player.
* Skips silently when the event is already cancelled.
*
* @param {GameEvent} event
* @param {Player} _player - Unused.
* @param {Cell} cell
* @param {Direction} _dir - Unused.
*/
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 a color event (and optional message) when the player steps on it,
* but only when the player <em>possesses</em> the specified flag or item.
* Persistent — fires every time the condition is met.
*
* @extends Trigger
*/
export class TriggerIf extends Trigger {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {string} test - Flag name or item name whose <em>presence</em>
* allows the trigger to fire.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string|null} message - Optional modal message shown to the player.
*/
constructor(terrain, test, color, message) {
super(terrain, color, message);
this.test = test;
}
/**
* Fires the trigger if the player possesses the tested flag or item.
* Skips silently when the event is already cancelled or when the player
* lacks the test value.
*
* @override
* @param {GameEvent} event
* @param {Player} player
* @param {Cell} cell
* @param {Direction} dir
*/
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 a color event (and optional message) when the player steps on it,
* but only when the player does <em>not</em> possess the specified flag or
* item. Persistent — fires every time the condition is met.
*
* @extends Trigger
*/
export class TriggerIfNot extends Trigger {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {string} test - Flag name or item name whose <em>absence</em>
* allows the trigger to fire.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string|null} message - Optional modal message shown to the player.
*/
constructor(terrain, test, color, message) {
super(terrain, color, message);
this.test = test;
}
/**
* Fires the trigger if the player does not have the tested flag or item.
* Skips silently when the event is already cancelled or when the player
* possesses the test value.
*
* @override
* @param {GameEvent} event
* @param {Player} player
* @param {Cell} cell
* @param {Direction} dir
*/
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 a color event (and optional message) when the player steps on it,
* then removes itself so it will never fire again.
*
* Typical use: play a one-time scene or reward the first time a player
* reaches a location — e.g. show an introductory message when entering a
* new area for the first time.
*
* @extends Trigger
*/
export class TriggerOnce extends Trigger {
/**
* Fires the color event (delegating to {@link Trigger#onEnterInternal}),
* then removes this decorator from the cell.
* Skips silently when the event is already cancelled.
*
* @override
* @param {GameEvent} event
* @param {Player} player
* @param {Cell} cell
* @param {Direction} dir
*/
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";
}
})();
}
/**
* A one-shot trigger that fires only when the player <em>does</em> possess
* the specified flag or item. Once it fires it removes itself, so it will
* never fire again.
*
* Typical use: gate an event or reward on the player already carrying a
* required item — e.g. react to the player arriving at a location while
* holding the Chalice, then vanish so the scene plays out only once.
*
* @extends TriggerOnce
*/
export class TriggerOnceIf extends TriggerOnce {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {string} test - Flag name or item name whose <em>presence</em>
* allows the trigger to fire.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string|null} message - Optional modal message shown to the player.
*/
constructor(terrain, test, color, message) {
super(terrain, color, message);
this.test = test;
}
/**
* Fires the trigger if the player possesses the tested flag or item.
* Skips silently when the event is already cancelled or when the player
* lacks the test value. On a successful fire the trigger removes itself
* via {@link TriggerOnce#onEnterInternal}.
*
* @override
* @param {GameEvent} event
* @param {Player} player
* @param {Cell} cell
* @param {Direction} dir
*/
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";
}
})();
}
/**
* A one-shot trigger that fires only when the player does <em>not</em> possess
* the specified flag or item. Once it fires it removes itself, so it will
* never fire again.
*
* Typical use: gate a warning or event on the player lacking a required
* prerequisite — e.g. display a reminder message until the player picks up
* the required item, at which point the trigger is gone and never interrupts
* them again.
*
* @extends TriggerOnce
*/
export class TriggerOnceIfNot extends TriggerOnce {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {string} test - Flag name or item name whose <em>absence</em>
* allows the trigger to fire.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string|null} message - Optional modal message shown to the player.
*/
constructor(terrain, test, color, message) {
super(terrain, color, message);
this.test = test;
}
/**
* Fires the trigger if the player does not have the tested flag or item.
* Skips silently when the event is already cancelled or when the player
* possesses the test value. On a successful fire the trigger removes itself
* via {@link TriggerOnce#onEnterInternal}.
*
* @override
* @param {GameEvent} event
* @param {Player} player
* @param {Cell} cell
* @param {Direction} dir
*/
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 ────────────────────────────────────────────────────
/**
* A one-shot trigger that fires when a specific named item is dropped on
* this cell. Once it fires it removes itself, so it will never fire again.
*
* Typical use: detect when the player places a particular item at a location
* — e.g. an altar that reacts only when the Chalice is set down — then
* vanish so the scene plays out exactly once.
*
* @extends Decorator
*/
export class TriggerOnceOnDrop extends Decorator {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string} itemName - Exact item name that must be dropped to
* trigger the event.
*/
constructor(terrain, color, itemName) {
super(terrain, null, 0, color, null);
this.itemName = itemName;
}
/**
* Fires the color event and removes this trigger when the dropped item's
* name exactly matches {@link TriggerOnceOnDrop#itemName}.
* Skips silently when the event is already cancelled or the item does not
* match.
*
* @override
* @param {GameEvent} event
* @param {Cell} cell
* @param {Item} item - The item being dropped.
*/
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";
}
})();
}
/**
* A one-shot trigger that fires when a specific item is picked up from this
* cell. The item is matched by its exact name or by a flag string (using
* {@link _itemMatchesFlag}). Once it fires it removes itself, so it will
* never fire again.
*
* Typical use: react to the player collecting a key item from a location
* — e.g. the moment the player lifts the Amulet off a pedestal — then
* vanish so the event fires only once.
*
* @extends Decorator
*/
export class TriggerOnceOnPickup extends Decorator {
/**
* @param {Terrain} terrain - The underlying terrain this decorator wraps.
* @param {Color} color - Color event broadcast to the board on firing.
* @param {string} flagStr - Item name or flag string that must match the
* picked-up item to trigger the event.
*/
constructor(terrain, color, flagStr) {
super(terrain, null, 0, color, null);
this.flagStr = flagStr;
}
/**
* Fires the color event and removes this trigger when the picked-up item
* matches {@link TriggerOnceOnPickup#flagStr} by name or by flag.
* Skips silently when the event is already cancelled or the item does not
* match.
*
* @override
* @param {GameEvent} event
* @param {Cell} cell
* @param {Agent} _agent - Unused; the agent performing the pickup.
* @param {Item} item - The item being picked up.
*/
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;
}