pieces/items/items.js

import { Item } from "../../core/item.js";
import { Registry } from "../../core/registry.js";
import { events } from "../../core/events.js";
import { game } from "../../core/game.js";
import { ADJ_DIRECTIONS } from "../../core/direction.js";
import { AgentProxy } from "../../core/agent-proxy.js";
import { Paralyzed, Statue } from "../agents/creatures.js";
import {
  MELEE_WEAPON,
  RANGED_WEAPON,
  AMMUNITION,
  NOT_EDITABLE,
  REQUIRES_AMMO,
  MEAT,
  AQUATIC,
  LAVITIC,
  FIRE_RESISTANT,
  WATER_RESISTANT,
  CARNIVORE,
  DETECT_HIDDEN,
  POISON_RESISTANT,
  POISONED,
  PARALYZED,
  ORGANIC,
  PARALYSIS_RESISTANT,
  PLAYER,
  STONING_RESISTANT,
  TURNED_TO_STONE,
} from "../../core/flags.js";
import {
  BLACK,
  BLUE,
  BUILDING_FLOOR,
  BUILDING_WALL,
  BURLYWOOD,
  CHARTREUSE,
  DARKGOLDENROD,
  DARKKHAKI,
  DARKORANGE,
  DARKRED,
  DARKSLATEBLUE,
  DARKVIOLET,
  GOLD,
  GOLDENROD,
  GREEN,
  LIMEGREEN,
  LIGHTBLUE,
  LIGHTSALMON,
  LIGHTSTEELBLUE,
  MEDIUMAQUAMARINE,
  MEDIUMSPRINGGREEN,
  NEARBLACK,
  NONE,
  OLIVE,
  ORANGE,
  PEACHPUFF,
  PERU,
  PURPLE,
  RED,
  SADDLEBROWN,
  SILVER,
  STEELBLUE,
  TAN,
  WHEAT,
  WHITE,
  YELLOW,
  colorByName,
} from "../../core/color.js";
import { Symbol } from "../../core/symbol.js";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.js";
import { oscillate } from "../effects/effects.js";

// ── Keys ─────────────────────────────────────────────────────────────────────

