This guide explains how to create game content for MudForge.
All game content lives in the /mudlib/ directory. The standard library provides base classes you extend to create your content.
Efuns (external functions) are driver-provided APIs available to all mudlib code. They are globally available through the efuns object - you don't need to declare them in each file.
// Efuns are globally available - just use them!
export class MyRoom extends Room {
async onCreate(): Promise<void> {
await super.onCreate();
const sword = await efuns.cloneObject('/std/sword');
await sword?.moveTo(this);
efuns.send(efuns.thisPlayer()!, 'A sword materializes!');
}
}See the Efuns Reference for the complete API.
Rooms are locations in the game world.
// /mudlib/areas/town/tavern.ts
import { Room } from '../../std/room.js';
export class Tavern extends Room {
shortDesc = 'The Rusty Tankard';
get longDesc(): string {
return `You stand in a cozy tavern. A fire crackles in the hearth,
and the smell of roasting meat fills the air.
Obvious exits: south`;
}
onCreate(): void {
super.onCreate();
// Add exits
this.addExit('south', '/areas/town/market');
this.addExit('up', '/areas/town/tavern_rooms');
// Add custom commands
this.addAction('order', this.handleOrder.bind(this));
}
private handleOrder(args: string): boolean {
const player = efuns.thisPlayer();
if (!player) return false;
efuns.send(player, 'The bartender slides a foamy ale across the bar.');
return true;
}
}Items are objects players can pick up and use.
// /mudlib/areas/town/items/sword.ts
import { Weapon } from '../../../std/weapon.js';
export class TownSword extends Weapon {
shortDesc = 'a rusty sword';
get longDesc(): string {
return 'This old sword has seen better days, but it still has an edge.';
}
onCreate(): void {
super.onCreate();
this.weight = 3;
this.value = 10;
this.damage = 5;
this.damageType = 'slashing';
}
// Called when player wields the weapon
onWield(): void {
const player = efuns.thisPlayer();
if (player) {
efuns.send(player, 'You grip the rusty sword tightly.');
}
}
}NPCs are non-player characters that populate your world. They can have periodic chat messages, respond to player speech, and perform autonomous behavior.
// /mudlib/areas/town/npcs/bartender.ts
import { NPC } from '../../../std/npc.js';
export class Bartender extends NPC {
constructor() {
super();
// Use setNPC() to configure all NPC properties at once
this.setNPC({
name: 'bartender',
shortDesc: 'the bartender',
longDesc: 'A grizzled man stands behind the bar, polishing a glass.',
chatChance: 15, // 15% chance per heartbeat to chat
});
// Add periodic chat messages with action types
this.addChat('The bartender whistles tunelessly.', 'emote');
this.addChat('What can I get for ya?', 'say');
this.addChat('polishes a glass methodically.', 'emote');
// Add response triggers (regex patterns)
this.addResponse(/hello|hi|hey/i, 'Welcome to my tavern!', 'say');
this.addResponse(/ale|beer|drink/i, 'Best ale in town, only 5 copper!', 'say');
this.addResponse(/food|eat|hungry/i, 'We have stew and fresh bread.', 'say');
}
}NPCs can periodically say or emote things using the chat system:
// Action types: 'say', 'emote', 'shout'
this.addChat('rings his brass bell loudly.', 'emote'); // "Crier rings his brass bell loudly."
this.addChat('Hear ye, hear ye!', 'say'); // "Crier says: Hear ye, hear ye!"
this.addChat('IMPORTANT NEWS!', 'shout'); // "Crier shouts: IMPORTANT NEWS!"NPCs can respond to player speech with pattern matching:
// Pattern matching with regex
this.addResponse(/quest|job|work/i, 'I might have something for you...', 'say');
this.addResponse(/bye|goodbye|farewell/i, 'waves goodbye.', 'emote');
// The NPC will automatically respond when a player says something matching the patternNPCs are displayed in room descriptions with red text (non-bold) and appear after players but before items:
Town Square
The central gathering place of town.
Acer is standing here. <- Player (bold white)
The town crier is standing here. <- NPC (red)
A rusty sword lies on the ground. <- Item (normal)
Merchants are NPCs that can buy and sell items through a GUI shop interface. See the Merchant System Guide for complete documentation.
// /mudlib/areas/town/blacksmith.ts
import { Merchant } from '../../std/merchant.js';
export class Blacksmith extends Merchant {
constructor() {
super();
// Configure the merchant
this.setMerchant({
name: 'Grond the Smith',
shopName: "Grond's Forge",
shopDescription: 'Quality weapons and armor.',
buyRate: 0.6, // Pays 60% of item value
sellRate: 1.0, // Sells at 100% of stock price
acceptedTypes: ['weapon', 'armor'],
shopGold: 5000,
});
// Set NPC properties
this.shortDesc = 'Grond the Smith';
this.longDesc = 'A massive, barrel-chested blacksmith.';
this.addId('grond');
this.addId('blacksmith');
this.addId('merchant');
// Stock the shop
this.addStock('/items/iron_sword', 'Iron Sword', 100, 5, 'weapon');
this.addStock('/items/leather_armor', 'Leather Armor', 75, 5, 'armor');
}
}- GUI Shop Modal: Three-panel interface (wares, ledger, inventory)
- Merged Transactions: Buy and sell items in a single transaction
- Charisma Modifier: Player charisma affects prices
- Sold Items: Items sold by players are resold with "(used)" tag
- Category Filtering: Merchants can specialize in item types
// addStock(itemPath, name, price, quantity, category)
this.addStock('/items/health_potion', 'Health Potion', 50, -1, 'potion'); // -1 = unlimited
this.addStock('/items/iron_sword', 'Iron Sword', 100, 5, 'weapon'); // Limited stockContainers can hold other items. Players can open, close, lock, unlock, and store items in them.
// /mudlib/areas/town/items/chest.ts
import { Container } from '../../../std/container.js';
export class TreasureChest extends Container {
constructor() {
super();
this.setContainer({
name: 'chest',
shortDesc: 'a wooden treasure chest',
longDesc: 'An ornate wooden chest bound with iron bands.',
maxCapacity: 100, // Maximum weight it can hold
isOpen: false, // Starts closed
isLocked: true, // Starts locked
keyId: 'brass_key', // Requires a key with this ID to unlock
});
}
async onCreate(): Promise<void> {
await super.onCreate();
// Add treasure inside
const gold = await efuns.cloneObject('/std/gold');
if (gold) {
gold.setProperty('amount', 50);
await gold.moveTo(this);
}
}
}Players can interact with containers using these commands:
open chest # Open a container
close chest # Close a container
look in chest # See what's inside (if open)
get sword from chest # Take an item from the container
drop sword in chest # Put an item in the container (or use 'put')
Containers can be locked and require keys:
export class LockedChest extends Container {
constructor() {
super();
this.setContainer({
name: 'chest',
shortDesc: 'a locked iron chest',
longDesc: 'A heavy iron chest with a brass lock.',
isLocked: true,
keyId: 'iron_chest_key', // The key item needs this as its keyId
});
}
}To create the matching key:
export class IronChestKey extends Item {
constructor() {
super();
this.shortDesc = 'an iron key';
this.longDesc = 'A heavy iron key with intricate teeth.';
this.keyId = 'iron_chest_key'; // Matches the container's keyId
}
}Weapons can be wielded by players for combat. They support handedness for dual-wielding.
// /mudlib/areas/town/items/longsword.ts
import { Weapon } from '../../../std/weapon.js';
export class Longsword extends Weapon {
constructor() {
super();
this.setWeapon({
name: 'longsword',
shortDesc: 'a gleaming longsword',
longDesc: 'A well-balanced steel longsword with a leather-wrapped hilt.',
damage: 10,
damageType: 'slashing',
handedness: 'one_handed', // 'one_handed', 'light', or 'two_handed'
});
}
// Called when player wields the weapon
onWield(): void {
const player = efuns.thisPlayer();
if (player) {
efuns.send(player, 'You grip the longsword confidently.');
}
}
}Weapons have three handedness types:
| Type | Description | Dual-Wield |
|---|---|---|
one_handed |
Standard weapon in main hand | Can hold shield in off-hand |
light |
Light weapon (daggers, shortswords) | Can go in main or off-hand |
two_handed |
Large weapons (greatswords, bows) | Uses both hands, no shield |
// Light weapon - can dual-wield
export class Dagger extends Weapon {
constructor() {
super();
this.setWeapon({
name: 'dagger',
shortDesc: 'a steel dagger',
damage: 4,
damageType: 'piercing',
handedness: 'light', // Can be wielded in off-hand
});
}
}
// Two-handed weapon - uses both hands
export class Greatsword extends Weapon {
constructor() {
super();
this.setWeapon({
name: 'greatsword',
shortDesc: 'a massive greatsword',
damage: 18,
damageType: 'slashing',
handedness: 'two_handed', // Cannot use shield
});
}
}wield sword # Wield in main hand
wield dagger in left # Wield in off-hand (dual-wield)
unwield # Unwield all weapons
unwield sword # Unwield specific weapon
Armor can be worn on different body slots:
// /mudlib/areas/town/items/chainmail.ts
import { Armor } from '../../../std/armor.js';
export class Chainmail extends Armor {
constructor() {
super();
this.setArmor({
name: 'chainmail',
shortDesc: 'a suit of chainmail',
longDesc: 'Interlocking steel rings form a protective shirt.',
armorClass: 5,
armorSlot: 'chest', // head, chest, hands, legs, feet, cloak
});
}
onWear(): void {
const player = efuns.thisPlayer();
if (player) {
efuns.send(player, 'The chainmail settles comfortably on your shoulders.');
}
}
}| Slot | Example Items |
|---|---|
head |
Helmets, hats, crowns |
chest |
Armor, robes, shirts |
hands |
Gloves, gauntlets |
legs |
Pants, greaves, leggings |
feet |
Boots, shoes |
cloak |
Cloaks, capes |
Shields use the off-hand equipment slot:
import { Armor } from '../../../std/armor.js';
export class WoodenShield extends Armor {
constructor() {
super();
this.setArmor({
name: 'shield',
shortDesc: 'a wooden shield',
longDesc: 'A round wooden shield with an iron boss.',
armorClass: 2,
armorSlot: 'shield', // Uses off-hand, blocks dual-wielding
});
}
}wear armor # Wear an armor piece
remove armor # Remove worn armor
equipment # View all equipped items (or 'eq')
Every object goes through these lifecycle events:
class MyObject extends MudObject {
// Called when the object is first created
onCreate(): void {
super.onCreate();
// Initialize properties
}
// Called when the object is about to be destroyed
onDestroy(): void {
// Clean up resources
super.onDestroy();
}
// Called when a clone is made (on the clone)
onClone(): void {
super.onClone();
// Customize the clone
}
// Called periodically if heartbeat is enabled
heartbeat(): void {
// Regular updates
}
}Add custom commands to objects:
class MagicWand extends Weapon {
onCreate(): void {
super.onCreate();
this.addAction('wave', this.handleWave.bind(this));
this.addAction('zap', this.handleZap.bind(this));
}
private handleWave(args: string): boolean {
const player = efuns.thisPlayer();
if (!player) return false;
this.broadcast(`${player.name} waves the wand mysteriously.`);
return true;
}
private handleZap(args: string): boolean {
const player = efuns.thisPlayer();
if (!player) return false;
if (args) {
efuns.send(player, `You zap ${args} with the wand!`);
} else {
efuns.send(player, 'Zap what?');
}
return true;
}
}Exits connect rooms:
class CrossRoads extends Room {
onCreate(): void {
super.onCreate();
// Simple exit to another room
this.addExit('north', '/areas/forest/entrance');
// Exit to a room that needs to be cloned
this.addExit('east', '/areas/dungeon/entrance', { clone: true });
}
}Send messages to everyone in a room:
class ExplosiveBarrel extends Item {
explode(): void {
const room = efuns.environment(this);
if (!room) return;
// Broadcast to everyone
room.broadcast('BOOM! The barrel explodes!');
// Broadcast excluding the thrower
const thrower = efuns.thisPlayer();
room.broadcast('You throw the barrel!', { exclude: [thrower] });
}
}Enable heartbeat for regular updates:
class PoisonedPlayer extends Living {
private poisonTicks = 10;
startPoison(): void {
efuns.setHeartbeat(this, true);
}
heartbeat(): void {
super.heartbeat();
if (this.poisonTicks > 0) {
this.hp -= 5;
this.receive('The poison courses through your veins...');
this.poisonTicks--;
} else {
efuns.setHeartbeat(this, false);
}
}
}Schedule actions to happen later:
class TimeBomb extends Item {
arm(): void {
const player = efuns.thisPlayer();
efuns.send(player, 'The bomb starts ticking...');
// Explode in 10 seconds
efuns.callOut(() => this.explode(), 10000);
}
private explode(): void {
const room = efuns.environment(this);
if (room) {
room.broadcast('KABOOM! The bomb explodes!');
}
efuns.destruct(this);
}
}- Always call super() - Ensure parent class methods are called
- Use descriptive names -
shortDescshould be brief,longDescdetailed - Handle null checks -
thisPlayer()andenvironment()can return null - Clean up heartbeats - Disable when no longer needed
- Validate input - Check command arguments before using
- Test your code - Use the in-game editor's error feedback
Organize your content logically:
/mudlib/areas/
├── town/
│ ├── index.ts # Town entrance
│ ├── market.ts # Market square
│ ├── tavern.ts # The tavern
│ ├── items/
│ │ ├── bread.ts
│ │ └── sword.ts
│ └── npcs/
│ ├── merchant.ts
│ └── guard.ts
├── forest/
│ ├── entrance.ts
│ └── ...
└── dungeon/
├── entrance.ts
└── ...