import { ADJ_DIRECTIONS } from "../../core/direction.ts";
import { AgentProxy } from "../../core/agent-proxy.ts";
import { events } from "../../core/event-bus.ts";
import { game } from "../../core/game.ts";
import { Item } from "../../core/item.ts";
import { Paralyzed, Pusher, Slider, Statue } from "../agents/creatures.ts";
import { Registry } from "../../core/registry.ts";
import {
AMMUNITION,
AQUATIC,
CARNIVORE,
DETECT_HIDDEN,
FIRE_RESISTANT,
LAVITIC,
MEAT,
MELEE_WEAPON,
NOT_EDITABLE,
ORGANIC,
PARALYSIS_RESISTANT,
PARALYZED,
PLAYER,
POISON_RESISTANT,
POISONED,
RANGED_WEAPON,
REQUIRES_AMMO,
STONING_RESISTANT,
TURNED_TO_STONE,
WATER_RESISTANT,
} from "../../core/flags.ts";
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,
SALMON,
SILVER,
STEELBLUE,
TAN,
WHEAT,
WHITE,
YELLOW,
colorByName,
Color,
} from "../../core/color.ts";
import { Agent } from "../../core/agent.ts";
import { Animated } from "../../core/animated.ts";
import { Cell } from "../../core/cell.ts";
import { GameEvent } from "../../core/game-event.ts";
import { oscillate } from "../effects/effects.ts";
import { Piece } from "../../core/piece.ts";
import { Sym } from "../../core/sym.ts";
import { Terrain } from "../../core/terrain.ts";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.ts";
export interface HasAmmo {
getAmmoType: () => Item;
}
export function hasAmmo(item: any): item is HasAmmo {
return ("getAmmoType" in item);
}
export class Key extends Item {
constructor(color: Color) {
super(color.name + " Key", 0, color, Sym.of("~", color));
}
onUse(event: GameEvent) {
event.cancel("Try walking toward a door while holding the key.");
}
static SERIALIZER = new (class extends BaseSerializer {
create([color]: string[]) {
return new Key(colorByName(color) ?? NONE);
}
store(k: Key) {
return `Key|${k.color.name}`;
}
example() {
return new Key(STEELBLUE);
}
template() {
return "Key|{color}";
}
tag() {
return "Mundane Items";
}
})();
}
export class Crowbar extends Item {
constructor() {
super("Crowbar", MELEE_WEAPON, NONE, Sym.of("∫", WHITE, null, BLACK, null));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(0) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Crowbar");
}
create() {
return new Crowbar();
}
tag() {
return "Mundane Items";
}
})();
}
export class GoldCoin extends Item {
constructor() {
super("Gold Coin", 0, NONE, Sym.of("•", GOLD, null, GOLDENROD, null));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("GoldCoin");
}
create() {
return new GoldCoin();
}
tag() {
return "Mundane Items";
}
})();
}
export class Chalk extends Item {
constructor() {
super("Chalk", 0, NONE, Sym.of("-", WHITE, null, BLACK, null));
}
onUse(event: GameEvent) {
const current = event.board.getCurrentCell();
const terrain = current.terrain;
if (terrain === Registry.terrain("Floor")) {
current.setTerrain(Registry.terrain("ChalkedFloor"));
events.fireMessage("You mark the floor", current);
} else if (terrain === Registry.terrain("ChalkedFloor")) {
current.setTerrain(Registry.terrain("Floor"));
events.fireMessage("You wipe the chalk off the floor", current);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Chalk");
}
create() {
return new Chalk();
}
tag() {
return "Mundane Items";
}
})();
}
/** Crystal — purely cosmetic item with a custom color */
export class Crystal extends Item {
constructor(color: Color) {
super(color.name + " Crystal", 0, color, Sym.of("◊", color));
}
static SERIALIZER = new (class extends BaseSerializer {
create([color]: string[]) {
return new Crystal(colorByName(color) ?? NONE);
}
store(c: Crystal) {
return `Crystal|${c.color.name}`;
}
example() {
return new Crystal(STEELBLUE);
}
template() {
return "Crystal|{color}";
}
tag() {
return "Mundane Items";
}
})();
}
export class Apple extends Item {
constructor() {
super("Apple", 0, NONE, Sym.of("õ", RED));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 5, event.board);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Apple");
}
create() {
return new Apple();
}
tag() {
return "Mundane Items";
}
})();
}
export class Bread extends Item {
constructor() {
super("Bread", 0, NONE, Sym.of("∞", TAN));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 10, event.board);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Bread");
}
create() {
return new Bread();
}
tag() {
return "Mundane Items";
}
})();
}
export class Fish extends Item {
constructor() {
super("Fish", MEAT, NONE, Sym.of("α", DARKKHAKI));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 25, event.board);
event.cancel();
}
onSteppedOn(event: GameEvent, agentLoc: Cell, agent: Agent) {
// Carnivore agents occasionally eat fish on the ground, leaving a bone behind.
if (agent.is(CARNIVORE) && Math.random() * 100 < 15) {
agentLoc.removeItem(this);
agentLoc.addItem(Registry.item("Bone"));
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Fish");
}
create() {
return new Fish();
}
tag() {
return "Mundane Items";
}
})();
}
// I'm leaving this hear because the AI created this and it's not a
// piece in the original game, nor does it act like a fish! Weird & wonderful
/** Salmon — passive organic aquatic creature; lives in water. */
export class Salmon extends Item {
constructor() {
super("Salmon", MEAT, NONE, Sym.of("α", SALMON));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 35, event.board);
event.cancel();
}
onSteppedOn(event: GameEvent, agentLoc: Cell, agent: Agent) {
// Carnivore agents eat fish on the ground (just removes it, no damage)
if (agent.is(CARNIVORE)) {
agentLoc.removeItem(this);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Salmon");
}
create() {
return new Salmon();
}
tag() {
return "Mundane Items";
}
})();
}
export class Kiwi extends Item {
constructor() {
super("Kiwi", 0, NONE, Sym.of("°", LIMEGREEN));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 5, event.board);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Kiwi");
}
create() {
return new Kiwi();
}
tag() {
return "Power-Ups";
}
})();
}
export class Mushroom extends Item {
constructor() {
super("Mushroom", 0, NONE, Sym.of("♠", WHEAT, null, PERU, null));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 2, event.board);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Mushroom");
}
create() {
return new Mushroom();
}
tag() {
return "Mundane Items";
}
})();
}
export class Healer extends Item {
constructor() {
super("Healer", 0, NONE, Sym.of("♥", RED));
}
onUse(event: GameEvent) {
event.player.heal(event, this, 50, event.board);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Healer");
}
create() {
return new Healer();
}
tag() {
return "Power-Ups";
}
})();
}
export class PeachElixir extends Item {
constructor() {
super("Peach Elixir", 0, NONE, Sym.of("¡", PEACHPUFF, null, LIGHTSALMON, null));
}
onUse(event: GameEvent) {
const cell = event.board.getCurrentCell();
events.fireMessage(
"Delicious! (You can't be turned to stone, but the effect can wear off when used.)", cell
);
event.player.add(STONING_RESISTANT);
event.player.bag.remove(this);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("PeachElixir");
}
create() {
return new PeachElixir();
}
tag() {
return "Power-Ups";
}
})();
}
export class CopperPill extends Item {
constructor() {
super("Copper Pill", 0, NONE, Sym.of("θ", PERU));
}
onUse(event: GameEvent) {
const cell = event.board.getCurrentCell();
events.fireMessage("Is it healthy to swallow this much copper? (You can't be paralyzed, but the effect can wear off when used.)", cell);
event.player.add(PARALYSIS_RESISTANT);
event.player.bag.remove(this);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("CopperPill");
}
create() {
return new CopperPill();
}
tag() {
return "Power-Ups";
}
})();
}
export class TerminusEst extends Item implements Animated {
constructor() {
super("Terminus Est", MELEE_WEAPON, NONE, Sym.of("†", RED, null, DARKRED, null));
}
onHit(event: GameEvent, cell: Cell, agent: Agent) {
if (agent.changeHealth(50) === 0) {
event.kill(cell, agent);
}
}
randomSeed(): boolean {
return true;
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
const color = !cell.board.outside
? oscillate(RED, ORANGE, 8, frame)
: oscillate(DARKRED, DARKORANGE, 8, frame);
cell.animItemSymbol = Sym.of(this.symbol.entity, color);
cell.board.notifyCellChange(cell);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("TerminusEst");
}
create() {
return new TerminusEst();
}
tag() {
return "Artifacts";
}
})();
}
export class Sword extends Item {
constructor() {
super("Sword", MELEE_WEAPON, NONE, Sym.of("†", WHITE, null, BLACK));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(15) === 0) {
event.kill(agentLoc, agent);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Sword");
}
create() {
return new Sword();
}
tag() {
return "Weapons";
}
})();
}
export class Dagger extends Item {
constructor() {
super("Dagger", MELEE_WEAPON, NONE, Sym.of("+", SILVER));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(10) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Dagger");
}
create() {
return new Dagger();
}
tag() {
return "Weapons";
}
})();
}
export class Hammer extends Item {
constructor() {
super("Hammer", MELEE_WEAPON, NONE, Sym.of("τ", SILVER));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(10) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Hammer");
}
create() {
return new Hammer();
}
tag() {
return "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.
*/
function assessAmmo(event: GameEvent, weapon: Item, ammoItem: Item): Item | null {
const entry = event.player.bag.findEntry(weapon);
if (entry && entry.ammo > 0) {
entry.ammo--;
events.fireInventoryChanged(event.player.bag);
return ammoItem;
}
const cell = event.board.getCurrentCell();
events.fireMessage("Out of ammo", cell);
return null;
}
export class Bow extends Item {
#arrow: Arrow | undefined;
constructor() {
super("Bow", RANGED_WEAPON, NONE, Sym.of(")", SADDLEBROWN));
this.#arrow = Registry.item("Arrow");
}
onFire(): Arrow {
return this.#arrow!;
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to aim at something");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Bow");
}
create() {
return new Bow();
}
tag() {
return "Weapons";
}
})();
}
export class Arrow extends Item {
constructor() {
super("Arrow", AMMUNITION, NONE, Sym.of("'", SADDLEBROWN));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(15) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Arrow");
}
create() {
return new Arrow();
}
tag() {
return "Ammunition";
}
})();
}
export class Rock extends Item {
constructor() {
super("Rock", 0, NONE, Sym.of("•", WHITE, null, BLACK, null));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(-5) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Rock");
}
create() {
return new Rock();
}
tag() {
return "Ammunition";
}
})();
}
export class SlingRock extends Item {
constructor() {
super(
"Rock",
AMMUNITION | NOT_EDITABLE,
NONE,
Sym.of("•", SILVER, null, NEARBLACK, null),
);
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(5) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("SlingRock");
}
create() {
return new SlingRock();
}
tag() {
return "Ammunition";
}
})();
}
export class Sling extends Item {
#rock: Item;
constructor() {
super("Sling", RANGED_WEAPON, NONE, Sym.of("ϑ", SADDLEBROWN));
this.#rock = Registry.item("SlingRock");
}
onFire(_event: GameEvent): Item | null {
return this.#rock;
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to aim at something");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Sling");
}
create() {
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 implements HasAmmo {
#arrow: Item;
constructor() {
super("Bow", RANGED_WEAPON | REQUIRES_AMMO, NONE, Sym.of(")", SADDLEBROWN));
this.#arrow = Registry.item("Arrow");
}
onFire(event: GameEvent): Item | null {
return assessAmmo(event, this, this.#arrow);
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to aim at something (requires arrow ammo)");
}
getAmmoType(): Item {
return this.#arrow;
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("AmmoBow");
}
create() {
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 implements HasAmmo {
#bullet: Item;
constructor() {
super(
"Gun",
RANGED_WEAPON | REQUIRES_AMMO,
NONE,
Sym.of("¬", WHITE, null, BLACK, null),
);
this.#bullet = Registry.item("Bullet");
}
onFire(event: GameEvent): Item | null {
return assessAmmo(event, this, this.#bullet);
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to fire at something (requires bullet ammo)");
}
getAmmoType(): Item {
return this.#bullet;
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("AmmoGun");
}
create() {
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 implements HasAmmo {
#bullet: Item;
constructor() {
super("Paralyzer", RANGED_WEAPON | REQUIRES_AMMO, NONE, Sym.of("¬", DARKVIOLET));
this.#bullet = Registry.item("Parabullet");
}
onFire(event: GameEvent): Item | null {
return assessAmmo(event, this, this.#bullet);
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to fire at something");
}
getAmmoType(): Item {
return this.#bullet;
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("AmmoParalyzer");
}
create() {
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 implements HasAmmo {
#rock: Item;
constructor() {
super("Sling", RANGED_WEAPON | REQUIRES_AMMO, NONE, Sym.of("ϑ", SADDLEBROWN));
this.#rock = Registry.item("SlingRock");
}
onFire(event: GameEvent): Item | null {
return assessAmmo(event, this, this.#rock);
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to aim at something (requires rock ammo)");
}
getAmmoType(): Item {
return Registry.item("Rock");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("AmmoSling");
}
create() {
return new AmmoSling();
}
tag() {
return "Weapons";
}
})();
}
// ── Rings / wearable ──────────────────────────────────────────────────────────
export class BlueRing extends Item {
constructor() {
super("Blue Ring", 0, NONE, Sym.of("o", BLUE));
}
onSelect(event: GameEvent, _cell: Cell) {
event.player.add(WATER_RESISTANT);
}
onDeselect(event: GameEvent, cell: Cell) {
if (cell.terrain.is(AQUATIC)) {
event.cancel(
"You can't swim. You've got to keep the ring on.",
cell,
);
} else {
event.player.remove(WATER_RESISTANT);
}
}
onDrop(event: GameEvent, cell: Cell) {
if (cell.terrain.is(AQUATIC)) {
event.cancel("You'd drown. Really.", cell);
}
}
onThrow(event: GameEvent, cell: Cell) {
if (cell.terrain.is(AQUATIC)) {
event.cancel("That would be suicide", cell);
}
}
onUse(event: GameEvent) {
event.cancel("To wear the ring, just keep it selected");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("BlueRing");
}
create() {
return new BlueRing();
}
tag() {
return "Power-Ups";
}
})();
}
export class RedRing extends Item {
constructor() {
super("Red Ring", 0, NONE, Sym.of("o", RED));
}
onSelect(event: GameEvent, _cell: Cell) {
event.player.add(FIRE_RESISTANT);
}
onDeselect(event: GameEvent, cell: Cell) {
if (cell.terrain.is(LAVITIC)) {
event.cancel("Are you kidding? You'd fry!", cell);
} else {
event.player.remove(FIRE_RESISTANT);
}
}
onDrop(event: GameEvent, cell: Cell) {
if (cell.terrain.is(LAVITIC)) {
event.cancel("That would be suicide", cell);
}
}
onThrow(event: GameEvent, cell: Cell) {
if (cell.terrain.is(LAVITIC)) {
event.cancel("That would be suicide", cell);
}
}
onUse(event: GameEvent) {
event.cancel("To wear the ring, just keep it selected");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("RedRing");
}
create() {
return new RedRing();
}
tag() {
return "Power-Ups";
}
})();
}
// ── Scrolls ───────────────────────────────────────────────────────────────────
export class Scroll extends Item {
readonly scrollName: string;
readonly url: string | null;
constructor(name: string, url?: string) {
super(`Scroll: "${name}"`, 0, NONE, Sym.of("§", WHITE, null, BLACK, null));
this.scrollName = name;
this.url = url ?? null;
}
getIndefiniteNoun(phrase: string) {
return phrase.replace("{0}", `a scroll: "${this.scrollName}"`);
}
onUse(event: GameEvent) {
if (this.url) {
const fullUrl = event.player.scenarioURL + this.url;
events.fireScrollPage(fullUrl, this.scrollName);
} else {
const cell = event.board.getCurrentCell();
events.fireMessage("It says nothing unexpected (or interesting).", cell);
}
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args: Array<string | Piece>) {
return args.length >= 2
? new Scroll(args[0] as string, args[1] as string)
: new Scroll(args[0] as string);
}
store(s: Scroll) {
return s.url
? `Scroll|${s.scrollName}|${s.url}`
: `Scroll|${s.scrollName}`;
}
example() {
return new Scroll("An Example");
}
template(_id: string) {
return "Scroll|{name}|{url?}";
}
tag() {
return "Mundane Items";
}
})();
}
// ── Artifacts ─────────────────────────────────────────────────────────────────
export class GoldenHarp extends Item {
constructor() {
super("Golden Harp", 0, NONE, Sym.of("ש", GOLD, null, DARKGOLDENROD, null));
}
onUse(event: GameEvent) {
event.cancel("Twang!");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("GoldenHarp");
}
create() {
return new GoldenHarp();
}
tag() {
return "Artifacts";
}
})();
}
export class SilverAnkh extends Item {
constructor() {
super("Silver Ankh", 0, NONE, Sym.of("☥", SILVER, null, BUILDING_WALL, null));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("SilverAnkh");
}
create() {
return new SilverAnkh();
}
tag() {
return "Artifacts";
}
})();
}
export class Chalice extends Item implements Animated {
static SYMBOLS = [
Sym.of("Y", GOLD),
Sym.of("Y", RED),
Sym.of("Y", YELLOW, null, BLACK, null),
Sym.of("Y", GREEN),
Sym.of("Y", WHITE),
];
constructor() {
super("The Chalice", 0, NONE, Chalice.SYMBOLS[0]);
}
randomSeed() {
return true;
}
onFrame(_event: GameEvent, cell: Cell, frame: number) {
cell.animItemSymbol = Chalice.SYMBOLS[frame % 5];
cell.board.notifyCellChange(cell);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Chalice");
}
create() {
return new Chalice();
}
tag() {
return "Artifacts";
}
})();
}
export class HelmOfTheAsciiroth extends Item implements Animated {
#stoneray: Item;
constructor() {
super("Helm of the Asciiroth", 0, NONE, Sym.of("Ω", WHITE, null, BLACK, null));
this.#stoneray = Registry.item("Stoneray");
}
randomSeed() {
return true;
}
onUse(event: GameEvent) {
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: GameEvent, cell: Cell, frame: number) {
const outside = cell.board.outside;
const bg = cell.terrain.symbol.getBackground(outside) ?? NONE;
const nbg = oscillate(bg, GREEN, 10, frame);
cell.animItemSymbol = Sym.of("Ω", WHITE, nbg, BLACK, nbg);
cell.board.notifyCellChange(cell);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("HelmOfTheAsciiroth");
}
create() {
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", 0, NONE, Sym.of("ø", WHITE, null, BLACK, null));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("MirrorShield");
}
create() {
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, Sym.of("δ", WHITE, null, BLACK, null));
}
onSteppedOn(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.is(PLAYER)) return;
if (agent instanceof Slider || agent instanceof Pusher) {
agentLoc.removeAgent(agent);
}
agentLoc.removeItem(this);
agentLoc.explosion(event.player);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Bomb");
}
create() {
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, Sym.of("ι", WHITE, null, BLACK, null));
}
onSteppedOn(_event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.is(CARNIVORE) && Math.random() < 0.05) {
agentLoc.removeItem(this);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Bone");
}
create() {
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,
NONE,
Sym.of("•", LIGHTSTEELBLUE, null, DARKSLATEBLUE, null),
);
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(20) === 0) event.kill(agentLoc, agent);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Bullet");
}
create() {
return new Bullet();
}
tag() {
return "Ammunition";
}
})();
}
/**
* Parabullet — AMMUNITION for the Paralyzer. Paralyzes agents on hit.
*/
export class Parabullet extends Item {
constructor() {
super("Paralysis Bullet", AMMUNITION, NONE, Sym.of("*", DARKVIOLET));
}
onHit(event: GameEvent, agentLoc: Cell, agent: 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() {
return new Parabullet();
}
tag() {
return "Ammunition";
}
})();
}
/**
* FishingPole — used at a FishPool to catch fish.
*/
export class FishingPole extends Item {
constructor() {
super("Fishing Pole", 0, NONE, Sym.of("ſ", SADDLEBROWN));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("FishingPole");
}
create() {
return new FishingPole();
}
tag() {
return "Mundane Items";
}
})();
}
/**
* GlassEye — grants DETECT_HIDDEN while selected.
*/
export class GlassEye extends Item {
constructor() {
super(
"The Glass Eye",
0,
NONE,
Sym.of("Θ", MEDIUMSPRINGGREEN, null, MEDIUMAQUAMARINE, null),
);
}
onSelect(event: GameEvent, _cell: Cell) {
event.player.add(DETECT_HIDDEN);
}
onDeselect(event: GameEvent, _cell: Cell) {
event.player.remove(DETECT_HIDDEN);
}
onDrop(event: GameEvent, _cell: Cell) {
event.player.remove(DETECT_HIDDEN);
}
onThrow(event: GameEvent, _cell: Cell) {
event.player.remove(DETECT_HIDDEN);
}
onUse(event: GameEvent) {
event.cancel("Just holding it brings power (look at your flags)");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("GlassEye");
}
create() {
return new GlassEye();
}
tag() {
return "Artifacts";
}
})();
}
/**
* Grenade — thrown item that explodes on landing.
*/
export class Grenade extends Item {
constructor() {
super("Grenade", 0, NONE, Sym.of("σ", OLIVE));
}
onUse(event: GameEvent) {
event.cancel("Just throw a grenade to use it, but stand back");
}
onThrowEnd(event: GameEvent, cell: Cell) {
event.cancel();
cell.explosion(event.player);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Grenade");
}
create() {
return new Grenade();
}
tag() {
return "Weapons";
}
})();
}
/**
* Gun — RANGED_WEAPON that fires bullets.
*/
export class Gun extends Item {
constructor() {
super("Gun", RANGED_WEAPON, NONE, Sym.of("¬", WHITE, null, BLACK, null));
}
onFire(_event: GameEvent): Item | null {
return Registry.item("Bullet");
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to fire at something");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Gun");
}
create() {
return new Gun();
}
tag() {
return "Weapons";
}
})();
}
/**
* KelpSmoothie — grants POISON_RESISTANT when used (consumed on use).
*/
export class KelpSmoothie extends Item {
constructor() {
super("Kelp Smoothie", 0, NONE, Sym.of("¡", CHARTREUSE));
}
onUse(event: GameEvent) {
const cell = event.board.getCurrentCell();
events.fireMessage("Truly foul. (You can't be poisoned, but the effect can wear off when used.)", cell);
event.player.add(POISON_RESISTANT);
event.player.bag.remove(this);
event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("KelpSmoothie");
}
create() {
return new KelpSmoothie();
}
tag() {
return "Power-Ups";
}
})();
}
/**
* Paralyzer — RANGED_WEAPON that fires paralyzer bullets.
*/
export class Paralyzer extends Item {
constructor() {
super("Paralyzer", RANGED_WEAPON, NONE, Sym.of("¬", DARKVIOLET));
}
onFire(_event: GameEvent): Item | null {
return Registry.item("Parabullet");
}
onUse(event: GameEvent) {
event.cancel("Use shift-direction to fire at something (requires paralyzer bullet ammo)");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Paralyzer");
}
create() {
return new Paralyzer();
}
tag() {
return "Weapons";
}
})();
}
/**
* ProteinBar — cures the WEAK condition when used.
*/
export class ProteinBar extends Item {
constructor() {
super("Protein Bar", 0, NONE, Sym.of("=", BURLYWOOD, null, PERU, null));
}
onUse(event: GameEvent) {
event.player.cureWeakness(event, this);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("ProteinBar");
}
create() {
return new ProteinBar();
}
tag() {
return "Power-Ups";
}
})();
}
/**
* PurpleMushroom — cures the POISONED condition when used.
*/
export class PurpleMushroom extends Item {
constructor() {
super("Purple Mushroom", 0, NONE, Sym.of("♠", PURPLE));
}
onUse(event: GameEvent) {
event.player.curePoison(event, this);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("PurpleMushroom");
}
create() {
return new PurpleMushroom();
}
tag() {
return "Power-Ups";
}
})();
}
/**
* EuclideanShard — colored item used to power an EuclideanEngine.
*/
export class EuclideanShard extends Item {
constructor(color: Color) {
super(color.name + " Euclidean Shard", 0, color, Sym.of("◆", color));
}
static SERIALIZER = new (class extends BaseSerializer {
create([color]: string[]) {
return new EuclideanShard(colorByName(color) ?? NONE);
}
store(s: EuclideanShard) {
return `EuclideanShard|${s.color.name}`;
}
example() {
return new EuclideanShard(STEELBLUE);
}
template(_id: string) {
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 {
readonly agent: Agent;
constructor(agent: Agent) {
super(
"Agentray",
AMMUNITION | NOT_EDITABLE,
NONE,
Sym.of("°", agent.symbol.color ?? NONE, null, agent.symbol.outsideColor, null),
);
this.agent = agent;
}
onThrowEnd(event: GameEvent, cell: Cell) {
event.cancel();
if (cell.agent == null) {
cell.setAgent(this.agent);
}
}
static SERIALIZER = new (class extends BaseSerializer {
create([agent]: Array<string | Piece>) {
return new Agentray(agent as Agent);
}
store(a: Agentray) {
return `Agentray|${Registry.serialize(a.agent).replace(/\|/g, "^")}`;
}
example() {
return new Agentray(Registry.agent("Sleestak"));
}
template(_id: string) {
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, NONE, Sym.of("'", SADDLEBROWN));
}
onHit(event: GameEvent, agentLoc: Cell, agent: Agent) {
if (agent.changeHealth(10) === 0) {
event.kill(agentLoc, agent);
}
if (
agent.is(PLAYER) &&
!event.player.testResistance(POISON_RESISTANT)
) {
event.player.add(POISONED);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("PoisonDart");
}
create() {
return new PoisonDart();
}
tag() {
return "Ammunition";
}
})();
}
const _STONERAY_SYMBOLS = [
Sym.of("×", LIGHTBLUE, null, BLUE, null),
Sym.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, NONE, _STONERAY_SYMBOLS[0]);
}
getSymbol() {
return _STONERAY_SYMBOLS[Math.random() < 0.5 ? 0 : 1];
}
onHit(event: GameEvent, agentLoc: Cell, agent: 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() {
return new Stoneray();
}
tag() {
return "Ammunition";
}
})();
}
/** Fireball — fired by Thermadon. Deals heavy fire damage. */
const _FIREBALL_SYMBOLS = [
Sym.of("*", RED),
Sym.of("*", ORANGE),
Sym.of("*", RED),
Sym.of("*", YELLOW),
];
export class Fireball extends Item {
constructor() {
super("Fireball", AMMUNITION | NOT_EDITABLE, NONE, _FIREBALL_SYMBOLS[0]);
}
getSymbol() {
return _FIREBALL_SYMBOLS[Math.floor(Math.random() * 4)];
}
onThrowEnd(event: GameEvent, cell: Cell) {
event.cancel();
cell.explosion(event.player);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Fireball");
}
create() {
return new Fireball();
}
tag() {
return "Ammunition";
}
})();
}
/** Weakray — fired by Asciiroth. Spreads a weakening energy cloud on landing. */
export class Weakray extends Item {
constructor() {
super("Weakray", AMMUNITION | NOT_EDITABLE, NONE, Sym.of("∴", WHITE, null, BLACK, null));
}
onThrowEnd(event: GameEvent, cell: Cell) {
event.cancel();
cell.createCloud("EnergyCloud");
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Weakray");
}
create() {
return new Weakray();
}
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("Salmon", Salmon.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("Weakray", Weakray.SERIALIZER);
Registry.register("EuclideanShard", EuclideanShard.SERIALIZER);
}