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);
}