export class Key extends Item {
  constructor(color) {
    super(color.name + " Key", 0, color, Symbol.of("~", color));
  }
  onUse(event) {
    event.cancelWithMessage("Try walking toward a door while holding the key.");
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]) {
      return new Key(colorByName(color) ?? NONE);
    }
    store(k) {
      return `Key|${k.color.name}`;
    }
    example() {
      return new Key(STEELBLUE);
    }
    template(_id) {
      return "Key|{color}";
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

// ── Mundane items ─────────────────────────────────────────────────────────────

export class Crowbar extends Item {
  constructor() {
    super("Crowbar", MELEE_WEAPON, Symbol.of("∫", WHITE, null, BLACK, null));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(25) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Crowbar");
    }
    create(_args) {
      return new Crowbar();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class GoldCoin extends Item {
  constructor() {
    super("Gold Coin", Symbol.of("•", GOLD));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("GoldCoin");
    }
    create(_args) {
      return new GoldCoin();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class Chalk extends Item {
  constructor() {
    super("Chalk", Symbol.of("-", WHITE, null, BLACK, null));
  }
  onUse(event) {
    const current = event.board.getCurrentCell();
    const terrain = current.terrain;
    if (terrain === Registry.get("Floor")) {
      current.setTerrain(Registry.get("ChalkedFloor"));
      events.fireMessage(current, "You mark the floor");
    } else if (terrain === Registry.get("ChalkedFloor")) {
      current.setTerrain(Registry.get("Floor"));
      events.fireMessage(current, "You wipe the chalk off the floor");
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Chalk");
    }
    create(_args) {
      return new Chalk();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

/** Crystal — purely cosmetic item with a custom color */
export class Crystal extends Item {
  constructor(color) {
    super(color.name + " Crystal", 0, color, Symbol.of("◊", color));
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]) {
      return new Crystal(colorByName(color) ?? NONE);
    }
    store(c) {
      return `Crystal|${c.color.name}`;
    }
    example() {
      return new Crystal(STEELBLUE);
    }
    template(_id) {
      return "Crystal|{color}";
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

// ── Food / healing ────────────────────────────────────────────────────────────

export class Apple extends Item {
  constructor() {
    super("Apple", Symbol.of("õ", RED));
  }
  onUse(event) {
    event.player.changeHealth(-5);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You eat the apple. (+5 HP)",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Apple");
    }
    create(_args) {
      return new Apple();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class Bread extends Item {
  constructor() {
    super("Bread", Symbol.of("∞", TAN));
  }
  onUse(event) {
    event.player.changeHealth(-10);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You eat the bread. (+10 HP)",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bread");
    }
    create(_args) {
      return new Bread();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class Fish extends Item {
  constructor() {
    super("Fish", MEAT, Symbol.of("α", DARKKHAKI));
  }
  onUse(event) {
    event.player.changeHealth(-25);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You eat the fish. (+25 HP)",
    );
    event.cancel();
  }
  onSteppedOn(event, agentLoc, agent) {
    // Carnivore agents eat fish on the ground (just removes it, no damage)
    if (agent.is?.(1 << 12 /* CARNIVORE */)) {
      agentLoc.removeItem(this);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Fish");
    }
    create(_args) {
      return new Fish();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class Kiwi extends Item {
  constructor() {
    super("Kiwi", Symbol.of("°", LIMEGREEN));
  }
  onUse(event) {
    event.player.changeHealth(-5);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell(),
      "You eat the kiwi. (+5 HP)",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Kiwi");
    }
    create(_args) {
      return new Kiwi();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

export class Mushroom extends Item {
  constructor() {
    super("Mushroom", Symbol.of("♠", WHEAT));
  }
  onUse(event) {
    event.player.changeHealth(-2);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You eat the mushroom. (+2 HP)",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Mushroom");
    }
    create(_args) {
      return new Mushroom();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

export class Healer extends Item {
  constructor() {
    super("Healer", Symbol.of("♥", RED));
  }
  onUse(event) {
    event.player.changeHealth(-50);
    event.player.bag.remove(this);
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You use the healer. (+50 HP)",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Healer");
    }
    create(_args) {
      return new Healer();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

export class PeachElixir extends Item {
  constructor() {
    super("Peach Elixir", Symbol.of("¡", PEACHPUFF, null, LIGHTSALMON, null));
  }
  onUse(event) {
    events.fireMessage(
      event.board.getCurrentCell(),
      "Delicious! (You can't be turned to stone, but the effect can wear off when used.)",
    );
    event.player.add(STONING_RESISTANT);
    event.player.bag.remove(this);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("PeachElixir");
    }
    create(_args) {
      return new PeachElixir();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

export class CopperPill extends Item {
  constructor() {
    super("Copper Pill", Symbol.of("θ", PERU));
  }
  onUse(event) {
    events.fireMessage(
      event.board.getCurrentCell(),
      "Is it healthy to swallow this much copper? (You can't be paralyzed, but the effect can wear off when used.)",
    );
    event.player.add(PARALYSIS_RESISTANT);
    event.player.bag.remove(this);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("CopperPill");
    }
    create(_args) {
      return new CopperPill();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

// ── Weapons ───────────────────────────────────────────────────────────────────

export class TerminusEst extends Item {
  constructor() {
    super(
      "Terminus Est",
      MELEE_WEAPON,
      Symbol.of("†", RED, null, DARKRED, null),
    );
  }
  onHit(event, cell, agent) {
    if (agent.changeHealth(50) === 0) event.kill(cell, agent);
  }
  randomSeed() {
    return true;
  }
  onFrame(event, cell, frame) {
    const color = !cell.board.outside
      ? oscillate(RED, ORANGE, 8, frame)
      : oscillate(DARKRED, DARKORANGE, 8, frame);
    cell._animItemSymbol = Symbol.of(this.symbol.entity, color);
    cell.board._notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("TerminusEst");
    }
    create(_args) {
      return new TerminusEst();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

export class Sword extends Item {
  constructor() {
    super("Sword", MELEE_WEAPON, Symbol.of("†", WHITE, null, BLACK));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(50) === 0) {
      event.kill(agentLoc, agent);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Sword");
    }
    create(_args) {
      return new Sword();
    }
    tag() {
      return "Weapons";
    }
  })();
}

export class Dagger extends Item {
  constructor() {
    super("Dagger", MELEE_WEAPON, Symbol.of("+", SILVER));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(25) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Dagger");
    }
    create(_args) {
      return new Dagger();
    }
    tag() {
      return "Weapons";
    }
  })();
}

export class Hammer extends Item {
  constructor() {
    super("Hammer", MELEE_WEAPON, Symbol.of("τ", SILVER));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(35) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Hammer");
    }
    create(_args) {
      return new Hammer();
    }
    tag() {
      return "Weapons";
    }
  })();
}

// ── Ranged weapons ────────────────────────────────────────────────────────────

/**
 * Check whether the player has loaded ammo for `weapon`. If so, decrement the
 * ammo count and return `ammoItem` to be fired. Otherwise fire an "Out of ammo"
 * message and return null.
 * @param {GameEvent} event
 * @param {Item} weapon   — the weapon item (used to locate the bag entry)
 * @param {Item} ammoItem — the projectile to return when ammo is available
 * @returns {Item|null}
 */
function assessAmmo(event, weapon, ammoItem) {
  const entry = event.player.bag._findEntry(weapon);
  if (entry && entry.ammo > 0) {
    entry.ammo--;
    events.fireInventoryChanged(event.player.bag);
    return ammoItem;
  }
  events.fireMessage(event.board.getCurrentCell(), "Out of ammo");
  return null;
}

export class Bow extends Item {
  constructor() {
    super("Bow", RANGED_WEAPON, Symbol.of(")", SADDLEBROWN));
    this._arrow = Registry.get("Arrow");
  }
  onFire(_event) {
    return this._arrow;
  }
  onUse(event) {
    event.cancelWithMessage(
      "Aim and fire with a direction key while holding the bow.",
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bow");
    }
    create(_args) {
      return new Bow();
    }
    tag() {
      return "Weapons";
    }
  })();
}

export class Arrow extends Item {
  constructor() {
    super("Arrow", AMMUNITION, Symbol.of("'", SADDLEBROWN));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(20) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Arrow");
    }
    create(_args) {
      return new Arrow();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

export class Rock extends Item {
  constructor() {
    super("Rock", Symbol.of("•", WHITE, null, BLACK, null));
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(10) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Rock");
    }
    create(_args) {
      return new Rock();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

export class SlingRock extends Item {
  constructor() {
    super(
      "Rock",
      AMMUNITION | NOT_EDITABLE,
      Symbol.of("•", SILVER, null, NEARBLACK, null),
    );
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(5) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("SlingRock");
    }
    create(_args) {
      return new SlingRock();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

export class Sling extends Item {
  constructor() {
    super("Sling", RANGED_WEAPON, Symbol.of("ϑ", SADDLEBROWN));
    this._rock = Registry.get("SlingRock");
  }
  onFire(_event) {
    return this._rock;
  }
  onUse(event) {
    event.cancelWithMessage(
      "Aim and fire with a direction key while holding the sling.",
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Sling");
    }
    create(_args) {
      return new Sling();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * AmmoBow — a bow that tracks arrow ammo. Fires only when the player has
 * arrows loaded; each shot consumes one arrow from the weapon's ammo count.
 */
export class AmmoBow extends Item {
  constructor() {
    super("Bow", RANGED_WEAPON | REQUIRES_AMMO, Symbol.of(")", SADDLEBROWN));
    this._arrow = Registry.get("Arrow");
  }
  onFire(event) {
    return assessAmmo(event, this, this._arrow);
  }
  onUse(event) {
    event.cancelWithMessage(
      "Use shift-direction to aim at something (requires arrow ammo).",
    );
  }
  getAmmoType() {
    return this._arrow;
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("AmmoBow");
    }
    create(_args) {
      return new AmmoBow();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * AmmoGun — a gun that tracks bullet ammo. Fires only when the player has
 * bullets loaded; each shot consumes one bullet from the weapon's ammo count.
 */
export class AmmoGun extends Item {
  constructor() {
    super(
      "Gun",
      RANGED_WEAPON | REQUIRES_AMMO,
      Symbol.of("¬", WHITE, null, BLACK, null),
    );
    this._bullet = Registry.get("Bullet");
  }
  onFire(event) {
    return assessAmmo(event, this, this._bullet);
  }
  onUse(event) {
    event.cancelWithMessage?.(
      "Use shift-direction to fire at something (requires bullet ammo).",
    );
  }
  getAmmoType() {
    return this._bullet;
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("AmmoGun");
    }
    create(_args) {
      return new AmmoGun();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * AmmoParalyzer — a paralyzer gun that tracks parabullet ammo. Fires only when
 * the player has parabullets loaded; each shot consumes one from the ammo count.
 */
export class AmmoParalyzer extends Item {
  constructor() {
    super(
      "Paralyzer",
      RANGED_WEAPON | REQUIRES_AMMO,
      Symbol.of("¬", DARKVIOLET),
    );
    this._bullet = Registry.get("Parabullet");
  }
  onFire(event) {
    return assessAmmo(event, this, this._bullet);
  }
  onUse(event) {
    event.cancelWithMessage?.(
      "Use shift-direction to fire at something (requires paralyzer bullet ammo).",
    );
  }
  getAmmoType() {
    return this._bullet;
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("AmmoParalyzer");
    }
    create(_args) {
      return new AmmoParalyzer();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * AmmoSling — a sling that tracks sling-rock ammo. Fires only when the player
 * has rocks loaded; each shot consumes one rock from the weapon's ammo count.
 */
export class AmmoSling extends Item {
  constructor() {
    super("Sling", RANGED_WEAPON | REQUIRES_AMMO, Symbol.of("ϑ", SADDLEBROWN));
    this._rock = Registry.get("SlingRock");
  }
  onFire(event) {
    return assessAmmo(event, this, this._rock);
  }
  onUse(event) {
    event.cancelWithMessage(
      "Use shift-direction to aim at something (requires rock ammo).",
    );
  }
  getAmmoType() {
    return Registry.get("Rock");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("AmmoSling");
    }
    create(_args) {
      return new AmmoSling();
    }
    tag() {
      return "Weapons";
    }
  })();
}

// ── Rings / wearable ──────────────────────────────────────────────────────────

export class BlueRing extends Item {
  constructor() {
    super("Blue Ring", Symbol.of("o", BLUE));
  }
  onSelect(event, cell) {
    event.player.add(WATER_RESISTANT);
  }
  onDeselect(event, cell) {
    if (cell.terrain.is(AQUATIC)) {
      event.cancelWithMessage(
        "You can't swim. You've got to keep the ring on.",
      );
    } else {
      event.player.remove(WATER_RESISTANT);
    }
  }
  onDrop(event, cell) {
    if (cell.terrain.is(AQUATIC)) {
      event.cancelWithMessage("You'd drown. Really.");
    }
  }
  onThrow(event, cell) {
    if (cell.terrain.is(AQUATIC)) {
      event.cancelWithMessage("That would be suicide");
    }
  }
  onUse(event) {
    event.cancelWithMessage("You can't remove the ring while wearing it.");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("BlueRing");
    }
    create(_args) {
      return new BlueRing();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

export class RedRing extends Item {
  constructor() {
    super("Red Ring", Symbol.of("o", RED));
  }
  onSelect(event, _cell) {
    event.player.add(FIRE_RESISTANT);
  }
  onDeselect(event, cell) {
    if (cell.terrain.is(LAVITIC)) {
      event.cancelWithMessage(
        "You're standing in lava. You can't remove the ring.",
      );
    } else {
      event.player.remove(FIRE_RESISTANT);
    }
  }
  onDrop(event, cell) {
    if (cell.terrain.is(LAVITIC)) {
      event.cancelWithMessage("The ring would melt. Don't throw it into lava.");
    }
  }
  onThrow(event, cell) {
    if (cell.terrain.is(LAVITIC)) {
      event.cancelWithMessage("That would destroy it.");
    }
  }
  onUse(event) {
    event.cancelWithMessage("You can't remove the ring while wearing it.");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("RedRing");
    }
    create(_args) {
      return new RedRing();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

// ── Scrolls ───────────────────────────────────────────────────────────────────

export class Scroll extends Item {
  constructor(name, url) {
    super(`Scroll: “${name}”`, Symbol.of("§", WHITE, null, BLACK, null));
    this._scrollName = name;
    this.url = url ?? null;
  }
  getIndefiniteNoun(phrase) {
    return phrase.replace("{0}", `a scroll: “${this._scrollName}”`);
  }
  onUse(event) {
    if (this.url) {
      events.fireModalMessage(`[Scroll: ${this._scrollName}] See: ${this.url}`);
    } else {
      events.fireMessage(
        event.player.getCurrentCell?.(),
        "It says nothing unexpected.",
      );
    }
    event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args) {
      return args.length >= 2
        ? new Scroll(args[0], args[1])
        : new Scroll(args[0]);
    }
    store(s) {
      return s.url
        ? `Scroll|${s._scrollName}|${s.url}`
        : `Scroll|${s._scrollName}`;
    }
    example() {
      return new Scroll("An Example");
    }
    template(_id) {
      return "Scroll|{name}|{url?}";
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

// ── Artifacts ─────────────────────────────────────────────────────────────────

export class GoldenHarp extends Item {
  constructor() {
    super("Golden Harp", Symbol.of("ש", GOLD, null, DARKGOLDENROD, null));
  }
  onUse(event) {
    events.fireMessage(
      event.player.getCurrentCell?.(),
      "You play a tune on the harp. Something stirs...",
    );
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("GoldenHarp");
    }
    create(_args) {
      return new GoldenHarp();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

export class SilverAnkh extends Item {
  constructor() {
    super("Silver Ankh", Symbol.of("☥", SILVER, null, BUILDING_WALL, null));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("SilverAnkh");
    }
    create(_args) {
      return new SilverAnkh();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

export class Chalice extends Item {
  static SYMBOLS = [
    Symbol.of("Y", GOLD),
    Symbol.of("Y", RED),
    Symbol.of("Y", YELLOW, null, BLACK, null),
    Symbol.of("Y", GREEN),
    Symbol.of("Y", WHITE),
  ];
  constructor() {
    super("The Chalice", Chalice.SYMBOLS[0]);
  }
  randomSeed() {
    return true;
  }
  onFrame(_event, cell, frame) {
    cell._animItemSymbol = Chalice.SYMBOLS[frame % 5];
    cell.board._notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Chalice");
    }
    create(_args) {
      return new Chalice();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

export class HelmOfTheAsciiroth extends Item {
  constructor() {
    super("Helm of the Asciiroth", Symbol.of("Ω", WHITE, null, BLACK, null));
    this._stoneray = Registry.get("Stoneray");
  }
  randomSeed() {
    return true;
  }
  onUse(event) {
    const cell = event.board.getCurrentCell();
    for (const dir of ADJ_DIRECTIONS) {
      const e = game.createEvent();
      game.shoot(e, cell, event.player, this._stoneray, dir);
    }
    event.player.changeHealth(10);
    event.cancel();
  }
  onFrame(_event, cell, frame) {
    const outside = cell.board.outside;
    const bg = cell.terrain?.symbol?.getBackground(outside) ?? null;
    const nbg = oscillate(bg, GREEN, 10, frame);
    cell._animItemSymbol = Symbol.of("Ω", WHITE, nbg, BLACK, nbg);
    cell.board._notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("HelmOfTheAsciiroth");
    }
    create(_args) {
      return new HelmOfTheAsciiroth();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

/**
 * MirrorShield — when wielded, Optilisks and Cephalids will not shoot at the
 * player. If a Parabullet or Stoneray does hit the player while the shield is
 * held, it is reflected back in the reverse direction.
 */
export class MirrorShield extends Item {
  constructor() {
    super("The Mirror Shield", Symbol.of("ø", WHITE, null, BLACK, null));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("MirrorShield");
    }
    create(_args) {
      return new MirrorShield();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

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

/**
 * Bomb — explodes when a non-player agent steps on it.
 */
export class Bomb extends Item {
  constructor() {
    super("Bomb", 0, WHITE, Symbol.of("δ", WHITE, null, BLACK, null));
  }
  onSteppedOn(event, agentLoc, agent) {
    if (agent.is(PLAYER)) return;
    agentLoc.removeItem(this);
    agentLoc.explosion?.(event.player);
    events.fireMessage(agentLoc, "The bomb explodes!");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bomb");
    }
    create(_args) {
      return new Bomb();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * Bone — MEAT item; carnivore agents will occasionally eat it.
 */
export class Bone extends Item {
  constructor() {
    super("Bone", MEAT, WHITE, Symbol.of("ι", WHITE, null, BLACK, null));
  }
  onSteppedOn(_event, agentLoc, agent) {
    if (agent.is(CARNIVORE) && Math.random() < 0.05) {
      agentLoc.removeItem(this);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bone");
    }
    create(_args) {
      return new Bone();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

/**
 * Bullet — AMMUNITION for the Gun. Damages agents on hit.
 */
export class Bullet extends Item {
  constructor() {
    super(
      "Bullet",
      AMMUNITION,
      Symbol.of("•", LIGHTSTEELBLUE, null, DARKSLATEBLUE, null),
    );
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(30) === 0) event.kill(agentLoc, agent);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bullet");
    }
    create(_args) {
      return new Bullet();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

/**
 * Parabullet — AMMUNITION for the Paralyzer. Paralyzes agents on hit.
 */
export class Parabullet extends Item {
  constructor() {
    super("Paralyzer Bullet", AMMUNITION, Symbol.of("•", DARKVIOLET));
  }
  onHit(event, agentLoc, agent) {
    // Do not double-wrap an already-proxied agent.
    if (agent instanceof AgentProxy) {
      return;
    }
    if (agent.is(PLAYER)) {
      if (event.player.testResistance(PARALYSIS_RESISTANT)) {
        return;
      }
      event.player.add(PARALYZED);
    }
    agentLoc.setAgent(new Paralyzed(agent));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Parabullet");
    }
    create(_args) {
      return new Parabullet();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

/**
 * FishingPole — used at a FishPool to catch fish.
 */
export class FishingPole extends Item {
  constructor() {
    super("Fishing Pole", Symbol.of("ſ", SADDLEBROWN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("FishingPole");
    }
    create(_args) {
      return new FishingPole();
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

/**
 * GlassEye — grants DETECT_HIDDEN while selected.
 */
export class GlassEye extends Item {
  constructor() {
    super(
      "The Glass Eye",
      Symbol.of("Θ", MEDIUMSPRINGGREEN, null, MEDIUMAQUAMARINE, null),
    );
  }
  onSelect(event, _cell) {
    event.player.add(DETECT_HIDDEN);
  }
  onDeselect(event, _cell) {
    event.player.remove(DETECT_HIDDEN);
  }
  onDrop(event, _cell) {
    event.player.remove(DETECT_HIDDEN);
  }
  onThrow(event, _cell) {
    event.player.remove(DETECT_HIDDEN);
  }
  onUse(event) {
    event.cancelWithMessage?.("You see everything more clearly");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("GlassEye");
    }
    create(_args) {
      return new GlassEye();
    }
    tag() {
      return "Artifacts";
    }
  })();
}

/**
 * Grenade — thrown item that explodes on landing.
 */
export class Grenade extends Item {
  constructor() {
    super("Grenade", Symbol.of("σ", OLIVE));
  }
  onUse(event) {
    event.cancelWithMessage("Just throw a grenade to use it, but stand back");
  }
  onThrowEnd(event, cell) {
    event.cancel();
    cell.explosion(event.player);
    events.fireMessage(cell, "The grenade explodes!");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Grenade");
    }
    create(_args) {
      return new Grenade();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * Gun — RANGED_WEAPON that fires bullets.
 */
export class Gun extends Item {
  constructor() {
    super("Gun", RANGED_WEAPON, Symbol.of("¬", WHITE, null, BLACK, null));
  }
  onFire(_event) {
    return Registry.get("Bullet");
  }
  onUse(event) {
    event.cancelWithMessage?.("Use shift-direction to fire at something");
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Gun");
    }
    create(_args) {
      return new Gun();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * KelpSmoothie — grants POISON_RESISTANT when used (consumed on use).
 */
export class KelpSmoothie extends Item {
  constructor() {
    super("Kelp Smoothie", Symbol.of("¡", CHARTREUSE));
  }
  onUse(event) {
    events.fireMessage(
      event.player?.getCurrentCell?.(),
      "Truly foul. (You can't be poisoned, but the effect can wear off when used.)",
    );
    event.player?.add?.(POISON_RESISTANT);
    event.player?.bag?.remove?.(this);
    event.cancel?.();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("KelpSmoothie");
    }
    create(_args) {
      return new KelpSmoothie();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

/**
 * Paralyzer — RANGED_WEAPON that fires paralyzer bullets.
 */
export class Paralyzer extends Item {
  constructor() {
    super("Paralyzer", RANGED_WEAPON, Symbol.of("¬", DARKVIOLET));
  }
  onFire(_event) {
    return Registry.get("Parabullet");
  }
  onUse(event) {
    event.cancelWithMessage?.(
      "Use shift-direction to fire at something (requires paralyzer bullet ammo)",
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Paralyzer");
    }
    create(_args) {
      return new Paralyzer();
    }
    tag() {
      return "Weapons";
    }
  })();
}

/**
 * ProteinBar — cures the WEAK condition when used.
 */
export class ProteinBar extends Item {
  constructor() {
    super("Protein Bar", Symbol.of("=", BURLYWOOD, null, PERU, null));
  }
  onUse(event) {
    event.player?.cureWeakness?.(event, this);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("ProteinBar");
    }
    create(_args) {
      return new ProteinBar();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

/**
 * PurpleMushroom — cures the POISONED condition when used.
 */
export class PurpleMushroom extends Item {
  constructor() {
    super("Purple Mushroom", Symbol.of("♠", PURPLE));
  }
  onUse(event) {
    event.player?.curePoison?.(event, this);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("PurpleMushroom");
    }
    create(_args) {
      return new PurpleMushroom();
    }
    tag() {
      return "Power-Ups";
    }
  })();
}

/**
 * EuclideanShard — colored item used to power an EuclideanEngine.
 */
export class EuclideanShard extends Item {
  constructor(color) {
    super(color.name + " Euclidean Shard", 0, color, Symbol.of("◆", color));
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]) {
      return new EuclideanShard(colorByName(color) ?? NONE);
    }
    store(s) {
      return `EuclideanShard|${s.color.name}`;
    }
    example() {
      return new EuclideanShard(STEELBLUE);
    }
    template(_id) {
      return "EuclideanShard|{color}";
    }
    tag() {
      return "Mundane Items";
    }
  })();
}

// ── Agent-only projectile items ───────────────────────────────────────────────

/**
 * Agentray — ammunition that releases an embedded agent on the target cell
 * when it lands. If the cell is already occupied, the shot has no effect.
 */
export class Agentray extends Item {
  /** @param {Agent} agent */
  constructor(agent) {
    super(
      "Agentray",
      AMMUNITION | NOT_EDITABLE,
      Symbol.of("°", agent.symbol.color, null, agent.symbol.outsideColor, null),
    );
    this.agent = agent;
  }
  onThrowEnd(event, cell) {
    event.cancel();
    if (cell.agent == null) {
      cell.setAgent(this.agent);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([agent]) {
      return new Agentray(agent);
    }
    store(a) {
      return `Agentray|${Registry.serialize(a.agent).replace(/\|/g, "^")}`;
    }
    example() {
      return new Agentray(Registry.get("Sleestak"));
    }
    template(_id) {
      return "Agentray|{agent}";
    }
    tag() {
      return "Ammunition";
    }
  })();
}

/** PoisonDart — fired by Triffids. Damages and poisons the player. */
export class PoisonDart extends Item {
  constructor() {
    super(
      "Poison Dart",
      AMMUNITION | NOT_EDITABLE,
      Symbol.of("'", SADDLEBROWN),
    );
  }
  onHit(event, agentLoc, agent) {
    if (agent.changeHealth(10) === 0) {
      event.kill(agentLoc, agent);
    }
    if (
      agent.is(PLAYER) &&
      event.player &&
      !event.player.testResistance?.(POISON_RESISTANT)
    ) {
      event.player.add?.(POISONED);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("PoisonDart");
    }
    create(_args) {
      return new PoisonDart();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

const _STONERAY_SYMBOLS = [
  Symbol.of("×", LIGHTBLUE, null, BLUE, null),
  Symbol.of("+", LIGHTBLUE, null, BLUE, null),
];

/** Stoneray — fired by Cephalids. Turns the target to stone. */
export class Stoneray extends Item {
  constructor() {
    super("Stoning Bullet", AMMUNITION | NOT_EDITABLE, _STONERAY_SYMBOLS[0]);
  }
  getSymbol() {
    return _STONERAY_SYMBOLS[Math.random() < 0.5 ? 0 : 1];
  }
  onHit(event, agentLoc, agent) {
    if (agent.is(ORGANIC) && !(agent instanceof AgentProxy)) {
      if (agent.is(PLAYER)) {
        const player = event.player;
        if (
          player.testResistance(PARALYSIS_RESISTANT) ||
          player.testResistance(STONING_RESISTANT)
        ) {
          return;
        }
        player.add(TURNED_TO_STONE);
      }
      agentLoc.setAgent(new Statue(agent, NONE));
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Stoneray");
    }
    create(_args) {
      return new Stoneray();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

/** Fireball — fired by Thermadon. Deals heavy fire damage. */
const _FIREBALL_SYMBOLS = [
  Symbol.of("*", RED),
  Symbol.of("*", ORANGE),
  Symbol.of("*", RED),
  Symbol.of("*", YELLOW),
];

export class Fireball extends Item {
  constructor() {
    super("Fireball", AMMUNITION | NOT_EDITABLE, _FIREBALL_SYMBOLS[0]);
  }
  getSymbol() {
    return _FIREBALL_SYMBOLS[Math.floor(Math.random() * 4)];
  }
  onThrowEnd(event, cell) {
    event.cancel();
    cell.explosion(event.player);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Fireball");
    }
    create(_args) {
      return new Fireball();
    }
    tag() {
      return "Ammunition";
    }
  })();
}

export function registerItems() {
  Registry.register("Key", Key.SERIALIZER);
  Registry.register("Crowbar", Crowbar.SERIALIZER);
  Registry.register("GoldCoin", GoldCoin.SERIALIZER);
  Registry.register("Chalk", Chalk.SERIALIZER);
  Registry.register("Crystal", Crystal.SERIALIZER);
  Registry.register("Apple", Apple.SERIALIZER);
  Registry.register("Bread", Bread.SERIALIZER);
  Registry.register("Fish", Fish.SERIALIZER);
  Registry.register("Kiwi", Kiwi.SERIALIZER);
  Registry.register("Mushroom", Mushroom.SERIALIZER);
  Registry.register("Healer", Healer.SERIALIZER);
  Registry.register("PeachElixir", PeachElixir.SERIALIZER);
  Registry.register("CopperPill", CopperPill.SERIALIZER);
  Registry.register("Sword", Sword.SERIALIZER);
  Registry.register("Dagger", Dagger.SERIALIZER);
  Registry.register("TerminusEst", TerminusEst.SERIALIZER);
  Registry.register("Hammer", Hammer.SERIALIZER);
  Registry.register("Bow", Bow.SERIALIZER);
  Registry.register("AmmoBow", AmmoBow.SERIALIZER);
  Registry.register("Arrow", Arrow.SERIALIZER);
  Registry.register("Rock", Rock.SERIALIZER);
  Registry.register("SlingRock", SlingRock.SERIALIZER);
  Registry.register("Sling", Sling.SERIALIZER);
  Registry.register("AmmoSling", AmmoSling.SERIALIZER);
  Registry.register("BlueRing", BlueRing.SERIALIZER);
  Registry.register("RedRing", RedRing.SERIALIZER);
  Registry.register("Scroll", Scroll.SERIALIZER);
  Registry.register("GoldenHarp", GoldenHarp.SERIALIZER);
  Registry.register("SilverAnkh", SilverAnkh.SERIALIZER);
  Registry.register("Chalice", Chalice.SERIALIZER);
  Registry.register("HelmOfTheAsciiroth", HelmOfTheAsciiroth.SERIALIZER);
  Registry.register("MirrorShield", MirrorShield.SERIALIZER);
  Registry.register("Bomb", Bomb.SERIALIZER);
  Registry.register("Bone", Bone.SERIALIZER);
  Registry.register("Bullet", Bullet.SERIALIZER);
  Registry.register("Parabullet", Parabullet.SERIALIZER);
  Registry.register("FishingPole", FishingPole.SERIALIZER);
  Registry.register("GlassEye", GlassEye.SERIALIZER);
  Registry.register("Grenade", Grenade.SERIALIZER);
  Registry.register("Gun", Gun.SERIALIZER);
  Registry.register("AmmoGun", AmmoGun.SERIALIZER);
  Registry.register("KelpSmoothie", KelpSmoothie.SERIALIZER);
  Registry.register("Paralyzer", Paralyzer.SERIALIZER);
  Registry.register("AmmoParalyzer", AmmoParalyzer.SERIALIZER);
  Registry.register("ProteinBar", ProteinBar.SERIALIZER);
  Registry.register("PurpleMushroom", PurpleMushroom.SERIALIZER);
  Registry.register("Agentray", Agentray.SERIALIZER);
  Registry.register("PoisonDart", PoisonDart.SERIALIZER);
  Registry.register("Stoneray", Stoneray.SERIALIZER);
  Registry.register("Fireball", Fireball.SERIALIZER);
  Registry.register("EuclideanShard", EuclideanShard.SERIALIZER);
}