Give any mob, room, or item a life of its own - greeters, guards, quest-givers, bosses, traps, whole scripted dungeons - in plain text, with no code and no reboot. This is the complete guide: 24 chapters and 772+ worked examples, every one tested live in the game.
MUDProg is Rogue's text scripting system for builders, modelled on CoffeeMUD's Scriptable/MOBPROG engine. Any mob, room, or item can carry a script that runs when things happen to it or around it - a shopkeeper that greets customers, a boss that taunts at half health, an altar that rewards a spoken password, a trapped room that damages intruders. Scripts are plain text stored in the object's mudprog property; you attach and edit them with the mudprog command. No LPC and no recompiling required.
This page is the quick reference. The full guide is a set of chapters, each its own help page - if you have never scripted before, start with help mudprog-basics and follow the reading order in help mudprog-glossary.
The Guide Chapters
mudprog-basics - Scripting for absolute beginners.
mudprog-triggers - Every trigger, explained with examples.
mudprog-commands - Every mp command reference.
mudprog-functions - Every function reference.
mudprog-variables - Dollar codes, variables, substitution.
mudprog-flow - If, switch, loops, and decisions.
mudprog-bus - Cancelling and observing game actions.
mudprog-cookbook - Complete working scripts to copy.
mudprog-reference - The one-page cheat sheet.
mudprog-workbook1 - Guided exercises: beginner.
mudprog-workbook2 - Guided exercises: intermediate.
mudprog-workbook3 - Guided exercises: advanced.
mudprog-troubleshooting - When scripts do not work.
mudprog-shopkeepers - Deep-dive: merchants and shops.
mudprog-guards - Deep-dive: guards and gatekeepers.
mudprog-questcraft - Deep-dive: quest scripting.
mudprog-dungeons - Deep-dive: rooms, traps, dungeons.
mudprog-companions - Deep-dive: pets, followers, mounts.
mudprog-events - Deep-dive: world events and schedules.
mudprog-combatai - Deep-dive: combat and boss scripting.
mudprog-dialogue - Deep-dive: dialogue, cutscenes, memory.
mudprog-patterns - The idiom library.
mudprog-faq - One hundred questions, answered.
mudprog-glossary - Glossary and master index.
A First Script
A script is one or more PROG blocks. Each block starts with a trigger name and its argument, then a body of commands, and ends with a line containing just a tilde:
GREET_PROG 100
say Welcome to my shop, $N! Have a look around.
emote dusts off the counter.
~
SPEECH_PROG password
if hastitle($n) == 'the Initiate'
say Ah, one of us. The vault is yours.
mpopen north
else
say That word means nothing to one like you.
endif
~
The first block greets every player who enters (100% of the time). The second fires when someone says a line containing "password", and branches on whether the speaker holds a title.
The mudprog Command
mudprog <target> - View a target's script and its triggers. mudprog <target> edit - Open the line editor (replaces the script). mudprog <target> set <inline> - Set the whole script from one line. mudprog <target> append <inline> - Append one line to the script. mudprog <target> clear - Remove the script. mudprog <target> test <TRIGGER> - Fire a trigger now, with you as the source.
The target is a mob or item in the room by name, or the word here for the current room, or self for you. In set and append, a semicolon becomes a new line, so you can write a whole block on one command line:
mudprog guard set GREET_PROG 100 ; say Halt! State your business. ; ~
In the editor, type lines normally, end each PROG block with a tilde, and finish with a single dot on its own line. Type @abort to cancel.
To test scripts quickly on yourself without attaching them, use scripttest (see help scripttest).
Triggers
Every block begins with a trigger and an argument. The argument is usually a percent chance to fire (an integer 1-100), the word all (or blank) for always, keywords to match for speech-like triggers, or a zapper mask (see below). These triggers fire automatically on live game events. Put the script on the object noted in brackets - the mob, the item, the room, or a container/door.
GREET_PROG <pct|mask> - A player enters the room [mob, room].
ALL_GREET_PROG <pct|mask> - As greet, including sneakers [mob, room].
GROUP_GREET_PROG <pct> - As greet, once for a whole entering group [mob].
SPEECH_PROG <keywords> - Someone speaks a matching line nearby [mob].
SPEAK_PROG <keywords> - The scripted mob itself speaks a line [mob].
ACT_PROG / MASK_PROG - As speech; match on spoken/acted text [mob].
SOCIAL_PROG <keywords> - Someone performs a social/emote nearby [mob].
RAND_PROG <pct> - Rolls each of the mob's heartbeats [mob].
ONCE_PROG - Once, when the mob first loads [mob].
FIGHT_PROG <pct> - Each combat round while the mob fights [mob].
HITPRCNT_PROG <pct> - A round where the mob is at or below <pct> hp [mob].
DAMAGE_PROG - The mob takes damage [mob].
DEATH_PROG - The mob is about to die [mob].
KILL_PROG - The mob has just killed its target [mob].
GIVE_PROG <keywords> - The mob is given an item (item is $o) [mob].
GIVING_PROG - Someone gives an item away (item is $o) [item].
BRIBE_PROG <amount> - The mob is given coins (amount in $g) [mob].
LOOK_PROG / LLOOK_PROG - A player looks at the mob or item [mob, item].
GET_PROG / GETTING_PROG - An item is picked up [item, room, mob / getter].
DROP_PROG / DROPPING_PROG - An item is dropped [item, room, mob / dropper].
PUT_PROG / PUTTING_PROG - An item is put in a container [item, container].
WEAR_PROG / WEARING_PROG - An item is worn or wielded [item / wearer].
REMOVE_PROG - An item is removed or unwielded [item].
CONSUME_PROG - Food is eaten or a drink drunk [item].
OPEN_PROG / CLOSE_PROG - A container or door is opened/closed [container].
LOCK_PROG / UNLOCK_PROG - A container or door is locked/unlocked [container].
BUY_PROG / SELL_PROG - A player buys from / sells to the vendor [vendor].
CAST_PROG / CASTING_PROG - A spell resolves on the mob / the mob casts (spell in $g).
FOLLOW_PROG / UNFOLLOW_PROG- Someone starts/stops following the mob [mob].
RIDE_PROG / RIDING_PROG - The mount is mounted / a rider mounts [mount / rider].
LOGIN_PROG / LOGOFF_PROG - A player enters or leaves the game [any scripted object].
LEVEL_PROG - A player gains a level (new level in $g) [any].
CHANNEL_PROG <chan words> - Traffic on a comm channel (channel+text in $g) [any].
TIME_PROG <hours> - The mud clock reaches one of the listed hours [mob].
DAY_PROG <days> - A new mud day begins (day # in $g) [any].
AGE_PROG - A player crosses a new hour of played age [any].
ENTRY_PROG / EXIT_PROG - The scripted mob enters/leaves a room [mob].
CNCLMSG_PROG <code> [mask] - CANCEL a game action before it happens; the
block runs INSTEAD of the action. Codes: GET,
DROP, PUT, GIVE, WEAR, REMOVE, OPEN, CLOSE,
LOCK, UNLOCK, EAT, DRINK, BUY, SELL, CAST,
ENTER, LEAVE, ATTACK [any object in the room].
EXECMSG_PROG [code] [mask] - Observe any of those actions after they
happen, without interfering [any object].
IMASK_PROG [text] - The scripted object's OWN output contains the
text (blank matches everything) [mob, item].
REGMASK_PROG <regexp> - Any text the object sees matches a regular
expression [mob, item].
CMDFAIL_PROG <pct> - A player's command fails nearby (the failed
command is $g) [mob, room].
QUEST_TIME_PROG <q> <mins> - A timed quest reaches the listed minutes
remaining [any object].
A FUNCTION_PROG block is a named routine you invoke yourself with mpcallfunc (or callfunc()) and never fires on its own. See help mudprog-bus for the full cancel/observe model.
Zapper Masks
Any trigger that takes a chance can instead take a CoffeeMUD-style zapper mask to restrict WHO may set it off. A mask is a set of clauses, each a -TYPE followed by the values that qualify:
Supported clause types: -class, -race, -level (a number means at-or-above; 30-40 a range), -sex, -name, -deity, -player, and -npc. All clauses must pass for the trigger to fire.
Variables and Substitution
Inside any line, dollar codes are replaced with live values. The source of the trigger (usually the acting player) is $n, the mob or object running the script is $i, and the target is $t:
$n $N source name $t $T target name $i this mob's name
$I this mob's short desc $q this object's name $Q its short desc
$o $O first item name $p $P second item name
$e $s $m source he / him / his $E $S $M target forms
$j $h $k this mob he / him / his $y $Y source / target sir-or-madam
$r $R a random player $c $C a random inhabitant here
$H $J $K random player him / he / his $f $F the mob's leader name / he
$w $W owner of item 1 / 2 $l list of mobs here $L list of items here
$a area name $d room title $D room description $x $X a random exit
$b $B last mpmload/mpoload name / desc
$g $G the message, lower / original case
$$ a literal dollar sign
The $ codes match CoffeeMUD's exactly, so a script written from CoffeeMUD's own guide substitutes identically on this mud.
Two extended forms read data at runtime:
$<obj var> - the value of a stored variable (see mpsetvar).
$%FUNC(args)% - the result of a function, inserted into the text.
For example, say You have $%goldamt($n)% gold and I know you, $<$i note>. The temporary slots $0 through $9 are set by FOR loops and by mpargset.
Control Flow
if <condition> Branch. Optional else. Close with endif.
...
else
...
endif
switch <value> Match a value against cases.
case A
...
case B
...
default
...
endswitch
for $1 = 1 to 5 Loop, counting into a $-slot.
...
next
while <condition> Loop while a condition holds.
...
endwhile
break Leave the nearest loop or case.
return Stop the script (a value after it is the
function-prog result).
Blocks nest freely. Loops are capped at a safe iteration count and each trigger run has a step budget, so a runaway script stops itself rather than hanging the mob.
Conditions
A condition is a function, optionally compared to a value, and conditions can be joined with and, or, and not:
if rand(30) 30% chance.
if level($n) >= 40 Numeric comparison.
if class($n) == mage String comparison (case-insensitive).
if hastitle($n) and goldamt($n) > 500
if isnpc($t) or ispc($t)
Comparison operators: ==!=><>=<= and .in. (substring). A bare function with no comparison is true when it returns non-zero and non-empty.
Commands
Any line that is not control flow is a command. If it is not one of the mp commands below, it is run as an ordinary game command by the mob, so say, emote, yell, whisper, socials, wield, wear, and cast all work exactly as a player would type them.
Messaging:
mpecho <text> - To everyone in the room.
mpechoat <who> <text> - To one target only.
mpechoaround <who> <text> - To the room except one target.
mpasound <text> - To the adjacent rooms.
mpchannel <channel> <text> - Onto a comm channel.
mpspeak <text> - The mob says it aloud.
mpllm <text> - To online staff only.
mplog <text> - To the server log.
mpprompt / mpconfirm / mpchoose <text> - Ask the player; the reply lands in
their prompt_answer / confirm_answer var.
Movement and combat:
mpgoto <room> - Move this mob to a room path.
mpat <room> <command> - Run a command as if in another room.
mptransfer <who> [room] - Move a target to a room (or here).
mpwalkto <dir...> - Step the mob in directions.
mpkill <who> - Start a fight with a target.
mphit <who> - Land one attack.
mpdamage <who> <amount> [type] - Deal direct damage (blunt, heat, magic...).
mpheal <who> <amount> - Restore health.
mpcast <spell> [target] - Cast a spell.
mpslay <who> - Kill a target outright.
mprejuv / mpreset [who] - Restore a mob to full.
mpstop [who] - End combat.
mpflee - Make the mob flee.
mpforce <who> <command> - Force a target to run a command.
mpbehave / mpunbehave <flag> - Toggle aggression and other flags.
mppossess <player> <mob> - Have a player control a mob.
mpbeacon / mpalarm <secs> <command> - Run a command after a delay.
mpsleep <seconds> - Pause HERE; the rest of the script resumes
after the delay (also: mpwait).
mpcondition <who> <id> <type> <secs> [mag] [pct] - Apply a full Rogue
condition (buff or debuff) with magnitude.
Loading and the world:
mpmload <path> - Clone an NPC into the room ($b afterwards).
mpoload <path> - Clone an item onto the mob.
mpoloadroom <path> - Clone an item into the room.
mpjunk <item> / mppurge <who> - Destroy an item or mob.
mpput <item> <container> - Put an item in a container.
mphide / mpunhide [who] - Hide or reveal.
mplink <dir> <room> / mpunlink <dir> - Add or remove a room exit.
mpopen / mpclose / mplock / mpunlock <dir> - Doors and containers.
Character and progression (target is usually $n):
mpset <who> <property> <value> - Set a property, stat, level, or short.
mpexp <who> <amount> - Grant (or remove) experience.
mpmoney <who> [type] <amount> - Give or take currency.
mptitle <who> <title> - Award a name title.
mpfaction <who> <faction> <n> - Change Brinewarren reputation.
mptrains / mppracs <who> <skill> <n> - Credit skill progress.
mpaffect <who> <id> [seconds] - Apply a condition (rooted, poisoned...).
mpunaffect <who> <id> - Remove a condition.
mptattoo <who> <text> - Give a visible tattoo.
mpachieve <who> <id> - Flag an achievement.
mpplayerclass <who> <class> - Change class.
Quests:
mpstartquest <who> <quest> - Begin a quest (this mob is the giver).
mpendquest <who> <quest> - Turn in, or drop if unfinished.
mpquestwin <who> <quest> - Complete a quest.
mpquestpoints <who> <amount> - Award quest points.
mploadquestobj <path> - Give the source a quest item.
Variables and structure:
mpsetvar <obj> <name> <value> - Store a variable on any object.
mpsavevar <obj> <name> <value> - As setvar; player vars persist.
mpgset <name> <value> - Set a mud-wide global.
mpargset <0-9> <value> - Set a temporary $-slot.
mpcallfunc <name> [args] - Run a FUNCTION_PROG block by name.
mpscript <line> - Run one script line immediately.
Functions
Functions are used in conditions and in $%...% substitution. They usually take an object as the first argument (a $-code or a name); many default to this mob if omitted. A selection - see the examples for typical use:
Actor: isnpc ispc isalive isfight isimmort ischarmed isfollow isservant
isgroup ispkill sex position level class baseclass race name deity
mood hitprcnt exp questpoints goldamt currency stat isable
Items: has hasnum worn wornon itemcount objtype value isopen islocked
incontainer mobitem
Room: nummobsroom numitemsroom numpcsroom roommob roomitem roompc
numraces inroom ishere inlocale inarea numpcsarea areapc
Time: istime ishour isday ismonth isyear isseason isweather ismoon
datetime isrlhour isrlday isrlmonth isrlyear
Quest: questwinner questscripted questobj qvar faction hastitle hastag
hastattoo clan clanrank isname isbehave
Shop: shophas shopitem numitemsshop
Utility: rand randnum rand0num number isodd math eval strin strcontains
islike callfunc var affected
Rogue: hp maxhp sp maxsp ep maxep skill factionrep weather season
timeofday isnight groupsize (extensions beyond CoffeeMUD)
Worked Examples
A boss that heals allies below half health and taunts at 25%:
FIGHT_PROG 40
if hitprcnt($i) <= 50
mpcast heal $i
mpecho $I channels a dark restoration.
endif
~
HITPRCNT_PROG 25
yell You will NOT take this keep while I draw breath!
mpaffect $t bleeding 20
~
A quest giver keyed on speech:
SPEECH_PROG quest
if questwinner($n cellar_rats)
say You have already cleared my cellar. My thanks stand.
else
say Rats have overrun my cellar. Clear them and I will pay you well.
mpstartquest $n cellar_rats
endif
~
A trapped room that fires once and rewards the brave:
GREET_PROG 100
if var($i sprung) == 1
mpechoat $n The pit has already been sprung; you step around it.
else
mpsetvar $i sprung 1
mpechoat $n The floor gives way beneath you!
mpdamage $n 40 blunt
mpoloadroom /obj/torch.c
endif
~
Notes
Scripts never crash their host; a bad argument simply does nothing. After editing an object's script the engine reparses automatically. For deeper testing use scripttest to run raw lines on yourself, and mudprog <target> test <TRIGGER> to fire a specific block.
This chapter is the ground floor of the MUDProg guide. It assumes you have never written a script, a program, or a line of code in your life. By the end of it you will have built a talking, reacting, stew-serving tavern keeper entirely out of plain text, without touching a single file of game code. Every idea is introduced from zero, and every example is complete: you can attach any of them to a practice mob exactly as printed and watch it work.
What A Script Is
Walk through any town on the mud and the people you meet are just standing there. A shopkeeper who never greets a customer, a guard who never challenges a stranger, a drunk who never mutters into his cup. They have bodies and descriptions, but no behavior.
A MUDProg script is a set of written instructions that gives an object behavior. You write the instructions in ordinary text, in plain game commands you already know how to type, such as say and emote. The mud stores that text on the mob, the room, or the item, and from then on the engine reads your instructions and performs them at the right moments. The shopkeeper greets. The guard challenges. The drunk mutters.
Three things make MUDProg friendly to a first-time scripter:
- It is only text. There is no game code to compile, no files to edit, and no reboot. You type the script in, and it is live immediately. - It reads like stage directions. A script is mostly lines such as say Welcome! and emote bows deeply, the same commands a player types. - It cannot break anything. A script with a mistake in it simply does less than you hoped. It never crashes the mob, the room, or the mud. There is a whole section on this below, because knowing you are safe is what makes experimenting fun.
The script itself lives in a property on the object called mudprog. You never need to touch that property directly; the mudprog builder command reads and writes it for you.
Events, Triggers, And Commands
Everything that happens in the game is an event. A player walks into a room: that is an event. Someone speaks: an event. A fight starts, an item is handed over, a mob dies: all events.
A script cannot run all the time. It needs to be told WHEN to act, and that is what a trigger is: a named hook that says "when this kind of event happens to me, run these instructions". The trigger for "a player entered my room" is called GREET_PROG. The trigger for "someone spoke near me" is called SPEECH_PROG. The trigger for "I am in a fight" is called FIGHT_PROG. There are about sixty of them, and the mudprog-triggers chapter walks through every one; this chapter uses only a handful.
The instructions under a trigger are commands, and they answer the other question: WHAT to do. Most commands are exactly what a player would type, performed by the scripted mob: say, emote, yell, socials, even wield and cast. On top of those there is a family of special script commands that all start with the letters mp, such as mpecho (print narrator text to the room) and mpoloadroom (create an item in the room). The mudprog-commands chapter lists all of them.
So every script is a set of pairs: a WHEN (the trigger) and a WHAT (the commands). That pair is written as a PROG block.
The Anatomy Of A PROG Block
Here is the shape of every block you will ever write:
TRIGGER_NAME argument
command
command
command
~
Four rules, and they are the only structural rules in all of MUDProg:
1. The first line is the header. It names the trigger, usually followed
by one argument. For most triggers the argument is a number from 1 to
100: the percent chance the block fires when its event happens. 100
means always. For speech-like triggers the argument is instead a list
of keywords to listen for, or the word all to match any speech. The
header GREET_PROG 100 therefore reads as "every single time a player
walks in".
2. The lines after the header are the body: the commands to perform, one
per line, run from top to bottom.
3. The block ends with a line containing just a tilde, the ~ character.
The tilde is the full stop of MUDProg. It tells the engine "this block
is finished; anything after this is a new block". Forgetting it is the
number one beginner mistake, and the mistakes section below shows you
exactly what that looks like.
4. A script may hold as many blocks as you like, one after another, each
ending in its own tilde. They can be for different triggers or even
for the same trigger; when an event fires, every matching block runs
in the order written.
A few comforts worth knowing before you write your first block:
- Blank lines are ignored, so you may space blocks apart for readability. - A line starting with the # character is a comment: a note to yourself that the engine skips completely. - Upper and lower case do not matter to the engine. GREET_PROG, greet_prog, and Greet_Prog are the same trigger. The convention is to write trigger names in capitals and commands in lower case, which is how every example in this guide is printed. - The examples on this page are indented three spaces so they stand out from the prose. Indentation is entirely optional; the engine trims it. Type the lines with or without leading spaces, exactly as you prefer.
Your Very First Script
Here it is. Three lines:
GREET_PROG 100
say Hello there, welcome to my little shop!
~
Read it line by line:
- GREET_PROG 100 is the header. GREET_PROG means "a player just walked into my room". The 100 means "act every time, no dice roll". - say Hello there, welcome to my little shop! is the body. When the trigger fires, the mob performs this exactly as if it had typed it: the whole room sees the mob speak the greeting aloud. - ~ ends the block.
That is a complete, working script. Attach it to any mob (the attaching section below shows how), walk out of the room, walk back in, and the mob greets you. There is nothing more to a script than this: everything else in the guide is just more triggers, more commands, and a few ways to make decisions.
One thing to notice: you did not write any code for "detect the player", "face the player", or "send text to the room". The trigger did the detecting and the ordinary say command did the talking. Scripts stay short because the game already knows how to do the hard parts.
Making It Personal With Dollar Codes
A greeting is warmer with a name in it. Inside any script line, special two-character codes starting with a dollar sign are swapped for live values at the moment the line runs:
$N - the name of whoever set the trigger off (the player, usually)
$n - the same name; in this engine $n and $N are identical
$i - the scripted mob's own name
$t - the name of the current target, when there is one
$g - the text of the message that fired the trigger, such as the
words a player spoke
So this script:
GREET_PROG 100
say Well met, $N! May the road treat you kindly.
~
greets Ashley with "Well met, Ashley!" and greets Josef with "Well met, Josef!". One script, personalized for everyone, forever.
There are a few dozen more codes, covering pronouns, held items, random bystanders, the room name, and much else, plus a way to store and recall your own values. All of that lives in the mudprog-variables chapter. For this chapter, $N and $g are all you need. If you ever need to print an actual dollar sign in a message, write two of them: $$.
More Than One Command
The body can hold as many command lines as you want, and they always run in the order written, top to bottom:
GREET_PROG 100
say Ah, a customer! Come in, come in.
emote tips his hat politely.
~
On entry the room first hears the mob speak, then sees the mob tip his hat. Speech and gesture together sell the character; a say followed by an emote is the bread and butter of NPC scripting.
Speaking Versus Narrating
The say command always shows the mob as the speaker, name attached. Sometimes you want atmosphere instead: text that belongs to no one, as if the room itself were telling the story. That is the mpecho command, your first mp command:
GREET_PROG 100
mpecho A cold draft sweeps through the room as the door creaks open.
~
Everyone in the room sees that sentence, plain, with no name in front of it. Attached to a mob or to the room itself, it turns an entrance into a scene.
Two sibling commands give you aim. mpechoat sends text to one person only, and mpechoaround sends text to everyone except that person. The first word after the command names the receiver, and $n (the person who fired the trigger) is the receiver you will want nine times out of ten:
GREET_PROG 100
mpechoat $n A hooded figure catches your eye and nods only to you.
mpechoaround $n The hooded figure nods at someone in the crowd.
~
The player who walks in reads a private line, and everyone else reads the public version of the same moment. This pair is how you script secrets, pickpocket warnings, and anything else where perspectives differ.
Listening: The Speech Trigger
SPEECH_PROG fires when someone speaks near the scripted mob. Its header argument is not a percent chance; it is the list of words to listen for. The word all means "react to any speech at all":
SPEECH_PROG all
say I heard that! You said: $g
~
Say anything in the room and the mob repeats it back, because $g carries the text of the message that set the trigger off; for a speech trigger, that is the spoken line.
In real building you almost never want all; you want keywords. A header of SPEECH_PROG stew food hungry fires when a spoken line contains stew or food or hungry, and stays quiet otherwise. That is how questmasters key on the word quest and how the tavern keeper at the end of this chapter takes orders. If you need to match a whole phrase rather than any single word, start the argument with the letter p, as in SPEECH_PROG p tell me a story, which fires only when the spoken line contains that phrase.
A Life Of Their Own: The Random Trigger
Everything so far reacts to a player. RAND_PROG is different: it rolls on its own, once every heartbeat (a heartbeat is a couple of seconds), with the header number as the percent chance per roll. It is how you give a mob idle life:
RAND_PROG 100
emote hums a quiet tune while sweeping the floor.
~
The 100 here is for demonstration, so that when you test it you see it fire at once. Left on a live mob it would hum every couple of seconds, which is torture for everyone in the room. On a real NPC use a small number: RAND_PROG 5 hums roughly once a minute, RAND_PROG 10 about twice as often. Idle flavor should surprise, not spam.
Making Decisions: if, else, endif
So far every block does the same thing every time. Scripts become characters when they notice things and choose. That is the job of the if line.
An if line asks a question. If the answer is yes, the lines under it run. If the answer is no, they are skipped, and the lines under the optional else run instead. The endif line marks the end of the question, and it is required: every if must have its endif, the same way every block must have its tilde.
The questions themselves are asked with little tools called functions. The function ispc($n) answers "is $n a player character?". The function level($n) answers "what level is $n?". There is a large toolbox of these, covered in the mudprog-conditions chapter; here are two blocks that show the shape.
First, a simple yes-or-no question:
GREET_PROG 100
if ispc($n)
say A real adventurer! Welcome, $N.
else
emote sniffs the air and goes back to sweeping.
endif
~
When a player walks in, ispc($n) answers yes, so the mob speaks the welcome. When a wandering mob walks in, the answer is no, so the mob merely sniffs. One block, two behaviors.
Second, a question that compares numbers. Symbols like >= mean "is at least", so level($n) >= 20 reads as "is the arrival level twenty or higher?":
GREET_PROG 100
if level($n) >= 20
say The guild has work for a veteran like you, $N.
else
say The guild has work for everyone, even newcomers, $N.
endif
~
The indentation inside the if is optional, like all indentation, but it makes the two paths easy to see at a glance, and you should keep the habit. Ifs can sit inside other ifs, and there are also switch blocks, for loops, and while loops; those live in the mudprog-commands chapter once you want them.
Remembering Things
A mob that greets you the same way the fifth time as the first feels like a machine. Scripts can remember, using named variables stored on any object. The mpsetvar command writes one, and the var function reads one back in an if question:
GREET_PROG 100
if var($i met) == 1
say Back again for more stew? Sit down, sit down.
else
mpsetvar $i met 1
say A new face! First bowl of stew is free.
endif
~
Walk through it. The very first time anyone enters, the variable named met on $i (the mob itself) has never been set, so the question "is it 1?" answers no and the else runs: the mob welcomes the new face AND sets met to 1. Every entrance after that, the question answers yes, and the mob uses the familiar greeting instead. The mob has a memory.
One honest caveat at this level: that memory is shared, not per-player, and it lasts only as long as the mob does. Per-player memory and permanent memory are both possible and both easy, and the mudprog-variables chapter shows the patterns.
Creating Items
Scripts can hand things to the world. The mpoloadroom command clones a fresh copy of an item, by its file path, into the mob's room:
GREET_PROG 100
say You look hungry, $N. Here, this one is on the house!
mpoloadroom /obj/meal
~
The path /obj/meal is a real item file on this mud, so the block drops a hot meal at the visitor's feet. Two relatives you will meet later: mpoload clones the item into the mob's own inventory instead (useful before handing it over), and mpmload clones a whole NPC into the room. Ask a senior builder for the item paths in your area, and test with harmless objects such as /obj/meal or /obj/torch first.
Counting: A First Loop
When you want a line repeated, you do not paste it five times; you loop. A for line counts from one number to another, keeping the current count in a numbered slot such as $1 that your lines can use, and the next line closes the loop the way endif closes an if:
GREET_PROG 100
say Watch this, $N!
for $1 = 1 to 3
emote tosses ball number $1 high into the air.
next
say Ta-daa!
~
The room sees the boast, then three emotes, ball number 1 through ball number 3, then the flourish. Loops are capped by the engine (see the safety section), so even a wild for cannot run away with the mud.
Taking A Breath: mpsleep
Real people pause. A script that fires six lines in the same instant reads like a machine gun. The mpsleep command pauses the script for a number of seconds, then continues with the remaining lines:
GREET_PROG 100
say Hold on, let me find my notes...
mpsleep 2
say Ah, here they are! Now, what did you need?
~
The visitor hears the first line, two seconds of silence, then the second. Sprinkle short sleeps through any long speech and it becomes a performance instead of a wall of text.
Reacting To Combat And To Looks
Two more triggers to round out your starter kit. FIGHT_PROG fires every combat round while the mob is fighting, with the usual percent argument:
FIGHT_PROG 100
say You will regret raising a blade against me, $N!
~
At 100 the mob taunts every round, which is a lot; live NPCs usually carry FIGHT_PROG 15 or so, for an occasional bark mid-fight.
LOOK_PROG fires when a player looks at the scripted mob or item. Combined with mpechoat it makes examining things feel noticed:
LOOK_PROG 100
mpechoat $n The old man notices your gaze and winks at you.
~
Only the person doing the looking sees that line, which is exactly the point.
Attaching A Script: The mudprog Command
Scripts are attached, viewed, and removed with one builder command:
mudprog <target> - View the script and its triggers.
mudprog <target> edit - Open the line editor.
mudprog <target> set <inline> - Replace the script from one line.
mudprog <target> append <inline> - Add lines to the end.
mudprog <target> clear - Remove the script entirely.
mudprog <target> test <TRIGGER> - Fire a trigger right now.
The target is the first word after mudprog, always. It can be a mob or item in the room by name, an item you are carrying, the word here for the room you are standing in, the word self for yourself, or an online player's name. Rooms and items take scripts exactly like mobs do; only the useful triggers differ.
There are two ways to type a script in. For anything longer than a line or two, use the editor:
1. Type mudprog guard edit (using your mob's name). The
current script, if any, is shown, and the editor opens with an empty
buffer; whatever you type replaces the old script when you save.
2. Type your script lines one at a time, exactly as in the examples,
including each block's closing ~ line.
3. Finish with a single period, the . character, alone on a line. That
saves the script and it is live immediately.
4. Or type @abort alone on a line to throw your typing away and keep the
old script.
For quick one-block scripts, set does it in a single command line. Because a command line cannot contain a real line break, the semicolon stands in for one; every ; becomes a new line:
mudprog guard set GREET_PROG 100 ; say Halt! State your business. ; ~
That one command attaches the full three-line block. The append form works the same way but adds to the end of what is already there instead of replacing it, which is handy for tacking one more block onto a working script.
However you attach it, the change takes effect the moment it is saved. The engine re-reads a script whenever its text changes; there is no reload step, no reboot, nothing else to do. View your work at any time with mudprog guard, which prints the script plus a Triggers line listing every trigger the engine found in it. Get in the habit of glancing at that list: it is your best proof that the script parsed the way you meant, and it stars in the mistakes section below.
One caution about permanence. The script lives on that one copy of the mob. If the mob is destroyed, or the area resets and replaces it, the fresh copy comes up without your script. That is perfect for experimenting; when a script is ready to keep forever, a coder bakes it into the NPC's file so every copy is born with it. The example file /domains/examples/npc/mudprog_greeter.c shows exactly how, and any senior builder can do it for you in a minute.
Testing Your Script
The truest test is the real event: attach a greeting, walk out, walk back in. Say the keyword out loud. Poke the mob and see the fight lines. Do this at least once for anything you ship.
While you are iterating, though, walking in and out gets old, and that is what mudprog <target> test <TRIGGER> is for. It fires the named trigger on the target immediately, with YOU standing in as the source, the target, and everything else:
mudprog guard test GREET_PROG
runs the guard's GREET_PROG blocks as if you had just walked in. If no block for that trigger exists you are told so, which is itself useful, as you will see in the mistakes section.
Three honest limitations of test, so its results never confuse you:
- You are both the source and the target of the pretend event, so $N and $t both show your own name. - The pretend event's message, the thing $g holds, is the single word test. A block headed SPEECH_PROG stew will therefore NOT fire from mudprog test, because the word stew does not appear in the word test. Keyword blocks are tested by actually saying the keyword in the room, or by temporarily changing their header to all. - A percent chance is still a percent chance. Testing a RAND_PROG 5 block fires it five times out of a hundred. While testing, raise the number to 100, then set it back before you walk away.
Admins have one more tool, scripttest, which runs raw script lines on yourself without attaching anything to anyone:
scripttest <lines> - Run script lines, ; as line break.
scripttest fire <TRIGGER> on <name> - Fire a trigger on a scripted target.
scripttest runfile <path> - Run a script body from a file.
The first form is a scratchpad for single lines and quick experiments, such as scripttest mpecho does this work. The fire form does what mudprog test does but lets you name any target in the room. The runfile form reads the script body from a file and is the reliable way to test anything full of dollar codes, because some client and telnet setups quietly mangle typed dollar signs; if a $ code misbehaves when typed inline but the script looks right, test it from a file or through the mudprog editor before blaming the code.
Why Scripts Never Crash The Game
This deserves its own section, because fear of breaking the mud is what keeps new builders from experimenting, and with MUDProg that fear is unfounded. The engine is built defensively at every layer:
- A command with a bad or missing argument simply does nothing. Asking mpechoat to whisper to someone who is not there, or mpoloadroom to clone a file that does not exist, quietly no-ops and the script moves to the next line. - A word the engine does not recognize is not an error either: it is handed to the mob as an ordinary game command. If the game does not know it, the mob fails that one command in private, exactly as you would if you typed gibberish at the prompt. - Runaway scripts stop themselves. Every trigger run has a budget of a few thousand steps, and every loop stops after two thousand passes. A script that trips the cap is halted mid-run and a note is written to the log file /log/script_runaway so a builder can find and fix it. The mob shrugs and carries on. - A broken script never blocks the game event that fired it. The player still enters the room, still gets the item, still lands the blow; only the flavor the script would have added goes missing.
The worst outcome a beginner can produce is a script that does nothing visible, or spams the room until someone clears it. Both are fixed with one mudprog <target> clear. So experiment freely: attach, test, tweak, repeat.
Common Beginner Mistakes
Every scripter makes these three in their first week. Here is each one, the broken version, what you will actually see in game, how to diagnose it, and the fix.
Mistake One: Forgetting The Tilde
You write two blocks but forget the ~ between them:
GREET_PROG 100
say Hello there, traveler!
SPEECH_PROG hello
say Hello to you too!
~
Nothing complains when you save it, because the engine cannot know a tilde is missing; it just reads on. Without the terminator, everything up to the final ~ is ONE block: a GREET_PROG whose body is three lines, one of which happens to be the text SPEECH_PROG hello. The symptoms in game are exactly the merge: when a player walks in, the mob says BOTH greetings back to back (the stray SPEECH_PROG hello line in the middle is tried as an ordinary game command, fails silently, and is skipped), and saying hello to the mob does nothing at all, because no speech trigger was ever registered.
The diagnosis takes five seconds: type mudprog <target> and read the Triggers line. It says GREET_PROG only. The SPEECH_PROG you thought you wrote is not in the list, so the engine never saw it as a header. Whenever a trigger you wrote is missing from that list, hunt for a missing tilde directly above it.
The fix is one character, ending the first block before the second begins:
GREET_PROG 100
say Hello there, traveler!
~
SPEECH_PROG hello
say Hello to you too!
~
Now the Triggers line reads GREET_PROG, SPEECH_PROG, and both behaviors work independently.
Mistake Two: The Wrong Trigger Name
You misremember GREET_PROG as GREETING_PROG:
GREETING_PROG 100
say Welcome, friend!
~
Again, saving it produces no error. The engine accepts any header name and files the block under it faithfully; it has no way to know you meant something else. But no event in the game is called GREETING_PROG, so nothing ever fires it. The symptom is the most frustrating one in scripting: total silence. You walk in and out of the room and the mob just stands there.
Diagnosis, two steps. First, mudprog guard test GREET_PROG answers that no GREET_PROG block exists on the target, which tells you the trigger you expected is not there under that name. Second, mudprog guard shows Triggers: GREETING_PROG, and there is the typo, in writing. Check any name you are unsure of against the trigger table in help mudprog or the mudprog-triggers chapter; if a trigger you wrote never fires, compare its spelling letter by letter.
The fix:
GREET_PROG 100
say Welcome, friend!
~
Walk out, walk in, and the mob finally speaks.
Mistake Three: Testing Without A Target
The test tools each need to be told WHO to test, and each fails politely but unhelpfully when you forget. The classic slip is dropping the target from the mudprog command:
mudprog test GREET_PROG
The first word after mudprog is always the target, so this asks to view the script of something in the room called test, and you are told there is no mob, item, or room here by that name. The fix is to put the target first, then the action:
mudprog guard test GREET_PROG
The same trap exists in scripttest fire: typed with no target while alone in a room, there is nothing to fire at, and it reports that no host was found. Name the scripted mob explicitly:
scripttest fire GREET_PROG on guard
And the subtlest form of the mistake is testing the wrong thing at the right target: firing a keyword speech block with mudprog test and concluding it is broken. As covered in the testing section, the test message is the word test, so SPEECH_PROG hello cannot match it. Walk up and say hello instead; the block was fine all along.
Putting It All Together: A Complete NPC
Here is everything in this chapter combined into one small, shippable character: a tavern keeper with a startup routine, a warm greeting, a keyword-driven service, idle flavor at a sane rate, and a combat line.
ONCE_PROG
emote lights the lanterns and wipes down the bar.
~
GREET_PROG 100
say Welcome to the Rusty Flagon, $N! Sit anywhere you like.
emote slides a menu down the bar.
~
SPEECH_PROG stew food hungry
say One bowl of my famous stew, coming right up!
mpoloadroom /obj/meal
emote ladles steaming stew into a wooden bowl.
~
RAND_PROG 8
emote polishes a dented tankard with a rag.
~
FIGHT_PROG 100
yell Take it outside! No brawling in my tavern!
~
Block by block:
- ONCE_PROG runs a single time, when the mob first loads. It takes no argument. Use it for setup theater: lighting lanterns, taking up a post, an opening line. - GREET_PROG 100 is our familiar greeting, a say plus an emote, with the visitor's name spliced in by $N. - SPEECH_PROG stew food hungry listens for any of three words. A player who says I am so hungry, or asks is there food here, or just says stew, gets the same happy service: an answer, a real meal cloned onto the floor by mpoloadroom, and a ladling emote to sell it. Notice how three commands in sequence read as one continuous moment. - RAND_PROG 8 gives him idle life at a rate that charms instead of spamming: a polish of a tankard every half minute or so. - FIGHT_PROG 100 makes him bellow every round if a brawl ever reaches him. For a fighting NPC you would lower it; for a shouting innkeeper, every round is the joke.
Attach it with mudprog <target> edit, typing the blocks exactly as printed and finishing with the period. Then run the checklist a professional would: view it and confirm the Triggers line shows all five; test the greeting with mudprog <target> test GREET_PROG; say the word stew out loud and collect your meal; and idle in the room a minute to catch him polishing. Five blocks, no code, a character.
Where To Go Next
You now know the entire structure of MUDProg: blocks, triggers, arguments, commands, dollar codes, if and else, and the test loop. Everything from here is vocabulary, and each chapter of this guide is one shelf of it:
- mudprog-triggers catalogs every trigger, its argument, and which kind of object it belongs on, including item, room, door, and vendor triggers this chapter skipped. - mudprog-variables covers every dollar code, storing values with mpsetvar, per-player and permanent memory, and inserting function results into text. - mudprog-commands walks through all of the mp commands, from movement and combat to quests and money, plus switch, while, and the rest of control flow. - mudprog-conditions covers everything you can ask in an if line and how to combine questions with and, or, and not.
The short reference card for all of it is help mudprog, and help scripttest covers the admin test harness. Now go give a guard a bad attitude.
This chapter is the complete reference for MUDProg triggers: every event the scripting engine can react to, what makes each one fire, what its header argument means, which object should carry the script, and what the script can see at the moment it runs. If you have never scripted before, read the first three sections slowly; everything after that is a catalogue you can dip into as needed. Every example here is a complete script you can paste onto a test mob with the mudprog command and fire immediately.
What a Trigger Is
A MUDProg script is a list of instructions attached to a mob, an item, or a room. The instructions do not run all the time. They run when something happens: a player walks in, someone speaks, a fight starts, an item is picked up. Each kind of happening is called a trigger. When the game detects the happening, it looks at the object's script, finds the block written for that trigger, and runs it.
Think of a trigger as the sentence "WHEN this happens, DO these things." The trigger name is the WHEN. The lines underneath it are the DO.
Triggers only ever fire on objects that actually carry a script, so an unscripted guard costs the game nothing. You attach a script with the mudprog command (see help mudprog for the editor); the engine reparses automatically every time you change it.
Anatomy of a PROG Block
Every block has three parts: a header line, a body, and a closing tilde.
GREET_PROG 100
say Welcome in from the road, $N. Take any seat by the fire.
emote wipes down a tankard and sets it ready.
~
The header is the trigger name, GREET_PROG, followed by its argument, here the number 100. The body is one or more lines of commands. The tilde on its own line says "this block is finished." A script can hold many blocks, for many different triggers, one after another.
Details worth knowing from the start:
- The _PROG suffix is optional when you write the header. GREET means the same as GREET_PROG. This guide always writes the full form. - A misspelled trigger name does not cause an error. It parses fine and then simply never fires, because no game event carries that name. If a block seems dead, run mudprog <target> and check the trigger list it prints against the names in this chapter. - Lines starting with # or * are comments and are ignored. - You may write several blocks for the SAME trigger. When the event happens, every block for that trigger runs, in the order written, and each block checks its own header argument separately:
GREET_PROG 100
say This is the first of my two blocks.
~
GREET_PROG 100
emote adds a flourish from the second block.
~
- Every run of a trigger has a step budget and a loop cap, so a runaway script stops itself instead of hanging the mob. Blown budgets are logged to the script_runaway log.
What the Script Sees When a Trigger Fires
At the instant a trigger fires, the engine takes a snapshot of who and what was involved, and your script reads that snapshot through dollar codes. The five that matter for this chapter:
$n - the source: whoever caused the event, usually a player.
$t - the target: a second party, when the event has one.
$o - the item involved, when the event has one.
$g - the text rider: a spoken line, a spell name, a number.
$i - the scripted object itself, the one running the script.
$N is the capitalised form of $n, and $G is the original-case form of $g. The full dollar-code table lives in the variables chapter. Each trigger entry below tells you exactly what these codes hold when it fires; when an entry says nothing about $o or $g, treat them as empty for that trigger.
Trigger Argument Forms
The header argument is the gatekeeper: after the event happens but before the body runs, the engine checks the argument, and if the check fails the block stays silent. Most triggers accept the following forms.
Blank, all, or 100. All three mean "always fire". These are equivalent:
GREET_PROG 100
say I greet every single visitor. Lower my header to 25 and I would greet one in four.
~
A percent. A whole number from 1 to 99 is a chance to fire, rolled fresh each time the event happens. RAND_PROG 15 fires on roughly fifteen of every hundred heartbeats; GREET_PROG 25 greets one visitor in four. Use small percents on frequent events or your mob becomes unbearable.
Keywords. On triggers that carry text in $g (speech, channels, spells), a header of one or more words fires the block only if ANY of those words appears anywhere in the text, case-insensitively. So the header gold treasure matches "the gold is gone" and also "treasures untold", since the match is by substring. On triggers that carry no text, a keyword header is meaningless and the block simply always fires.
A phrase. Start the header with the letter p and the rest is matched as one whole phrase instead of separate words. p open sesame matches only speech containing the words "open sesame" together, in order.
A zapper mask. A header starting with a dash restricts WHO may set the trigger off, judged against the source ($n). A mask is one or more clauses; each clause is a dash-word optionally followed by the values that qualify, and EVERY clause must pass:
GREET_PROG -player -level 1
say Ah, flesh and blood, and seasoned enough to walk in here. Welcome, $N.
~
GREET_PROG -npc
emote eyes the creature warily and says nothing.
~
Supported clauses: -player (or -pc) and -npc (or -mob) take no values and check what the source is; so do -good and -evil. -class, -race, -sex, -name, and -deity take one or more values and pass when the source matches any of them, case-insensitively. -level takes a plain number meaning at-or-above, or a range like 30-40. A clause type the engine does not recognise passes leniently rather than blocking the trigger.
Special headers. A handful of triggers give their header a different meaning entirely: HITPRCNT_PROG takes a health threshold, TIME_PROG and DAY_PROG take lists of hours and days, QUEST_TIME_PROG takes a quest id and minute list, CNCLMSG_PROG and EXECMSG_PROG take a message code, IMASK_PROG takes a plain substring, REGMASK_PROG takes a regular expression, and FUNCTION_PROG takes the routine's name. Each is explained at its own entry below.
Arrival and Presence
GREET_PROG fires when a player walks into the room where the scripted mob stands. It fires about one second after the arrival, so your greeting appears after the player has read the room description, and it quietly skips firing if the player has already moved on. It also works on a scripted ROOM: attach the script with mudprog here set and the room itself greets. Header: percent or zapper mask. When it fires, $n is the arriving player. The script goes on the mob doing the greeting, or on the room.
The innkeeper from the anatomy section is the classic form; here is a sharper one that only greets seasoned players, from the zapper section above. For the plainest possible case:
GREET_PROG 100
say Welcome in from the road, $N. Take any seat by the fire.
emote wipes down a tankard and sets it ready.
~
ALL_GREET_PROG and GROUP_GREET_PROG exist for CoffeeMUD compatibility. In CoffeeMUD, ALL_GREET also catches entrants the mob cannot see, and GROUP_GREET fires once for a whole entering party. On this mud all three greet triggers are wired to the same arrival event and fire per entering player, so scripts ported from CoffeeMUD behave sensibly, and on native scripts you can treat them as extra GREET blocks. Both fire on mobs; rooms fire GREET, ALL_GREET, and ENTRY. Header and snapshot are the same as GREET_PROG.
ALL_GREET_PROG 100
emote nods to everything that enters, seen or unseen.
~
GROUP_GREET_PROG 100
say Travelling together keeps you alive out there.
~
ENTRY_PROG on a ROOM fires when a player enters, alongside the room's GREET_PROG; $n is the entering player. This is the natural trigger for trapped floors, ambience, and threshold effects. Put the script on the room:
ENTRY_PROG 100
mpechoat $n A cold draft slides across the back of your neck as you enter.
~
ENTRY_PROG, ARRIVE_PROG, and EXIT_PROG on a MOB describe the mob's own movement: the engine reserves ENTRY and ARRIVE for the moment the scripted mob steps into a room, and EXIT for the moment it leaves, with no source (do not use $n in these). The live movement code does not yet call the mob-side hooks, so today these three fire on mobs only through mudprog <target> test; write them for the future, or drive the same effect from the wander system:
ARRIVE_PROG 100
emote strides in and takes stock of the room.
~
EXIT_PROG 100
emote departs without a backward glance.
~
LOGIN_PROG and LOGOFF_PROG are world-wide triggers: they fire on EVERY loaded scripted object that defines them, wherever it stands, when any player enters or leaves the game. $n is that player, who is usually NOT in your room; the reaction happens wherever the scripted object is. Header: leave it blank. An object hears world-wide triggers once its script has been parsed, which happens automatically when you attach or view the script and shortly after a scripted NPC loads, so in practice you never need to think about it.
LOGIN_PROG
mpecho The innkeeper glances up as word spreads that $N has arrived in the realm.
~
LOGOFF_PROG
mpecho The innkeeper quietly strikes $N's name from the evening list.
~
LEVEL_PROG is world-wide in the same way: it fires when any player gains a level. $n is the player and $g holds the new level as a number, so you can compare it or speak it:
LEVEL_PROG
mpecho Somewhere a bell tolls, honoring $N reaching level $g.
~
Speech and Sound
SPEECH_PROG is the workhorse of talking NPCs. It fires when a player in the room says something, and the scripted mob is the listener. $n is the speaker; $g is the spoken line in lower case ($G keeps the original case). Header: keywords, a p phrase, a percent, or all. The mob never triggers on its own speech, so it cannot argue with itself. Put the script on the mob that should listen.
SPEECH_PROG all
say I hear every word spoken in this room, $N.
~
SPEECH_PROG gold treasure
say Gold, you say? Now you have my full attention.
~
SPEECH_PROG p open sesame
say The old password! Few remember it these days.
~
The first block answers everything; the second only lines containing gold or treasure; the third only the exact phrase. All three are checked on every spoken line, so one sentence can set off more than one block. Write your keyword blocks narrow and your catch-all blocks rare, or the mob will talk over itself.
ACT_PROG and MASK_PROG are CoffeeMUD compatibility names wired to the same heard-speech event as SPEECH_PROG, with the same header forms and snapshot. Use them when porting CoffeeMUD scripts; on native scripts prefer SPEECH_PROG for readability.
ACT_PROG all
emote tilts his head, tracking the words with interest.
~
MASK_PROG all
emote makes a quiet note of what was just said.
~
SPEAK_PROG is the mirror image of SPEECH_PROG: it fires on the SPEAKER's own script when the speaker says something. Attach it to a scripted mob (or even a player) and it reacts to its own lines; $g is what was said. Use it for side effects of speech, dramatic echoes, or a mob that always follows certain words with a gesture. Be careful not to make the body speak words that match the header, or the mob will chain-react.
SPEAK_PROG all
mpecho A faint echo repeats the last words spoken.
~
SOCIAL_PROG is reserved for reacting to emotes and socials performed at the mob. The engine and the bridge define it, but the live emote system does not yet call the hook, so today it fires only through mudprog <target> test SOCIAL_PROG. To react to emotes in live play right now, use REGMASK_PROG (below), which sees all room text.
SOCIAL_PROG 100
emote beams, delighted by the attention.
~
CHANNEL_PROG is world-wide and fires on any traffic over the chat channels. $n is the person speaking on the channel and $g holds the channel name followed by the message, as one line of text. Because the header keywords are matched against that whole line, putting a channel name in the header is how you listen to one channel only. A blank header hears everything.
CHANNEL_PROG
emote cups an ear, listening to distant chatter.
~
CHANNEL_PROG gossip
emote scribbles down a fresh piece of gossip.
~
CMDFAIL_PROG fires when a player in the room types a command the game does not recognise. It fires on the room and on every scripted mob present. $n is the confused player and $g is the exact line they typed. This makes a kindly tutor, or a mocking parrot:
CMDFAIL_PROG 100
say No such art is practiced here, $N. Perhaps you misspoke.
~
Idle Life
ONCE_PROG fires exactly once per mob, about one second after the mob loads into the world. It is the place for setup: initial equipment commands, opening emotes, variable defaults. A respawned mob is a fresh copy, so it runs its ONCE_PROG again; a mob that merely walks between rooms does not. Header: leave blank. There is no source, so do not use $n here. Mob only.
RAND_PROG is the heartbeat of idle life. Every NPC heartbeat, roughly every two seconds, the header percent is rolled, and on success the body runs. This is how mobs hum, fidget, sweep floors, and mutter. Keep the percent low; at 100 the mob acts every couple of seconds without pause. No source here either. Mob only, since rooms and items have no heartbeat.
ONCE_PROG
emote shoulders his pack, ready for a long shift.
~
RAND_PROG 15
emote hums an old marching tune.
~
DELAY_PROG fires from the mob's idle pulse once the countdown stored in the mob's _mudprog_delay property has run out; with nothing set it fires on the first idle pulse after load. It is a lightly-wired CoffeeMUD compatibility trigger. For dependable "do something N seconds after spawning", prefer an ONCE_PROG whose body uses mpalarm (see the commands chapter); reach for DELAY_PROG mainly when porting.
DELAY_PROG
emote finally stirs from a long, deep stillness.
~
The Clock and the Calendar
TIME_PROG fires when the mud clock reaches an hour you list in the header. The header is REQUIRED: one or more hours from 0 to 23, separated by spaces; a blank header never matches anything. The mob checks the clock on its own heartbeat and fires once as the hour changes to a listed value, with the hour riding in $g. Because the check rides the heartbeat, this is a mob-only trigger, and the mob must be loaded (someone visited its area) for it to notice the hour.
DAY_PROG is the calendar cousin: it fires world-wide as each new mud day begins, on scripted objects whose header lists that day number. Like TIME_PROG the header list is required. $g holds the day number.
TIME_PROG 0 6 20
emote lights a lantern against the changing light.
~
DAY_PROG 1 15
say A new day is marked on the calendar, friends.
~
AGE_PROG fires world-wide whenever any player crosses another full hour of total played time. $n is the player and $g is their new age in hours. Leave the header blank; a number there would be read as a percent chance. Test $g in a condition if you care about a particular age.
AGE_PROG
mpecho An hourglass turns itself over, marking another hour of $N's journey.
~
QUEST_TIME_PROG serves time-limited quests. When a player accepts a task that carries a time limit, the engine pulses once per minute until the limit runs out. Each pulse fires world-wide on scripted objects defining this trigger. The header is the quest id, optionally followed by the minute marks you care about, where the numbers are minutes REMAINING and count down to 0. A header of just the quest id fires every minute. $n is the questing player and $g holds the quest id and the minutes left. A typical pairing is a giver whose speech starts the clock and a warning at five and one minutes:
SPEECH_PROG all
say Bring me the relic before the hour runs out!
~
QUEST_TIME_PROG relic_run 5 1
mpechoat $n The old man's warning rings in your ears. Time is running short.
~
Combat
FIGHT_PROG fires once per combat round, roughly every two seconds, for as long as the scripted mob is fighting. $n is the mob's current enemy. Header: percent, and you almost always want one well below 100, or the mob chatters through every single round. This is where taunts, special attacks via mpcast, and battle theatrics live.
HITPRCNT_PROG gives bosses their phases. Its header is not a chance but a HEALTH THRESHOLD: each combat round the mob's current health percent is compared against it, and the block fires when health is at or below the header. The header is required and must be above zero. $n is the enemy and $g carries the current health percent. Important: it fires EVERY qualifying round, not once, so for a one-time phase change guard it with a variable, as shown in the second example.
DEATH_PROG fires as the mob dies, before the corpse takes its place. $n is the killer. Keep the body immediate: last words, a curse, loading a reward onto the floor. Delayed commands scheduled here quietly do nothing once the mob is gone, and there is no coming back; do not try to heal your way out.
KILL_PROG is the other side: it fires on the scripted mob when it kills its target. $n is the fallen victim.
FIGHT_PROG 100
emote spits blood and grins through the exchange.
~
HITPRCNT_PROG 50
say Half measures! You will have to do better than that!
~
DEATH_PROG
emote crumples, whispering a name no one recognizes.
~
KILL_PROG
emote plants a boot on the fallen and scans for the next threat.
~
In a real script that FIGHT_PROG header would be 20 or 30; it is 100 here so the example fires on demand. And here is the one-shot enrage, using a stored variable so the speech happens a single time per fight rather than every round below the threshold:
HITPRCNT_PROG 30
if var($i enraged = 1)
return
endif
mpsetvar $i enraged 1
say Enough! Now you face my true strength!
~
DAMAGE_PROG is meant to fire each time the mob takes a hit, with $n as the attacker. Its bridge hook is not yet called by the live combat code, so today it fires only via mudprog <target> test; for live reactions to being hurt, use FIGHT_PROG and HITPRCNT_PROG, which cover nearly every practical case.
DAMAGE_PROG 100
emote staggers, clutching the fresh wound.
~
Items Changing Hands
GIVE_PROG fires on a scripted mob when a player hands it an item. $n is the giver and $o is the item. Header: percent or all; there is no text rider on this event, so keyword headers do not filter anything here. To accept only particular items, test $o or use the has() and isname() functions in a condition (see the functions chapter). This is the classic fetch-quest turn-in trigger.
GIVE_PROG 100
say Ah, $N, you bring me $o. I will not forget this kindness.
~
GIVING_PROG is the item's point of view: it fires on the ITEM when someone gives it away, and the room and scripted mobs present witness the same event. $n is the giver, $t the recipient, $o the item itself. Put the script on the item.
GIVING_PROG 100
mpechoat $n The trinket grows warm in your hand as you give it away.
~
BRIBE_PROG is reserved for coin gifts, with the amount in copper riding in $g. The live money-give path does not yet call its hook, so today it fires only via mudprog <target> test; the shape of a finished bribe-taker looks like this, testing the amount with a condition rather than the header, since a number in the header would mean a percent:
BRIBE_PROG 100
if number($g > 99)
say Now that buys some proper cooperation, friend.
else
say Coin is coin, but this barely buys my silence.
endif
~
GET_PROG fires when someone picks the scripted item up. The event fans out: it fires on the item itself, on the room, and on scripted mobs standing there, so a floor guard can object to looting. $n is the taker and $o the item. DROP_PROG is the mirror for dropping. Put these on the item (or on the room or a witness mob to watch all takings).
GET_PROG 100
mpechoat $n The coin purse squirms slightly as you pick it up.
~
DROP_PROG 100
mpecho The coin purse hits the floor with a resentful jingle.
~
GETTING_PROG and DROPPING_PROG fire on the LIVING who did the taking or dropping, not the item. Attach them to a scripted mob (a trained scavenger, a cursed knight) and they run whenever that mob picks up or lets go of anything; $o is the item.
GETTING_PROG 100
emote pockets the find with practiced speed.
~
DROPPING_PROG 100
emote lets it fall without a second glance.
~
PUT_PROG fires when an item goes into a container. It fires on the item being put, where $t is the container, and also on the CONTAINER, where $o is the arriving item; $n is always the person doing it. Script whichever side of the exchange interests you. PUTTING_PROG fires on the person doing the putting, with $t the container and $o the item.
PUT_PROG 100
mpecho The chest creaks as something settles inside it.
~
PUTTING_PROG 100
emote double-checks the latch afterward, twice.
~
Wearing, Removing, Consuming
WEAR_PROG fires on the scripted item when it is worn, or wielded in the case of a weapon. $n is the wearer and $o the item itself; the room and scripted mobs present witness it too. REMOVE_PROG is the mirror, firing when the item is taken off or unwielded. These make talking swords and haunted armor:
WEAR_PROG 100
mpechoat $n The armor settles over you, snug and strangely warm.
~
REMOVE_PROG 100
mpechoat $n A chill washes over you as the enchantment lets go.
~
WEARING_PROG fires on the LIVING who put the item on, with $o the item. Attach to a scripted mob that should react to its own dressing:
WEARING_PROG 100
emote adjusts each strap with a soldier's care.
~
CONSUME_PROG fires on scripted food or drink at the moment it is eaten or drunk. $n is the consumer, $o the meal. The room and witnesses see the event as well. Remember the item is usually destroyed right afterward, so say your piece now:
CONSUME_PROG 100
mpecho A savory aroma lingers in the air for a long moment.
~
Doors, Locks, and Shops
OPEN_PROG, CLOSE_PROG, LOCK_PROG, and UNLOCK_PROG fire on a scripted container or door when it is opened, closed, locked, or unlocked. $n is the person doing it; note that on these four $o is empty, since the scripted thing IS the door. Put the script on the container or door itself; the room and scripted mobs present witness the event too.
OPEN_PROG 100
mpecho The hinges shriek like something waking up angry.
~
CLOSE_PROG 100
mpecho The lid thuds shut, and the silence afterward feels thick.
~
LOCK_PROG 100
mpecho The lock clicks home with grim finality.
~
UNLOCK_PROG 100
mpecho The mechanism releases with a soft, guilty click.
~
BUY_PROG fires on a scripted vendor when a player buys something from it; $n is the buyer and $o the item bought. SELL_PROG fires when a player sells something TO the vendor; $n is the seller and $o the item sold. Put both on the vendor. These are pure reaction triggers; to actually refuse a sale, use the message bus veto described below.
BUY_PROG 100
say A fine choice, $N. That $o has a story to it.
~
SELL_PROG 100
say Hmm. I can always find a home for $o, I suppose.
~
Magic
CASTING_PROG fires when magic happens nearby. When any living uses a class skill, the event fans out to the user, the room, and scripted mobs present, with the skill's name riding in $g; when a spell built on the old spell library is cast, it fires on the caster's own script with the spell name in $g. Header keywords match against that name, so CASTING_PROG fireball reacts only to fireballs while a blank or all header reacts to everything. $n is the caster.
CAST_PROG is narrower: it fires on the scripted mob that a spell from the old spell library RESOLVES ON. $n is the caster, the mob itself is the target, and $g is the spell name. Most live magic flows through the skill system, so expect CASTING_PROG to be the trigger you actually see firing.
CASTING_PROG 100
emote traces the gesture in the air, mirroring the casting.
~
CAST_PROG 100
say I felt that spell land on me, $N. Mind your aim.
~
Companions, Mounts, and Being Watched
FOLLOW_PROG fires on the scripted leader when someone starts following it; UNFOLLOW_PROG when they stop. $n is the follower in both. Put the script on the one being followed.
FOLLOW_PROG 100
say Stay close, $N, and step only where I step.
~
UNFOLLOW_PROG 100
say Off on your own then, $N? Watch the shadows.
~
RIDE_PROG fires on the scripted MOUNT when someone climbs on; $n is the rider. RIDING_PROG fires on the RIDER's own script, with $t the mount. Two sides of the same saddle; script whichever carries the personality.
RIDE_PROG 100
emote shifts its weight, accepting the rider with a snort.
~
RIDING_PROG 100
emote settles into the saddle with an easy confidence.
~
LOOK_PROG and LLOOK_PROG fire when a player looks at the scripted mob; $n is the looker. Both names are wired to the same event (LLOOK is CoffeeMUD's long-look), so use one or both. Mobs only at present. A mob that notices being studied is instantly more alive:
LOOK_PROG 100
emote straightens under the scrutiny, chin lifted.
~
LLOOK_PROG 100
say Seen enough, $N, or shall I turn around for you?
~
The Message Bus: Observing and Vetoing Actions
Underneath the named triggers, most game actions travel a message bus, and each action has a short CODE: GET, DROP, PUT, WEAR, REMOVE, OPEN, CLOSE, LOCK, UNLOCK, EAT, DRINK, CAST, ATTACK, ENTER, LEAVE, BUY, SELL, GIVE. Two special triggers plug straight into that bus, and their header is a code spec, optionally followed by a keyword mask matched against the message text, which for item actions is the item's key name. The spec ALL matches every code, and some friendly aliases are accepted: CONSUME covers both EAT and DRINK, FIGHT and KILL cover ATTACK, ARRIVE covers ENTER, EXIT and DEPART cover LEAVE, and SPELL covers CAST.
EXECMSG_PROG is the observer. While a matching action executes, every scripted object in scope runs its block: the object acted on, the room, everything scripted in the room, and the actor. Notably this includes scripted ITEMS lying on the floor, which makes EXECMSG_PROG the way an inanimate object can watch events that happen to other things. When it fires, $n is the actor, $t the target, $o the item, and $g the message text.
EXECMSG_PROG
mpecho A quiet clerk notes down everything that happens here.
~
EXECMSG_PROG GET ALL
mpecho The clerk pays special attention to things being picked up.
~
A blank header observes every code; GET ALL observes only picking-up. The mask after the code narrows further: GET relic observes only takings of items whose name contains relic.
CNCLMSG_PROG is the veto, and it is the most powerful trigger in the engine. It runs BEFORE the action commits. If the code and mask match, your block runs INSTEAD of the action, and the action is cancelled: the item is not taken, the door does not open, the fight does not start, the player does not leave the room. Because the normal behavior is replaced entirely, your body must tell the player what happened, or the world just silently refuses. Scope is the same as EXECMSG_PROG, so a veto can live on the item itself, on the room, or on a guard mob standing there. Safety valve: if a veto script has an error, the action is ALLOWED, so a broken script can never lock up the game.
GREET_PROG 100
say Mind the relic on the pedestal, $N. It does not like to be handled.
~
CNCLMSG_PROG GET relic
mpechoat $n The relic twists away from your fingers as though it were alive.
~
Attach that to a mob or room, drop an item named relic there, and watch the take fail with your message instead of succeeding. The classic uses are cursed items (veto GET), sealed rooms (veto ENTER or LEAVE on the room), sanctuaries (veto ATTACK), picky vendors (veto BUY or SELL), and geas food (veto CONSUME). Be careful with broad specs: a CNCLMSG_PROG ALL cancels everything in scope, including movement, and is almost never what you want outside a demonstration.
Text Masks: Reacting to Raw Output
Two triggers watch the raw lines of text an object sees, colors stripped, and they are the escape hatch when no named trigger covers your event.
IMASK_PROG fires when the scripted object's OWN action produces visible text containing the header, matched as a case-insensitive substring; a blank header matches every own-action line. REGMASK_PROG fires when ANY line the object sees, from anyone, matches the header as a regular expression. In both, $g holds the line of text. REGMASK_PROG is how a mob reacts to emotes, combat spam, or anything else printed in front of it today.
One serious warning: if a mask trigger's body produces text that its own header matches, the script sets itself off again, around and around. Keep mask bodies quiet from the object's own point of view. The command mpechoaround $i is perfect for this: the room hears the reaction, but the scripted object itself does not, so no loop is possible.
IMASK_PROG
mpechoaround $i A faint chime sounds each time the bellringer acts.
~
REGMASK_PROG laughs|giggles
mpechoaround $i The bellringer rings a sour, disapproving note at the merriment.
~
The first block chimes at the bellringer's every visible action; the second answers any laughter or giggling it witnesses, using the regular expression bar character to mean or.
Named Subroutines: FUNCTION_PROG
FUNCTION_PROG never fires on its own. Its header is a name, and the block becomes a routine you invoke yourself with the mpcallfunc command, or from a condition with the callfunc() function. Use it to share one piece of behavior between many triggers instead of pasting it into each block. A return line ends the routine, and any text after return becomes the routine's result when called through callfunc().
GREET_PROG 100
mpcallfunc fanfare
say That fanfare was for you, $N.
~
FUNCTION_PROG fanfare
mpecho Trumpets sound a short, bright fanfare.
return done
~
A Complete Worked NPC
Here is a small finished character built from four triggers: a bridge keeper who challenges arrivals, accepts a story or a gift as toll, and has a survival instinct. Attach it to any mob and every behavior is live at once.
GREET_PROG 100
say Halt, $N. This bridge has a toll: one gift, or one good story.
~
SPEECH_PROG story tale
say A story! Sit, tell it, and cross with my blessing.
emote settles onto the parapet, listening intently.
~
GIVE_PROG 100
say A gift honestly given. Cross freely, $N.
emote waves $N across the bridge with a flourish.
~
HITPRCNT_PROG 40
say Enough! No toll is worth dying over!
mpflee
~
The GREET block sets the scene for every arrival. The SPEECH block listens for the words story or tale in anything said nearby. The GIVE block fires when any item is pressed into his hands. And if someone decides the toll is better paid in violence, the HITPRCNT block has him flee once he drops to two fifths of his health, every round it still applies, which in practice means he keeps trying to run until he makes it out.
Testing Your Triggers
mudprog <target> test <TRIGGER> fires any trigger on the spot, with you as both source and target and the word test as the text rider. That last detail matters: a block whose header is keywords will not match the word test, so it stays silent under the test command. That is the header doing its job, not a broken script; temporarily switch the header to all, or say the keyword out loud for speech triggers, to see the body run. Blocks with special headers behave predictably under test as well, for instance a HITPRCNT_PROG always fires under test since the rider is not a number, while TIME_PROG fires under test only if its list contains 0.
scripttest runs raw script lines on yourself without attaching anything, and scripttest fire <TRIGGER> on <name> fires a real trigger on a scripted target. One gotcha for telnet users: some clients mangle the dollar sign, so for bodies full of dollar codes write the script to a file and use scripttest runfile instead of typing it inline.
While developing, mplog lines write to the mudprog log, and a script that blows its step budget is recorded in the script_runaway log with the mob and trigger name, so silent failures always leave a trail.
This chapter is the complete command reference for MUDProg, Rogue's builder scripting system. It assumes you have never written a script or a line of code in your life. Every command in the engine is listed here, each with its exact syntax, a plain-English explanation of what it does, and a worked example you can paste onto a test mob and fire yourself.
If you have not read the main help mudprog page yet, start there. It explains what a script is, how to attach one with the mudprog command, and what triggers are. This chapter is the dictionary you come back to while writing.
What a Command Line Is
A script is a list of lines. When a trigger fires, the engine reads your lines from top to bottom and performs each one in order. Every line that is not control flow (if, else, switch, for, while, and their closers) is a command line.
A command line has two parts: the first word is the command, and everything after it is the arguments. In the line:
mpechoat $n A hush falls as you enter.
the command is mpechoat, the first argument is $n (the player who set off the trigger), and the rest of the line is the text to show. Command words are not case sensitive: MPECHOAT, MpEchoAt, and mpechoat are all the same command.
Commands starting with the letters mp are script commands, handled by the engine itself. They are the subject of this chapter. Any other first word is treated as an ordinary game command and performed by the scripted object itself, exactly as if a player had typed it. More on that below.
One promise holds for every command here: scripts are builder-safe. A command given a bad argument, a missing target, or a path that does not exist simply does nothing. It never crashes the mob, the room, or the mud. You can experiment freely.
How to Read the Syntax Lines
Each command below starts with a syntax line like:
mpdamage <who> <amount> [type]
The angle brackets and square brackets are notation for this manual only. You never type them in a real script. A word in angle brackets is a required argument you must supply; a word in square brackets is optional. So all of these are correct real lines: mpdamage $n 25 and mpdamage $n 25 heat.
Three kinds of argument appear over and over:
<who> is an object reference: a living thing or an item. You can write a dollar code such as $n (the source, usually the player who fired the trigger), $t (the target), $i (the scripted object itself), or $b (the last thing a load command created). You can also write a plain name such as guard, which is looked up in the mob's room first and then in its inventory, or the word self for the scripted object.
<room> is a room reference: a full file path starting with a slash, such as /realms/loralei/aurin/rooms/room1, or the word here for the room the scripted object is standing in, or a dollar code naming a living thing, which means that creature's current room.
<text> is free text. Before it is shown or used, every dollar code inside it is replaced with its live value, so say Welcome, $N! greets each visitor by name. See help mudprog-variables for the full dollar code table.
Throughout this chapter, the scripted object carrying the script is called the host. The host can be a mob, an item, or a room; most examples use a mob because that is the common case.
Any Other Line Is a Game Command
This is the single most useful thing to know. If the first word of a line is not an mp command and not control flow, the engine substitutes the dollar codes and then has the host perform the whole line as a normal game command, just as if the mob were a player typing it. That means say, emote, yell, whisper, shout, every social such as nod, smile, bow, and cackle, plus get, drop, give, wear, wield, open, and anything else a player could type, all work in scripts with no special support at all.
A baker who reacts to a customer walking in needs nothing but plain commands:
GREET_PROG 100
say Fresh bread, still warm from the oven!
emote wipes flour from his hands.
nod
~
Because dollar codes are substituted first, plain commands can be personal: say Good day to you, $N. speaks the visitor's actual name. If the host cannot perform the command (a social that does not exist, a door that is not there), the failure message goes to the host and the script simply carries on with the next line.
Messaging Commands
These commands write text to players. The difference between them is who gets to see it. Choosing the right one is most of the craft of making a scene feel right: narration nobody said out loud is mpecho, a private aside is mpechoat, and words spoken in character are a plain say line.
mpecho <text>
Shows the text to everyone in the host's room, including the source. This is pure narration: no name is attached and nothing is spoken. Use it for atmosphere, scenery in motion, and stage direction. The text is word wrapped for you.
GREET_PROG 100
mpecho A cold draft slides through the room, guttering the candles.
~
mpechoat <who> <text>
Shows the text to exactly one person and nobody else. Perfect for private sensations, whispered hints, and anything only one player should know. The short alias mea does the same thing.
mpechoaround <who> <text>
The mirror image: shows the text to everyone in the room except the named person. Use the pair together to describe one event from two points of view. The short alias is mer.
GREET_PROG 100
mpechoat $n A silver charm on the shelf seems to wink at you, $N.
mpechoaround $n $N is briefly bathed in a pale silver light.
~
The player sees the first line; everyone else in the room sees the second. Nobody sees both, so the moment feels personal instead of broadcast.
mpasound <text>
Shows the text in every room exactly one exit away from the host's room, but not in the host's own room. The name is short for adjacent sound. Use it for noises that carry: a bell, a scream, a distant explosion. Players next door see the text; players in the room with the host do not, so pair it with an mpecho for the local version.
GREET_PROG 100
mpasound A deep bell tolls somewhere close by.
mpecho The great bell right beside you nearly deafens you.
~
mpspeak <text>
Makes the host say the text out loud, exactly like a plain say line. It exists so scripts written for CoffeeMUD paste in without editing; in new scripts just write say.
mpchannel <channel> <text>
Sends the text onto one of the mud's chat channels, visible to everyone tuned in anywhere in the world. The first word is the channel name and the rest is the message: mpchannel gossip The east gate has fallen! Use this very sparingly; a script that talks on a global channel is heard by the entire mud every time its trigger fires.
mpllm <text>
Sends the text to every staff member currently online, tagged as script output. Players never see it. Use it to flag rare events a builder would want to know about, such as a player finding a secret you want telemetry on.
mplog <text>
Appends the text to the server log file /log/mudprog, stamped with the time and the host's name. Nothing is shown to anyone in the game. This is your printf: when a script misbehaves, add mplog lines at the steps you care about and read the file afterward.
GREET_PROG 100
mpspeak The ledger never lies, friend.
mpllm The ledger greeter fired its greet block.
mplog ledger greet block ran
~
mpprompt <text>mpconfirm <text>
These ask the triggering player a question. mpprompt prints the text as a question and captures the player's next input line into a variable named prompt_answer stored on that player. mpconfirm does the same but adds yes or no to the question, and stores a clean yes or no into confirm_answer. A later trigger, usually a SPEECH_PROG or a second visit, reads the answer back with the var function or the $<...> form: if var($n confirm_answer) == yes. The alias mpchoose behaves exactly like mpprompt. The question is always shown; the capture works when the trigger was set off by the player's own action, such as speaking or entering a room.
GREET_PROG 100
mpprompt What password do you speak, traveler?
~
GREET_PROG 100
mpconfirm Do you swear to keep the secrets of this guild?
~
mpaccuse <who>
Publicly accuses the target of a crime: the room sees the accusation and the target loses 25 points of Syndicate reputation. A flavorful stick for law-and-order NPCs in the Brinewarrens.
This is the most important command in this chapter, and it is a Rogue extension that CoffeeMUD does not have. mpsleep pauses the script right where it is, waits the given number of seconds, and then carries on with the rest of the lines as if nothing happened. Everything the script knew, who the source is, what the variables hold, survives the pause. The alias mpwait is identical.
Why it matters: without a pause, a five line dramatic scene plays as five messages in the same instant, a wall of text nobody reads. With pauses it becomes theater. Here is a museum curator performing a small cutscene for each visitor:
GREET_PROG 100
mpecho The curator lifts her lantern and beckons you toward a dusty case.
mpsleep 2
say This case holds the first coin ever struck in Aurin.
mpsleep 2
mpecho She unlocks the case; the hinges part with a soft click.
mpsleep 1
say Beautiful, is it not? Mind the glass on your way out.
~
Read it as a script for a stage: line, two second beat, line, two second beat, and so on. Two to four seconds is a good beat; longer than five and players wander off.
Rules for mpsleep:
The shortest pause is one second; a smaller or missing number is treated as one. You may sleep several times in one block, as the curator does. Sleeping inside an if or else branch works fine, and the lines after the endif still run when the pause ends. The one place not to sleep is inside a for or while loop body: the pause ends the loop's remaining passes. If you need a repeating timed beat, write the steps out one after another with sleeps between them, as above.
In CoffeeMUD the only way to get this effect is to chain MPALARM calls, each one carrying the next chunk of the scene. Those scripts still work here, but you should never need to write one again.
mpalarm <seconds> <command>
Schedules one single command line to run after the given number of seconds, and then immediately carries on with the current script. Where mpsleep pauses the whole script, mpalarm splits off just one delayed line and lets the rest run now. The delayed line may be an mp command or a plain game command. The alias mpbeacon is identical.
One subtlety: the dollar codes in the delayed line are filled in when the alarm is set, not when it goes off. If you write mpalarm 10 say Farewell, $N! the name is locked in immediately, so the mob says the right name even if the player has left the room by then.
GREET_PROG 100
mpalarm 2 mpecho A muffled thump echoes from the cellar below.
mpecho The innkeeper cocks his head, listening.
~
The second line prints at once; the thump arrives two seconds later, even though it is written first. Use mpsleep when a scene unfolds in order; use mpalarm when you want a delayed side effect and the script itself should not wait.
Movement Commands
mpgoto <room>
Moves the host itself to the given room. The usual real use is a path: mpgoto /realms/loralei/aurin/rooms/room1 sends the mob to the Aurin town square. You can also write a dollar code naming a living thing to jump to that creature's room. The move is silent; add your own mpecho lines before and after if players should see a departure and an arrival. This example uses the word here, which moves the host to its own room, a harmless way to see the command run:
GREET_PROG 100
mpecho The patrol sergeant checks his post and stays exactly where he is.
mpgoto here
~
mpat <room> <command>
Runs one game command as if the host were standing in another room, then returns it home. The host really does travel there and back in the same instant, so the command affects the far room: its people hear the say, its floor receives the dropped item. The command portion is a plain game command line, dollar codes included.
GREET_PROG 100
mpat here emote inspects the floorboards for loose nails.
~
With a real path this is how a jailer shouts down to the cells without leaving his desk: mpat /realms/sivaine/keep/rooms/cell1 say Quiet down there!
mptransfer <who> [room]
Moves someone or something else to a room. With both arguments it is a teleport: mptransfer $n /realms/loralei/aurin/rooms/room1. With only a target it is a summons, pulling the target into the host's own room. A transferred living thing is shown its new surroundings so it is not staring at nothing. Be gentle using this on players: being yanked across the world with no message is jarring, so narrate it.
GREET_PROG 100
mpmload /domains/examples/npc/mudprog_greeter
mptransfer $b here
mpecho The greeter is yanked through a fold in space and lands here.
mppurge $b
~
mpwalkto <direction...>
Steps the host through the listed exits one at a time, as if it typed go for each: mpwalkto north north east walks two rooms north then one east. Each step is a real move that players see. There is no pathfinding; you supply the turns yourself, and a blocked or missing exit simply stops that step. mptrackto is accepted as an alias and behaves the same way, walking the directions you give it.
Combat Commands
mpkill <who>
Makes the host attack the target, starting a normal fight that plays out by the ordinary combat rules. This is the standard way for a script to turn hostile: a guard who attacks anyone who says the wrong word, an idol that animates when touched.
mphit <who>
Lands exactly one attack on the target right now, entering combat first if needed. Use it inside FIGHT_PROG blocks for an extra flurry, or for a single warning blow that a full mpkill would escalate.
mpstop [who]
Orders a combatant to stop fighting. With no argument the host itself stands down. The classic pattern is a sparring master who starts and ends a bout in one scripted breath:
GREET_PROG 100
mpmload /domains/examples/npc/mudprog_greeter
mpkill $b
mphit $b
mpecho The watchman hurls himself at the intruder!
mpstop $i
mpstop $b
mppurge $b
~
mpdamage <who> <amount> [type]
Deals direct damage to the target, no attack roll, no miss. The optional type is one of blunt, cutting, thrusting, pierce, heat, fire, cold, ice, shock, lightning, or magic; if you leave it off, magic is used. The damage goes through the normal mitigation pipeline, so armor and resistances still matter. This is the command for traps, cursed items, and environmental harm.
mpheal <who> <amount>
Restores that many health points to the target, up to its maximum. If the target cannot be found the host heals itself. Use a positive amount; to hurt someone use mpdamage, which plays fair with armor.
mprejuv [who]
Restores a living thing completely: full health, full spell points, full stamina, all at once. With no argument the host restores itself. Perfect for training dummies and for resetting a boss when everyone flees.
GREET_PROG 100
mpdamage $i 25 blunt
mpecho The practice dummy rocks back under a phantom blow.
mpheal $i 10
mprejuv $i
mpecho Its wounds close, leaving it good as new.
~
mpreset [who]
For a living target this is the same full restore as mprejuv. Pointed at a non-living object such as a chest in the room, it runs that object's own reset instead, restocking it the way a server reset would.
GREET_PROG 100
mpreset $i
mpecho The dummy squares itself, restored for the next drill.
~
mpcast <spell> [target]
Makes the host cast a spell by name, optionally at a target: mpcast fireball $n. The host must actually be able to cast the spell, exactly as a player must; a mob with no magic fails quietly. mpcastext is an accepted alias.
GREET_PROG 100
mpcast light
mpecho The hermit sketches a quick sigil in the air.
~
mpslay <who>
Kills the target outright, on the spot, no fight and no saving throw. A real death with a real corpse. This is dramatic machinery for executions and divine judgment; never point it at players in a live area without a very good reason.
GREET_PROG 100
mpmload /domains/examples/npc/mudprog_greeter
mpecho A bolt of judgment strikes the greeter where he stands!
mpslay $b
~
mpflee
Makes the host turn and flee through an exit, exactly as a panicking player would. A bare mpflee inside a HITPRCNT_PROG block is the whole recipe for a coward: when health drops past the threshold, the mob runs.
mpforce <who> <command>
Makes another living thing perform a game command, as if it had typed the line itself. It works on mobs and on players. Forcing players is a sharp tool: fine for harmless theater, hostile if abused.
GREET_PROG 100
mpechoat $n A strange compulsion tugs at the corners of your mouth.
mpforce $n smile
~
mpbehave <flag> [value]mpunbehave <flag>
Switches a standing behavior on or off. Two flags are wired to real systems: mpbehave aggressive makes the host attack players on sight and mpunbehave aggressive calms it, and mpbehave wander with an optional speed number starts it roaming. Any other word is remembered as a named flag on the host, which your own scripts can test later with the isbehave function; this makes mpbehave a simple way to give a mob switchable moods.
GREET_PROG 100
mpbehave gruff
mpecho The doorman squares his shoulders and glowers at everyone.
mpunbehave gruff
~
mppossess <player> <mob>
Puts a player at the controls of a mob through the possession system: the player sees through the mob's eyes and commands its body until the possession ends. Example use: mppossess $n ratling as the payoff of a fortune teller's trance. Coordinate with an admin before building this into anything public.
Creating, Destroying, and Moving Things
The load commands bring new objects into the world from their file paths. After any of them succeeds, the fresh object is remembered as $b (and $B for its display name), so the very next lines can move it, junk it, or talk about it. Each new load replaces the previous $b.
mpmload <path>
Loads a mob from its file and places it in the host's room. The new mob is flagged to despawn on the next area reset, so scripted summons clean themselves up instead of littering the world forever.
GREET_PROG 100
mpmload /domains/examples/npc/mudprog_greeter
mpecho A door bangs open and $B hurries in, full of apologies.
mppurge $b
~
mpoload <path>
Loads an item and puts it in the host's own inventory, ready to be given, sold, or worn. Combine with a plain give line for the classic reward moment.
mpoloadroom <path>
Loads an item and drops it straight into the host's room.
GREET_PROG 100
mpoloadroom /obj/meal
mpecho Out of nowhere, $B drops onto the table.
mpjunk $b
~
mploadquestobj <path>
Loads an item and places it directly into the inventory of the source, the player who set off the trigger. This is the quest handout command: no give, no chance to drop it on the floor, it simply arrives in their pack.
GREET_PROG 100
mploadquestobj /obj/torch
mpechoat $n The quartermaster presses something into your hands.
~
mpoloadshop <path>
For vendor hosts only: loads an item into the vendor's storage room, so it appears in the shop's stock rather than in anyone's hands. On a host that is not a vendor with a storage room, nothing happens. mpmloadshop is an accepted alias.
GREET_PROG 100
mpoloadshop /obj/meal
mpecho The merchant scribbles a note to restock the pantry.
~
mpjunk <item>
Destroys one item, in the room or in the host's inventory. It refuses to touch living things, so a mistyped name cannot vaporize a mob.
mppurge <who>
Destroys a mob or an item. It will never destroy a player. This is the cleanup command for anything your script loaded: mppurge $b right after the summoned creature has served its purpose.
mpput <item> <container>
Moves an item into a container. The chest fills itself, the letter slides into the satchel:
GREET_PROG 100
mpoloadroom /obj/meal
mpoload /obj/container
mpput meal $b
mpecho The cook sweeps the leftovers into a battered container.
~
Note the order: the meal is loaded first, then the container, so that $b means the container on the mpput line and the meal is found by its name.
mphide [who]mpunhide [who]
Turns invisibility on or off for the target, or for the host itself with no argument. An invisible mob still hears its triggers, which makes this the tool for ghosts, watchers, and dramatic entrances.
GREET_PROG 100
mphide $i
mpecho The air shimmers where the magistrate stood a moment ago.
mpsleep 2
mpunhide $i
mpecho The magistrate fades back into view.
~
mprload <room>
Runs a room's area reset immediately, the same refresh the world performs on its own schedule: mprload /realms/loralei/aurin/rooms/room1. Use it when a scripted event should restock or restore a place on the spot instead of waiting for the timer.
mplink <direction> <path>mpunlink <direction>
Adds or removes an exit on the host's room while the room is loaded. The new exit lasts until the room reloads, which makes the pair ideal for secret passages that a script opens and closes:
GREET_PROG 100
mplink crevice /realms/loralei/aurin/rooms/room1
mpecho With a grinding of stone, a crevice opens in the wall!
mpunlink crevice
mpecho The crevice grinds shut again.
~
A real secret door would leave the exit open and close it from a later trigger or an mpalarm.
The host opens, closes, locks, or unlocks a door or container, using the same verbs a player would, with the same rules: locked doors need the key, and a missing door fails quietly.
GREET_PROG 100
mpopen north
mpclose north
mplock north
mpunlock north
mpecho The porter rattles the north door, checking its hinges.
~
mpm2i2m
A CoffeeMUD command for morphing mobs into items and back. Rogue has no morph system, so the command is accepted and ignored; a pasted CoffeeMUD script containing mpm2i2m $n runs without error and that line does nothing.
Changing a Character
These commands change stats, wealth, standing, and status effects. They are usually pointed at $n, the player who earned something, but they work on mobs too, which several examples below use.
mpset <who> <field> <value>
The general-purpose setter. A handful of field names are wired to the real systems: level, str, agi, con, int, wis, cha, hp, sp, name, short, and long. So mpset $b level 20 makes the summoned mob level 20 and mpset $i short A Nervous Clerk renames the host on the spot. Any other field name is stored as a custom property on the target, invisible to players but readable by scripts and code; whole numbers are stored as numbers. mpsetinternal is an accepted alias.
GREET_PROG 100
mpset $i mood theatrical
mpecho The playwright strikes a pose, feeling suddenly theatrical.
~
mpexp <who> <amount>
Grants experience points; a negative amount takes them away. Quest rewards, lesson payoffs, and cursed bargains. The alias mprpexp behaves identically.
GREET_PROG 100
mpexp $n 10
mpechoat $n Watching the old fencer, you feel a little more experienced.
~
mpmoney <who> [type] <amount>
Gives currency to the target, or takes it with a negative amount. With two arguments the type is gold: mpmoney $n 50 hands over fifty gold. Name a type to use another coin: mpmoney $n silver 200.
GREET_PROG 100
mpmoney $i 5
mpecho The beggar tucks a few coins deeper into his rags.
~
mptitle <who> <title>
Awards a title the target can wear with their name, such as the Ratcatcher or Friend of the Grove. Titles are honors; hand them out for real accomplishments.
GREET_PROG 100
mptitle $i the Well Documented
mpecho The archivist smiles, newly titled and insufferably proud.
~
mpfaction <who> <faction> <amount>
Adjusts the target's standing with a Brinewarrens faction: syndicate, tideborn, or faceless. Positive numbers raise reputation, negative lower it. This is how scripted deeds ripple into the faction system.
GREET_PROG 100
mpfaction $i syndicate 5
mpecho The fence nods; the Syndicate will hear of this kindness.
~
mptrains <who> <skill> [points]
Credits skill points toward one of the target's skills, one hundred of them if you give no number. mppracs is an accepted alias. A drillmaster who actually teaches:
GREET_PROG 100
mptrains $i sword 10
mpecho The drillmaster runs through a practice form, sharpening his craft.
~
Applies or removes a status condition by its id: rooted, stunned, poisoned, blinded, or any id the condition system knows. The duration defaults to sixty seconds. Conditions are the real thing, the same effects skills apply, so a scripted trap that roots a player interacts correctly with everything else in the game.
GREET_PROG 100
mpaffect $i dazzled 15
mpecho Spots dance before the apprentice's eyes.
mpunaffect $i dazzled
mpecho The apprentice blinks the spots away.
~
The full-control version of mpaffect, and a Rogue extension. You choose the condition's type, most usefully buff or debuff, its duration, and optionally a magnitude and a percent strength for effects that scale. Use mpaffect for a quick debuff; use mpcondition when you are crafting a proper blessing.
GREET_PROG 100
mpcondition $i inspired buff 30 5
mpecho A surge of inspiration straightens the poet's back.
~
mptattoo <who> <text>
Marks the target with a described tattoo, stored on their character. A story scar players carry with them. mpacctattoo is an accepted alias.
GREET_PROG 100
mptattoo $i a coiled serpent over the left wrist
mpecho Fresh ink glistens on the sailor's wrist.
~
mpachieve <who> <id>
Flags an achievement on the target and tells them so with an unlocked announcement. The id is a single word of your choosing; use the same id everywhere the same feat can be earned.
GREET_PROG 100
mpachieve $n guide_reader
~
mpplayerclass <who> <class>
Changes the target's class, a drastic and permanent act. Meant for carefully built story moments and staff tools, never a casual trigger. The example dubs a summoned mob, which is harmless:
GREET_PROG 100
mpmload /domains/examples/npc/mudprog_greeter
mpplayerclass $b fighter
mpecho The greeter squares up like a soldier on parade.
mppurge $b
~
Sets the target's clan, and stores named pieces of clan data on them, such as a rank or an oath date. These are bookkeeping commands for clan storylines.
GREET_PROG 100
mpsetclan $i Order of the Quill
mpsetclandata $i rank scribe
mpecho A quill-and-ink sigil gleams on the clerk's breast.
~
Quest Commands
These commands drive the quest system. Every one of them takes the quest's id, the short name it is registered under with the quest daemon; a quest id that does not exist safely does nothing, which is why the examples below run harmlessly anywhere. The scripted mob itself acts as the quest giver.
mpstartquest <who> <quest id>
Puts the target on a quest, with the host as the giver. The natural home is a SPEECH_PROG, so saying the right word to the right mob begins the story:
GREET_PROG 100
mpstartquest $n cellar_rats
say Rats have the run of my cellar. Clear them out and I will pay well.
~
mpquestwin <who> <quest id>
Completes the quest for the target, delivering whatever rewards the quest defines. Fire it when the deed is verifiably done.
mpquestpoints <who> <amount>
Awards quest points directly, the mud's long-term score for questing, independent of any one quest's rewards.
GREET_PROG 100
mpquestwin $n cellar_rats
mpquestpoints $n 5
say The cellar is quiet at last. You have my thanks, $N.
~
mpendquest <who> <quest id>
Ends the quest either way: if the target has met the goals it completes like mpquestwin, and if not the quest is dropped from their log unfinished. Use it for deadlines and betrayals.
mpstepquest <who> <kill | visit | talk>
Nudges the target's quest progress by crediting one quest event: talk counts as having talked to the host, visit as having visited the host's room, and kill as having slain the host. It lets a script stand in for the real deed, for instance crediting the talk objective when a player instead bribes the witness.
mpqset <who> <quest id> <key> <value>
Writes one named value into the target's active copy of a quest. This is surgical bookkeeping for multi-stage quests; it only works while the target actually has the quest in progress.
GREET_PROG 100
mpstepquest $n talk
mpqset $n cellar_rats note spoke_to_innkeeper
mpendquest $n cellar_rats
say Come back if the scratching starts again.
~
For handing the player a physical quest item, see mploadquestobj in the loading section above.
Variables and Script Control
Scripts can remember things. A variable is a named value stored on an object; store it on a player and it follows that player, store it on the host and it belongs to the mob or room. You read variables back in text with $<object name> and in conditions with the var function. The full story lives in help mudprog-variables; here are the commands that write them.
mpsetvar <object> <name> <value>
Stores a named value on any object. The value is everything after the name, dollar codes substituted, so mpsetvar $n sworn_to $I records the host's name on the player. Values written to players are saved with the character and survive logging out. mpsavevar is an accepted alias, kept because CoffeeMUD distinguishes the two.
GREET_PROG 100
mpsetvar $i customers 1
say You are customer number $<$i customers> today, welcome!
~
The classic use is a memory: greet a stranger one way and an old friend another. Note the shared last line, which runs in both cases:
GREET_PROG 100
if var($n met_seer) == yes
say Back again? The stars said you would return.
else
mpsavevar $n met_seer yes
say We have not met before. Let me look at your palm.
endif
say Every path ends at my table eventually.
~
mpgset <name> <value>
Stores a mud-wide global value in the script daemon itself, shared by every script everywhere and saved across reboots. Use globals for world state bigger than any one mob, such as which army holds a gate.
GREET_PROG 100
mpgset town_alert high
mpecho The guard captain chalks a warning mark on the gatepost.
~
The ten temporary slots $0 through $9 are scratch paper that lasts only for the current trigger run. mpargset writes a value into a slot directly, and mploadvar copies a stored variable into a slot so you can use it repeatedly without the longer $<...> form.
GREET_PROG 100
mpargset 3 seventeen
mpsetvar $i tally seventeen
mploadvar $i tally 4
say The count stands at $3, or as the ledger has it, $4.
~
mpcallfunc <name> [text]
Runs a FUNCTION_PROG block by name. A FUNCTION_PROG is a block you write in the same script that never fires on its own; it is a subroutine, a chunk of lines you can call from many places instead of copying them. The text after the name, dollar codes substituted, is passed along and appears inside the function block as $g.
GREET_PROG 100
mpcallfunc fanfare $N
say Step forward and be recognized.
~
FUNCTION_PROG fanfare
mpecho A herald raises a battered trumpet and blows a note for $G.
~
Anything that should happen the same way from three different triggers belongs in one FUNCTION_PROG called three times.
mpscript <line>
Runs one line as a fresh script on the host, right now, with dollar codes already substituted. Mostly useful when the line itself was assembled from variables; day to day you will rarely need it.
GREET_PROG 100
mpscript mpecho A rune flares as the scripted ward inspects the newcomer.
~
mpunloadscript
Deletes the host's entire script, all blocks, permanently. The script removes itself from the world. This is the one-shot ender: a mob whose whole purpose is a single scene can finish the scene and go quiet forever.
GREET_PROG 100
say My work here is done. Forget you ever saw me.
mpunloadscript
~
mpdisable <trigger>mpenable <trigger>
Records a trigger type as switched off on the host, and switches it back on. Name the trigger as it appears in headers, such as RAND_PROG. A town crier who falls silent during a scene and resumes after:
GREET_PROG 100
mpdisable RAND_PROG
mpecho The jester stops mid-caper, suddenly all business.
mpenable RAND_PROG
~
mpnotrigger
Marks the host to suppress its own triggered reactions, bookkeeping the engine keeps alongside the disable ledger. Use it when a script's actions would otherwise set off the host's own triggers in a loop.
GREET_PROG 100
mpnotrigger
mpecho The clockwork sentry ticks once and holds perfectly still.
~
Putting It All Together
Here is one small complete character built almost entirely from commands in this chapter: a bridge toll keeper who performs a timed scene, conjures a parting gift, and sends the traveler off with a blessing. Notice the rhythm: speech, pause, gesture, pause, payoff.
GREET_PROG 100
say Ho there, $N! The bridge toll is one good story, payable now.
mpsleep 2
emote leans on his pike, waiting expectantly.
mpsleep 2
say Ah, never mind. Your face tells a story all by itself. Go on through.
mpoloadroom /obj/meal
mpecho The toll keeper sets out $B for the road ahead.
mpaffect $n well_fed 60
mpechoat $n You feel ready for the road.
~
Walk through it line by line. The greeting is a plain say using $N for the visitor's name. Two mpsleep beats turn three messages into a scene with timing. mpoloadroom conjures the gift and $B names it in the narration without hard-coding its description. mpaffect applies a real condition as the blessing, and mpechoat closes on a private note only the traveler sees. Ten lines, six commands, one memorable NPC.
Attach a script and fire a block on demand with mudprog <target> test GREET_PROG; you are the source, so $n means you. To run loose lines without attaching anything, use scripttest, separating lines with semicolons. One practical warning: some clients mangle the dollar sign when you paste, so if your dollar codes come out wrong, use the mudprog <target> edit editor or scripttest runfile <file> instead of pasting inline.
While debugging, remember mplog writes to /log/mudprog, and a script that runs away past its step budget is stopped safely and noted in /log/script_runaway. Nothing you write in a script can crash its host; the worst a mistake can do is nothing at all, which is exactly how a scripting system for builders should fail.
This chapter is the complete reference for MUDProg functions: every question a script can ask about the game world, from whether the person in front of it is a player to what the weather is doing outside. If you have never written a script or a line of code before, read the first few sections in order - they teach the only grammar you need. After that, the reference reads like a menu. Find the question you want to ask, copy the example under it, and change the words.
A function is a question with a fixed name. You write the name, you put the thing you are asking about between parentheses, and the game hands back an answer. The answer is always a number or a piece of text. That is the whole idea. level($n) asks what level the player who set off the script is, and the answer comes back as a number such as 12. weather() asks what the sky is doing, and the answer comes back as a word such as rain. Functions never change anything - they only look. Changing the world is the job of commands such as mpecho and mpoload, which have their own chapter.
Everything below was written for someone starting from zero, and every example is a complete script you can paste onto a test mob and fire yourself. Nothing here is theoretical - each one has been run against the live engine.
The Two Ways to Use a Function
There are exactly two places a function can appear, and everything in this chapter is one or the other.
The first way is inside a condition. A condition is the part after the word if. The script runs the lines between if and endif only when the function's answer counts as yes. For a function that answers 1 or 0, that reads exactly like English: if this person is a player, do the following.
The second way is inside text, wrapped in $%...%. Wherever a script writes text - a say, an mpecho, an mpechoat - you can splice a function between $% and % and the engine replaces it with the answer before anyone sees the line. This is called substitution, and it is how a mob speaks numbers and names it could not know in advance.
Here is one script that does both. The if uses ispc($n) as a yes-or-no test, and the last line splices the answer of numpcsroom() into the middle of a sentence.
GREET_PROG 100
if ispc($n)
mpechoat $n Welcome, $N. A real person at last!
endif
mpechoat $n There are $%numpcsroom()% players standing here.
~
Read it aloud. When a player walks in, the trigger fires with that player as $n. The condition asks: is $n a player? Yes, so the welcome line runs. Then the second mpechoat line is scanned for dollar codes before printing, the engine sees $%numpcsroom()%, asks the room how many players are standing in it, and pastes the number into the sentence. The player reads: There are 2 players standing here.
A useful habit: use conditions when you want the script to DO something different depending on the answer, and use substitution when you just want to SAY the answer.
How Arguments Work
The things inside the parentheses are called arguments - they tell the function what to look at. Three rules cover everything.
Rule one: arguments are separated by spaces, never commas. Write hasnum($i meal 2), not hasnum($i, meal, 2). A comma will quietly break the lookup.
Rule two: when an argument names a person or thing, you can use a dollar code or a plain name. The codes you will use constantly: $n is the source, the one who set off the trigger, usually the player. $i is the scripted object itself, the mob or item or room carrying the script. $t is the target of the event, $b is the last thing the script loaded with mpoload or mpmload, and $r is a random player in the room. A plain word such as guard is searched for by name in the script's room, then in its inventory. The variables chapter covers the full code table.
Rule three: if you leave out the first argument entirely, the function looks at the scripted object itself. level() is the host's own level. This is handy on room and item scripts where there is no other obvious person to ask about.
GREET_PROG 100
mpecho With no name given the question falls on the host itself.
mpecho The host is level $%level()% with $%hp()% health.
~
Function names ignore capitals - ISPC($n), ispc($n) and IsPc($n) are the same question. One warning that will save you an evening of confusion: a misspelled function name does not produce an error. The engine shrugs and answers 0, which reads as no. If a condition never seems to fire, check the spelling of the function first.
A second warning of the same flavor: in a condition, always write the parentheses, even when a function takes no arguments. if isnight() asks the game whether it is night. if isnight without parentheses is treated as plain text, and plain non-empty text counts as yes, so that line would be true at high noon. Inside $%...% the parentheses are optional, but writing them everywhere is the habit that never bites.
Arguments that are text or numbers - the ones taken by number, isodd, math, strin, islike and friends - are scanned for dollar codes first, so you can feed one function's answer to another: if isodd($%level($n)%) works. Functions that expect a person or item do not do this with their name arguments; if you need to stage a computed value, park it in a numbered slot with mpargset and pass $0 - there is a worked example in the strings section.
Comparing Results
Many functions answer with a number or a word rather than a plain yes or no. To turn such an answer into a test, compare it against a value using one of these operators:
== or = - equal. Text comparisons ignore capitals.
!= or <> - not equal.
> < >= <= - greater, less, at least, at most.
.in. - the left value occurs somewhere in the right text.
You may place the comparison inside the closing parenthesis or after it - both forms mean the same thing, and both appear throughout Rogue scripts. CoffeeMUD's own guide writes them inside, so scripts copied from there run unchanged.
GREET_PROG 100
if level($n) > 0
mpechoat $n The comparison outside the parentheses agrees.
endif
if level($n > 0)
mpechoat $n The comparison inside the parentheses agrees too.
endif
~
When both sides look like whole numbers the comparison is numeric, so 9 is less than 40. Otherwise it is a text comparison, and for equality the capitals do not matter: class($n) == Mage matches a mage. A compared value can be more than one word, with or without quotes around it - quotes are simply stripped.
GREET_PROG 100
if name($n) == 'Utter Nobody'
mpechoat $n Ah, the famous Utter Nobody honors us.
else
mpechoat $n Quote check: you are clearly not the famous Utter Nobody.
endif
~
The .in. operator asks whether the left side appears inside the right side, which makes a tidy one-line version of a long chain of equality checks:
GREET_PROG 100
if season() .in. spring summer autumn winter
mpecho The season sits in the expected list.
endif
~
A function used with no comparison at all is a bare test: the answer counts as yes when it is a number other than zero or any non-empty text. The words 0, false and no count as no. This is why if clan($n) works as a test for having any clan at all - the clan name is text, and text counts as yes.
Conditions join with and, or, and not and or not (also written andnot and ornot; a bare not between two tests means and-not). To flip a single test, put ! directly in front of it. One trap: never START a condition with the word not, because the engine reads it as part of a function name and the test silently fails. Use ! for the first test instead.
GREET_PROG 100
if !isnpc($n)
mpechoat $n Not-check: you are no scripted creature.
endif
if ispc($n) and not isnpc($n)
mpechoat $n Calm check: a player, and certainly not a mob.
endif
~
Reading This Reference
Each entry below shows the function with the arguments it expects, then what it answers. In the syntax, <who> stands for any way of naming a living thing: $n, $t, $i, $b, or a plain name in the room. <item> works the same for objects. Answers described as 1 or 0 mean yes or no. Where an entry says it defaults to the host, leaving the argument out asks about the scripted object itself.
Every example block is a complete script. To try one, target a throwaway test mob, attach the script with mudprog <target> edit (or build it line by line with mudprog <target> append), then set it off with mudprog <target> test GREET_PROG - the test command fires the named trigger with you as the source, so $n is you. Examples that check things no fresh character has - a title, a clan, poison in the blood - print BOTH ways on purpose, with a shared opening phrase, so you always see output and always learn which branch ran.
Actor Checks: Who Is Standing Before You
These functions size up a living thing - almost always $n, the player who triggered the script. They are the backbone of every doorman, gatekeeper and quest giver you will ever write.
ispc(<who>) - 1 if <who> is a connected player, 0
otherwise. Defaults to the host.
isnpc(<who>) - 1 if <who> is a creature run by the game
rather than a person.
isalive(<who>) - 1 if <who> is living and not dead. isfight(<who>) - 1 if <who> is in combat right now. iscontent(<who>) - 1 if <who> is NOT in combat - the calm
mirror of isfight.
The first example every builder writes: prove who is who. The source is a player and the host is a mob, so both tests pass and the line prints.
GREET_PROG 100
if ispc($n) and isnpc($i)
mpecho A living player faces a scripted creature.
endif
~
Life and battle state, printed both ways so the script always answers:
GREET_PROG 100
if isalive($i)
mpecho Life check: the creature lives and breathes.
endif
if isfight($i) or isfight($n)
mpecho War check: someone here is locked in battle.
else
mpecho War check: all is calm.
endif
if iscontent($i)
mpecho Content check: no battle troubles this creature.
else
mpecho Content check: this creature has other things on its mind.
endif
~
isimmort(<who>) - 1 if <who> is a member of staff, a creator
or administrator.
ischarmed(<who>) - 1 if <who> is under a charmed condition. isfollow(<who>) - 1 if <who> is following a leader. isservant(<who>) - 1 if <who> is a companion creature such as
a squire or familiar.
isgroup(<who>) - 1 if <who> belongs to an adventuring
group.
Five bond-and-status checks in one script. Note the or chain: one line covers four different kinds of attachment.
GREET_PROG 100
if isimmort($n)
mpechoat $n Staff check: you are one of the immortals.
else
mpechoat $n Staff check: you are a mortal soul.
endif
if ischarmed($i) or isfollow($i) or isservant($i) or isgroup($i)
mpecho Bond check: this creature is bound to someone.
else
mpecho Bond check: this creature stands alone.
endif
~
ispkill(<who>) - 1 if the ROOM <who> stands in permits
player killing. It judges the ground, not
the person.
GREET_PROG 100
if ispkill($n)
mpechoat $n Danger check: player killing is allowed on this ground.
else
mpechoat $n Danger check: this is safe ground.
endif
~
isspeaking() - 1 if this trigger carried spoken text -
useful in speech triggers to confirm there
are words in $g to react to.
A speech trigger reacting to whatever was said. The spoken line arrives in $g:
SPEECH_PROG all
if isspeaking()
mpechoat $n Echo check: I heard you say $g just now.
endif
~
cansee(<who> <target>) - 1 unless <target> is invisible. The second
argument defaults to the trigger source.
Honest note: only the target's
invisibility is judged - the first
argument is accepted for CoffeeMUD
compatibility.
canhear(<who>) - 1 unless <who> is deafened.
GREET_PROG 100
if cansee($i $n)
mpecho Sight check: the creature marks your arrival.
else
mpecho Sight check: the creature peers about blindly.
endif
if canhear($n)
mpechoat $n Sound check: you hear a dry chuckle.
else
mpechoat $n Sound check: silence presses on your ears.
endif
~
sex(<who>) - the word male, female or neuter. name(<who>) - the proper name, such as Aldric. level(<who>) - the level as a number. class(<who>) - the guild name in lower case, such as mage
or warrior. baseclass(<who>) is an
accepted alias for the same answer.
race(<who>) - the race, such as human. racecat(<who>) is
an alias.
deity(<who>) - the name of the god <who> worships, or
empty text for none.
The identity functions all at once, spliced into prose with $%...%:
GREET_PROG 100
mpechoat $n You are $%name($n)%, a $%race($n)% $%class($n)%.
mpechoat $n Your level is $%level($n)% by the ledger.
mpechoat $n The records list your gender as $%sex($n)%.
mpechoat $n Your god is $%deity($n)% and your lineage $%racecat($n)%.
~
And one of them used as a test. Both branches open with the same phrase, so the script speaks either way:
GREET_PROG 100
if class($n == mage)
mpechoat $n Guild check: ah, a fellow student of the arcane.
else
mpechoat $n Guild check: your art is $%class($n)%, I see.
endif
mpechoat $n The old rolls also name it $%baseclass($n)%.
~
position(<who>) - the posture word: standing, sitting,
kneeling, lying, flying or swimming.
GREET_PROG 100
if position($n == standing)
mpechoat $n Posture check: you are on your feet.
else
mpechoat $n Posture check: you are $%position($n)% at the moment.
endif
~
hitprcnt(<who>) - health as a percentage from 0 to 100. The
classic use is a boss script that changes
tactics below half health.
GREET_PROG 100
if hitprcnt($n < 50)
mpechoat $n Health check: you look badly hurt!
else
mpechoat $n Health check: hale enough at $%hitprcnt($n)% percent.
endif
~
exp(<who>) - total experience points. questpoints(<who>) - quest points earned. goldamt(<who>) - gold carried if <who> is living; if it is
an item, its coin value.
currency(<who>) - the word gold - Rogue trades in a single
coin, so this always answers gold. It
exists so CoffeeMUD scripts run unchanged.
GREET_PROG 100
mpechoat $n You carry $%goldamt($n)% gold coins today.
mpechoat $n You hold $%questpoints($n)% quest points.
mpechoat $n You have earned $%exp($n)% experience in your life.
if currency($n == gold)
mpechoat $n The realm trades in gold, as always.
endif
~
stat(<who> <stat>) - the rated level of a stat: str, con, int,
wis, cha, or dex and agi which both read
agility. Full words like strength work
too.
gstat(<who> <name>) - as stat for a recognised stat name; for
anything else it reads that property off
the object, which makes it a handy
peephole for values set with mpset.
GREET_PROG 100
if stat($n str >= 10)
mpechoat $n Muscle check: strength $%stat($n str)% will serve you well.
else
mpechoat $n Muscle check: strength $%stat($n str)% could use some work.
endif
mpechoat $n The gstat form reads it too: $%gstat($n str)%.
~
isable(<who> <skill>) - 1 if <who> has any training in the named
skill. Skill names with spaces are fine.
expertise(<who> <skill>) - the skill rank as a number, but only for
one-word skill names. For multi-word names
use skill(), listed under Rogue
extensions.
GREET_PROG 100
if isable($n magic attack)
mpechoat $n Skill check: you know something of magic attack.
else
mpechoat $n Skill check: magic attack is a mystery to you.
endif
mpechoat $n Your dodge rank stands at $%expertise($n dodge)%.
~
affected(<who> <condition>) - 1 if the named condition sits on <who> -
poisoned, rooted, stunned and so on. With
no condition given it instead answers the
NAME of the first active condition, or
empty text for none.
GREET_PROG 100
if affected($n poisoned)
mpechoat $n Blood check: venom courses through you!
else
mpechoat $n Blood check: your blood runs clean.
endif
mpechoat $n Aura reading: $%affected($n)%.
~
isgood(<who>) - 1 if the alignment property on <who> is
good.
isevil(<who>) - 1 if it is evil. isneutral(<who>) - 1 if it is neutral or not set at all. Plain honesty for Rogue builders: alignment is not a live system on this mud. Nothing sets that property in normal play, so on players isgood and isevil answer 0 and isneutral answers 1. They exist for CoffeeMUD parity, and they do read the property if your own script sets one, which is what the example does:
GREET_PROG 100
mpset $i alignment good
if isgood($i)
mpecho The creature radiates a kindly light.
endif
mpset $i alignment 0
if isevil($n)
mpechoat $n Shadow check: darkness clings to you.
else
mpechoat $n Shadow check: no darkness upon you.
endif
if isneutral($n)
mpechoat $n Soul check: you walk the middle road.
else
mpechoat $n Soul check: you have chosen a side.
endif
~
mood(<who>) - the mood property - free text your own
scripts store with mpset, empty until
then.
isbehave(<who> <flag>) - 1 if the named behaviour flag was switched
on with mpbehave.
isname(<who> <word>) - 1 if <word> matches one of <who>'s names
or ids. Use a plain word here, not a
dollar code.
These three shine as script-to-script memory - one trigger sets, another asks:
GREET_PROG 100
mpset $i mood cheerful
mpbehave gentle
if mood($i == cheerful) and isbehave($i gentle)
mpecho The creature hums, clearly in a gentle and cheerful humor.
endif
mpunbehave gentle
if isname($n dragon)
mpechoat $n Name check: you are some manner of dragon!
else
mpechoat $n Name check: you are no dragon.
endif
~
ipaddress(<who>) - the network address <who> is connected
from. Treat it as staff-only information:
log it or send it to staff with mpllm, do
not echo it to players.
GREET_PROG 100
mpllm Connection note: $N arrives from address $%ipaddress($n)%.
mpechoat $n The doorkeeper quietly notes where you hail from.
~
Items and Inventory
These functions look at things rather than people: what someone carries, what they wear, and what state a container is in. Several examples below conjure a prop with mpoload first - that loads a fresh item into the host's pack and points $b at it, which keeps the example self-contained - and tidy scripts throw the prop away afterwards with mpjunk $b.
has(<who> <name>) - 1 if <who> carries an item matching
<name>.
GREET_PROG 100
mpoload /obj/armor
if has($i armor)
mpecho Pack check: the creature carries a piece of armor.
endif
mpjunk $b
~
hasnum(<who> <name> <count>) - 1 if <who> carries at least <count> items
matching <name>.
itemcount(<who>) - how many items <who> carries.
numitemsmob(<who>) is the
CoffeeMUD-flavored alias.
GREET_PROG 100
mpoload /obj/meal
mpoload /obj/meal
if hasnum($i meal 2)
mpecho Count check: at least two meals stowed away.
endif
mpecho All told the creature holds $%itemcount($i)% items.
mpecho The alias numitemsmob agrees: $%numitemsmob($i)%.
~
worn(<who> <name>) - 1 if <who> has the named item and is
actually wearing it, not just carrying it.
wornon(<who> <limb>) - 1 if any worn piece covers the named body
part, such as torso or head.
Notice the middle line: wear armor is not an mp command, so the host simply performs it as a game command, exactly as a player would type it. Functions and plain commands mix freely:
GREET_PROG 100
mpoload /obj/armor
wear armor
if worn($i armor)
mpecho Dress check: the creature has strapped on its armor.
else
mpecho Dress check: the armor hangs unworn.
endif
mpecho Torso coverage reports $%wornon($i torso)%.
~
objtype(<item>) - the word armor, weapon, container, or
item.
value(<item>) - the item's coin value.
GREET_PROG 100
mpoload /obj/armor
mpechoat $n Appraisal: the $B is $%objtype($b)% by type.
mpechoat $n It would fetch about $%value($b)% coin.
mpjunk $b
~
isopen(<item>) - 1 if the container or door is open. islocked(<item>) - 1 if it is locked.
GREET_PROG 100
mpoload /obj/container
if isopen($b)
mpecho Lid check: the container stands open.
else
mpecho Lid check: the container is shut.
endif
if islocked($b)
mpecho Lock check: sealed tight.
else
mpecho Lock check: no lock bars the way.
endif
mpjunk $b
~
incontainer(<item> <holder>) - 1 if <item> sits directly inside <holder>.
A creature counts as the holder of its own
pack, which the example leans on.
GREET_PROG 100
mpoload /obj/meal
if incontainer($b $i)
mpecho Nesting check: the meal rides in the creature's pack.
endif
mpjunk $b
~
mobitem(<who> <n>) - the name of the n-th carried item,
counting from 0, or empty text past the
end.
GREET_PROG 100
mpoload /obj/meal
mpecho Slot zero of the pack holds $%mobitem($i 0)%.
mpjunk $b
~
shophas(<vendor> <name>) - 1 if the vendor's storeroom stocks a
matching item.
shopitem(<vendor> <n>) - the name of the n-th stocked item,
counting from 0.
numitemsshop(<vendor>) - how many items the storeroom holds. All
three answer 0 or empty on anything that
is not a vendor, which is what the test
mob shows.
GREET_PROG 100
if shophas($i bread)
mpecho Stock check: fresh bread is for sale.
else
mpecho Stock check: no bread here today.
mpecho The back room holds $%numitemsshop($i)% wares in all.
mpecho First listed ware: $%shopitem($i 0)%.
endif
~
Rooms and Areas
These functions take the room's pulse: who is here, what is lying about, and where here actually is. The counting functions take no arguments and always mean the room the scripted object is standing in.
numpcsroom() - how many players are in the room. nummobsroom() - how many game-run creatures are in the
room. nummobs() is an alias.
numitemsroom() - how many loose items lie in the room. numraces() - how many different races the livings here
span. numracesinarea() is an alias and
currently also counts the room.
GREET_PROG 100
mpecho Standing here: $%numpcsroom()% players, $%nummobsroom()% creatures.
mpecho Loose on the floor lie $%numitemsroom()% items.
if numraces(> 1)
mpecho A mixed crowd spanning $%numraces()% races.
else
mpecho Everyone present is of a single race.
endif
~
roompc(<n>) - the name of the n-th player in the room,
counting from 0, or empty text past the
end.
roommob(<n>) - the same for game-run creatures. roomitem(<n>) - the same for loose items.
GREET_PROG 100
mpecho The first player here is $%roompc(0)%.
mpecho The first creature here is $%roommob(0)%.
mpoloadroom /obj/torch
mpecho Among the floor clutter sits $%roomitem(0)%.
mpjunk $b
~
numpcsarea() - how many players are anywhere in this
room's area.
areapc(<n>) - the name of the n-th player in the area,
counting from 0.
nummobsinarea() - creatures in the area - in the current
engine this counts the host's room.
GREET_PROG 100
if numpcsarea(> 0)
mpecho Area watch: $%numpcsarea()% adventurers walk this area.
mpecho First among them is $%areapc(0)%.
mpecho Creatures about: $%nummobsinarea()%.
endif
~
ishere(<name>) - 1 if something matching the plain <name>
is present in the script host's room. Use
a real name, not a dollar code.
inroom(<who> <room>) - 1 if <who> stands in the given room.
<room> is the room's file path WITHOUT the
.c ending, or the room's exact title.
GREET_PROG 100
mpoloadroom /obj/container
if ishere(container)
mpecho Floor check: a container sits right here.
endif
mpjunk $b
if inroom($n /realms/loralei/aurin/rooms/room1)
mpechoat $n Map check: you stand at Aurin's center.
else
mpechoat $n Map check: you are somewhere else entirely.
endif
~
inlocale(<who> <word>) - 1 if <word> appears in the file path of
the room <who> is in - a loose way to ask
which neighborhood of files we are
standing in.
inarea(<who> <word>) - 1 if <word> appears in the name of the
area <who> is in.
GREET_PROG 100
if inlocale($i aurin)
mpecho Locale check: this room's file lives under an aurin path.
else
mpecho Locale check: we are far from Aurin's streets.
endif
if inarea($n aurin)
mpechoat $n Area check: the charter lists you inside Aurin.
else
mpechoat $n Area check: you are beyond Aurin's bounds.
endif
~
Time and Weather
The mud runs its own clock, calendar, seasons, moon and weather, and scripts can read all of them. A tavern that lights its lamps at dusk, a shrine that only answers under a full moon, a guard who grumbles in the rain - this group is how. There is also a small family of real-world clock checks for events tied to actual wall-clock time.
datetime(<part>) - the current value of one part of the mud
clock: hour, day, month or year. Hours run
0 to 23.
istime(<hour>) - 1 if the mud clock reads exactly that
hour. ishour() is an alias.
GREET_PROG 100
mpecho The mud clock reads hour $%datetime(hour)% of day $%datetime(day)%.
mpecho The month is $%datetime(month)% in year $%datetime(year)%.
if istime(12)
mpecho Noon exactly! The bell tolls twelve.
else
mpecho It is not the noon hour.
endif
~
isday(<n>) - 1 on that day of the mud month. ismonth(<name>) - 1 in the mud month of that NAME. A month
number never matches - use the name.
isyear(<n>) - 1 in that mud year. isseason(<season>) - 1 during spring, summer, autumn or winter. ismoon(<phase>) - 1 while the moon shows that phase, named
as the game names it, such as full moon.
GREET_PROG 100
if isseason(winter)
mpecho Season check: snow lies deep across the realm.
else
mpecho Season check: it is not winter now.
endif
if isday(1) or ismonth(frostmoon) or isyear(1)
mpecho Calendar check: a notable date.
else
mpecho Calendar check: an ordinary date.
endif
if ismoon(full moon)
mpecho Moon check: the full moon rides high.
else
mpecho Moon check: the moon keeps a different face tonight.
endif
~
isweather(<word>) - 1 if <word> appears in the current weather
condition of the realm the host stands in,
so rain matches both rain and rainstorm.
isrlhour(<0-23>) - 1 during that REAL-world hour on the
server clock.
isrlday(<1-31>) - 1 on that real-world day of the month. isrlmonth(<1-12>) - 1 in that real-world month. isrlyear(<year>) - 1 in that real-world year, written in
full, such as 2026.
GREET_PROG 100
if isweather(rain)
mpecho Sky check: rain drums on the rooftops.
else
mpecho Sky check: no rain at the moment.
endif
if isrlhour(0) or isrlday(1) or isrlmonth(1) or isrlyear(2020)
mpecho Real-world check: what an hour to be playing!
else
mpecho Real-world check: just an ordinary earthly moment.
endif
~
Quests, Factions, and Clans
The book-keeping group: has this player done my quest, how does a faction regard them, what banner do they march under, what marks and titles do they carry. This is where repeat-visit content comes from - a giver who remembers, a doorman who defers to the decorated.
questwinner(<who> <quest_id>) - 1 if <who> has ever completed the named
quest. The id is the internal quest name
used by mpstartquest, not the display
title.
The single most useful quest test there is - it stops a giver from handing the same errand out twice:
GREET_PROG 100
if questwinner($n guide_demo_quest)
mpechoat $n Ledger check: you have already finished my errand.
else
mpechoat $n Ledger check: my errand is still open for you.
endif
~
questscripted(<who>) - 1 if <who> carries a script - the engine's
own definition of a quest-flavored mob.
qvar(<quest_id> <key>) - a stored value from an active quest. Read
carefully: the first argument is the
QUEST, not a person - it always asks the
player who set off the trigger, and
answers empty text if that quest is not
active for them.
questobj(<player> <item>) - 1 if <item> is quest-protected for
<player>, meaning the quest system will
not let it be dropped.
GREET_PROG 100
if questscripted($i)
mpecho Script check: this creature carries a script of its own.
endif
mpechoat $n Step marker: $%qvar(guide_demo_quest step)% just now.
mpoload /obj/meal
if questobj($n $b)
mpecho Bond check: that item is quest-bound and cannot be dropped.
else
mpecho Bond check: an ordinary item, freely dropped.
endif
mpjunk $b
~
faction(<who> <faction>) - the standing tier name the faction gives
<who>, from Feared up through Neutral to
Worshipped. The Brinewarrens factions are
syndicate, tideborn and faceless. For the
raw number, use factionrep() under Rogue
extensions.
GREET_PROG 100
mpechoat $n Standing check: Syndicate tier $%faction($n syndicate)%.
if factionrep($n syndicate > 500)
mpechoat $n They practically worship you.
endif
~
hastitle(<who> [<title>]) - with no title, 1 if <who> bears any title
at all; with one, 1 if they hold that
title, matching on any part of it.
GREET_PROG 100
if hastitle($n)
mpechoat $n Herald check: you bear a title already.
else
mpechoat $n Herald check: no titles yet grace your name.
endif
~
hastattoo(<who>) - 1 if <who> carries a tattoo given by
mptattoo. hasacctattoo() and
hastattootime() are aliases with the same
answer.
GREET_PROG 100
mptattoo $i a coiled serpent
if hastattoo($i)
mpecho Ink check: a coiled serpent marks the creature's arm.
endif
~
clan(<who>) - the name of the clan <who> belongs to, or
empty text for none - which makes a bare
if clan($n) a clean membership test.
clanrank(<who>) - their numeric rank within it, 0 if
unranked or clanless.
GREET_PROG 100
if clan($n)
mpechoat $n Banner check: you march with $%clan($n)%.
mpechoat $n Your rank there is $%clanrank($n)%.
else
mpechoat $n Banner check: you march under no banner.
endif
~
Strings, Numbers, and Stored Values
The toolbox group. Dice rolls, arithmetic, text matching, and the functions that read values your scripts store for themselves. If a script feels clever, this group is usually why.
rand(<percent>) - answers 1 that percent of the time -
rand(30) is yes on roughly three greetings
in ten. For variety in what a mob says,
not for whether a trigger fires at all;
the trigger header's own percent handles
that.
randnum(<n>) - a random number from 1 to n - a die. rand0num(<n>) - a random number from 0 to n minus one.
GREET_PROG 100
if rand(50)
mpecho Coin toss: heads!
else
mpecho Coin toss: tails!
endif
mpecho The die shows $%randnum(6)% and the deck offers card $%rand0num(52)%.
~
number(<text>) - the whole number found in <text> after
dollar codes are substituted; 0 if there
is none.
isodd(<number>) - 1 if the number is odd. The argument is
substituted first, so a $%...% value works
inside it.
math(<expression>) - whole-number arithmetic with + - * / and %
for remainder. IMPORTANT: it works
strictly left to right with no
times-before-plus rule, so 3+4*2 is 7
times 2, which is 14. Keep expressions
simple or stage them in steps.
GREET_PROG 100
mpecho Number reads $%number( 42 )% from the padded text.
mpecho Three plus four times two, left to right, makes $%math(3+4*2)%.
if isodd($%level($n)%)
mpechoat $n Your level is an odd number.
else
mpechoat $n Your level is an even number.
endif
~
strin(<word> <text>) - 1 if <word> occurs anywhere in <text>,
ignoring capitals. The word comes first
and must be a single word.
strcontains(<text> <sought>) - the same check with the arguments the
other way around: a single word of text
first, then what to seek in it.
islike(<text> <mask>) - 1 if <text> loosely matches <mask>, where
* stands for anything - *a* asks whether
an a appears at all.
GREET_PROG 100
if strin(cat concatenate)
mpecho Needle check: cat hides inside concatenate.
endif
if strcontains(concatenate cat)
mpecho Haystack check: concatenate indeed contains cat.
endif
if islike($%name($n)% *a*)
mpechoat $n Pattern check: the letter a appears in your name.
else
mpechoat $n Pattern check: not a single letter a in your name.
endif
~
eval(<condition>) - evaluates its argument as a full condition
and answers 1 or 0. Its everyday use is
testing a staged or stored value, as
below; it also lets you splice a yes-or-no
answer into text.
callfunc(<name> [<text>]) - runs the FUNCTION_PROG block of that name
on the host and answers whatever the
block's return line says. The text
argument arrives inside the block as the
message, readable as $G. This is how you
write a computation once and use it from
many triggers.
This example also shows the staging pattern promised earlier: mpargset parks a computed value in slot $0, and eval then tests the slot. The second block only ever runs when called by name:
GREET_PROG 100
mpargset 0 $%numpcsroom()%
if eval($0 > 0)
mpecho Eval check: the staged player count is positive.
endif
mpecho The named routine answers: $%callfunc(bless_line $N)%.
~
FUNCTION_PROG bless_line
return May fortune follow $G
~
var(<obj> <name>) - the value of a script variable stored on
<obj> with mpsetvar, or empty text if it
was never set.
hastag(<obj> <property>) - 1 if the named property on <obj> is set to
something non-zero - the reader for flags
planted with mpset.
Variables are the memory between trigger firings; the variables chapter covers them in depth. The reading half lives here:
GREET_PROG 100
mpsetvar $i greetings 3
if var($i greetings == 3)
mpecho Memory check: the creature recalls three greetings.
endif
mpset $i famous_flag 1
if hastag($i famous_flag)
mpecho Tag check: the famous flag is set.
mpecho And gstat peeks at it too, reading $%gstat($i famous_flag)%.
endif
~
Rogue Extensions
Everything above exists in CoffeeMUD. The functions in this group are Rogue's own additions - direct reads of the systems this mud actually runs on, added so builders never have to approximate. They follow all the same rules.
hp(<who>) - current health points. Defaults to the
host, like the rest of this family.
maxhp(<who>) - maximum health points. sp(<who>) - current spell points. maxsp(<who>) - maximum spell points. ep(<who>) - current stamina points. maxep(<who>) - maximum stamina points. Where hitprcnt() answers a rounded percentage, these answer the real numbers, which is what you want for exact thresholds and for reporting:
GREET_PROG 100
mpechoat $n Vitals: health $%hp($n)% of $%maxhp($n)%.
mpechoat $n Spell power $%sp($n)% of $%maxsp($n)%.
mpechoat $n Stamina $%ep($n)% of $%maxep($n)%.
if hp($n > 0)
mpechoat $n Pulse check: still alive by the numbers.
endif
~
skill(<who> <skill>) - the rank of the named skill, with skill
names of any number of words - the fully
capable version of expertise().
GREET_PROG 100
if skill($n magic attack > 0)
mpechoat $n Study check: you have trained magic attack.
mpechoat $n Its rank stands at $%skill($n magic attack)%.
else
mpechoat $n Study check: you have never trained magic attack.
endif
~
factionrep(<who> <faction>) - the raw reputation number from -1000 to
1000 with the named faction - the precise
companion to faction()'s tier name.
GREET_PROG 100
mpechoat $n Ledger note: $%factionrep($n syndicate)% Syndicate points.
~
weather() - the current weather condition word for the
realm the host stands in, such as clear,
overcast or rainstorm.
season() - the current season word: spring, summer,
autumn or winter.
timeofday() - the current band of light: dawn, day, dusk
or night.
isnight() - 1 at night - the shortcut for the single
most common time check in building.
GREET_PROG 100
mpecho The sky says $%weather()% and the calendar says $%season()%.
mpecho The light overhead says $%timeofday()%.
if isnight()
mpecho Night watch: stars wheel overhead.
else
mpecho Day watch: the sun still holds the sky.
endif
~
groupsize(<who>) - how many members <who>'s adventuring group
has, 0 for someone travelling alone.
GREET_PROG 100
if groupsize($n > 1)
mpechoat $n Party check: your group counts $%groupsize($n)% members.
else
mpechoat $n Party check: you travel alone.
endif
~
Compatibility Stubs
CoffeeMUD defines a handful of functions for systems Rogue does not have - practice sessions, training points, birthday tracking, numbered quest rooms. So that scripts pasted from CoffeeMUD documentation run without errors, Rogue accepts all of them and answers 0 or empty text: trains, pracs, isbirthday, isrecall, isable2, questmob, isquestmobalive, questroom, questarea, explored, clandata and clanqualifies. They never error and never answer yes. If a pasted script leans on one of these for its logic, that branch will simply never run; rewrite it with a live function from this chapter.
GREET_PROG 100
mpecho Stub check: trains=$%trains($n)% pracs=$%pracs($n)%.
mpecho More: birthday=$%isbirthday($n)% recall=$%isrecall($n)%.
mpecho Also: explored=$%explored($n)% able2=$%isable2($n)%.
mpecho Quest stubs: questmob=$%questmob(1)% questroom=$%questroom(1)%.
mpecho And questarea=$%questarea(1)% rounds out the set.
~
A Combined Scenario
Everything in one working doorman. Read it top to bottom before the commentary; most of it should read as English by now.
GREET_PROG 100
mpechoat $n The warden looks you over slowly, $N.
if isimmort($n)
mpechoat $n Warden's verdict: staff pass where they please.
else
if level($n > 10) and hitprcnt($n > 50)
mpechoat $n Warden's verdict: strong enough to pass the gate.
else
mpechoat $n Warden's verdict: come back rested and seasoned.
endif
endif
if !var($i visitors)
mpsetvar $i visitors 0
endif
mpsetvar $i visitors $%math($<$i visitors> + 1)%
mpecho The warden notches his tally: $<$i visitors> visitors so far.
~
Line by line: the greeting substitutes the visitor's name with $N. The first test waves staff through - a courtesy your testers will thank you for. Mortals face a joined condition: level above ten AND more than half health, an if nested inside the else branch. Then the tally: the first visit finds no visitors variable, so the script creates it at 0; every visit then stores the old count plus one, computed by math() over the stored value, and announces it. Attach this to a mob, walk in and out a few times, and watch the count climb - that is a script with memory, built from three functions and two commands.
Testing Your Functions
Three habits make function work painless. First, fire triggers by hand: mudprog <target> test GREET_PROG sets off the named block with you as the source, no walking in and out required. Second, when a condition mystifies you, print the pieces - drop a line such as mpecho debug level=$%level($n)% above the if and read what the engine actually sees. Third, admins testing raw lines on themselves can use scripttest, with one caution: some clients mangle dollar signs typed at the prompt, so for anything with dollar codes in it, put the lines in a file and run scripttest runfile <path> instead. See help scripttest for the details.
And the rule worth repeating one last time, because it explains nearly every silent failure: an unknown or misspelled function answers 0, quietly. When in doubt, echo it.
This chapter teaches the part of MUDProg that makes a script feel alive: variables and substitution. You do not need to have written a script before, and you do not need to know anything about programming. Every idea is built up from nothing, and every idea comes with an example you can paste into the game and watch run.
If you have not read the main help mudprog page yet, read its first two sections so you know what a PROG block looks like and how the mudprog command attaches a script to a mob, an item, or a room. Everything else you need is here.
Why Substitution Exists
Imagine you are writing a gatekeeper who greets people. You could write:
GREET_PROG 100
say Welcome, traveler!
~
That works, but it is flat. The gatekeeper says the same words to everyone, and players notice. What you really want is for him to greet Marla by name when Marla walks in, and Tuck by name when Tuck walks in. But when you write the script, you have no idea who will walk in. You cannot type a name that does not exist yet.
Substitution solves this. You write a small placeholder that starts with a dollar sign, and the moment the line actually runs, the engine swaps the placeholder for the real, live value:
GREET_PROG 100
say Welcome, $N!
~
When Marla enters, the room hears Welcome, Marla! When Tuck enters, it hears Welcome, Tuck! You wrote the line once; the engine fills in the blank every time, freshly, using whatever is true at that instant. That is all substitution is: fill-in-the-blank for scripts.
These placeholders are called dollar codes. There are about forty of them, and this chapter lists every single one. They work in every line of a script: in say and emote lines, in mpecho text, inside stored values, everywhere text passes through the engine.
The Cast Of An Event
Before the table, you need one idea, and it is the single most important idea in all of MUDProg: every time a trigger fires, the engine gathers a small cast of characters for that one event. The dollar codes are simply names for the members of that cast.
The cast is:
The host - the object the script is attached to: the mob, the item,
or the room that owns the PROG block. The script always
runs "as" the host.
The source - whoever caused the event. A player walks in: they are the
source of the GREET_PROG. A player speaks: they are the
source of the SPEECH_PROG. Almost always a player.
The target - the other party of the event, when there is one. In many
triggers the target is simply the source again; it differs
when the event has a victim or recipient of its own.
Item one - the item involved in the event, when there is one: the
thing given, dropped, picked up, or worn.
Item two - a second item, for the rare events that involve two.
The message - a line of text riding along with the event: the sentence
someone spoke, the number of coins in a bribe, the new
level on a level-up.
Two more members join the cast while the script runs:
The last load - the most recent object this script created with
mpmload or mpoload
or mpoloadroom.
The ten slots - numbered scratch spaces 0 through 9 the script can
write into and read back during this one run.
Every dollar code in the tables below reads one of these cast members, or something about the room the host stands in. When you wonder what a code will produce, ask yourself: who is the cast for this trigger?
How To Read The Tables
All the samples below share one imaginary scene so you can see real values. The script is attached to Bram, a male gatekeeper NPC whose short description is Bram the Gatekeeper. The trigger was fired by Marla, a female player. The target of the moment is Tuck, a male bandit. Item one is a bronze key and item two is a leather pouch. The room is called Gatehouse Square, its area is Aurin, and it has exits north and east.
In each entry, the indented line before the word becomes is text you might write in a script, and the text after it is what appears in its place when the line runs. Only the substitution is shown; if the line is a say, the game still dresses it up as speech in the usual way.
One warning before the list: dollar codes are case sensitive. A capital letter is a different code from its small letter. Some pairs happen to produce the same text, and those are pointed out, but never assume it.
Names
$n and $N - the name of the source: whoever set the trigger off, almost always the acting player. If there is no source it reads as someone. Both spellings give the same text; the pair exists because old CoffeeMUD scripts use both, and both should work here.
Good day, $n. becomes Good day, Marla.
$t and $T - the name of the target. In most triggers the target is just the source again, so this reads the same as $n; it differs where the event has a second party, such as the mob's combat opponent. Reads someone when there is no target.
Leave $t out of this. becomes Leave Tuck out of this.
$i - the host's own proper name: the name of the mob, item, or room the script is attached to.
I am $i, and I keep this gate. becomes I am Bram, and I keep this gate.
$I - the host's short description, the fuller line you see in a room listing. Falls back to the plain name if there is no short.
You stand before $I. becomes You stand before Bram the Gatekeeper.
$q and $Q - identical to $i and $I. In CoffeeMUD these were the codes used when the host was a room or an item rather than a mob; on Rogue they read the same either way, and they exist so CoffeeMUD scripts paste in unchanged.
This is $q you are speaking to. becomes This is Bram you are speaking to.
$r and $R - the name of a random player in the room. Empty when no player is present. The player is picked fresh every time the code appears, so two $r in one line can name two different people when several players are in the room.
$r, you look honest enough. becomes Marla, you look honest enough.
$c and $C - the name of a random living thing in the room other than the host: a player or a mob, whichever the roll lands on. Empty when the host is alone. Re-rolled at every occurrence, like $r.
Keep an eye on $c for me. becomes Keep an eye on Tuck for me.
$f - the name of whoever the host is following, its leader. Empty when the host follows no one. Suppose Bram follows a captain named Elsa:
I answer only to $f. becomes I answer only to Elsa.
Pronouns
These codes produce he, him, and his, or she, her, and hers, or it and its, according to the gender of a cast member. They let one script read naturally for everyone. Marla is female and Tuck and Bram are male in the samples.
$e, $s, $m - the source as he, him, his (here: she, her, her).
$n lowers $m hood. becomes Marla lowers her hood.
$E, $S, $M - the target as he, him, his.
$t sheathes $M blade. becomes Tuck sheathes his blade.
$j, $h, $k - the host itself as he, him, his. Most useful in mpecho lines that describe the host from the outside.
mpecho $I strokes $k beard. becomes Bram the Gatekeeper strokes his beard.
$J, $H, $K - a random player in the room as he, him, his. Careful: each of the three picks its own random player, so avoid mixing them in one sentence when more than one player may be present.
$F - he or she for the host's leader, the person $f names. Reads it when the host follows no one.
Sir And Madam
$y - sir or madam according to the source's gender, with sir as the fallback. Perfect for servants, guards, and shopkeepers.
As you wish, $y. becomes As you wish, madam.
$Y - the same, for the target.
See that $Y is comfortable. becomes See that sir is comfortable.
Items In The Event
Some triggers carry items in their cast. A GIVE_PROG fires because someone handed the host something: that something is item one. Drop, get, put, and wear events carry the item involved the same way. Most triggers carry no items at all, and the item codes then fall back to the word something.
$o and $O - the name of item one.
So you offer me $o? becomes So you offer me a bronze key?
$p and $P - the name of item two, for the few events that involve a second item.
And $p besides? Generous. becomes And a leather pouch besides? Generous.
$b and $B - the last object this script run loaded with mpmload, mpoload, or mpoloadroom: its name and its short description, which on Rogue are the same text in practice. Empty before the script has loaded anything. This is how a script talks about a thing it just created without having to know the thing's name in advance.
mpecho Out of the crate rolls $b. becomes Out of the crate rolls a torch.
$w and $W - the name of the living creature currently carrying item one and item two, respectively. Empty when the item is lying loose or inside a container rather than being carried.
Careful, $o belongs to $w. becomes Careful, a bronze key belongs to Tuck.
The Room Around The Script
These codes describe wherever the host is standing at the moment the line runs. If the host wanders, they change with it.
$a and $A - the name of the area the room belongs to. Empty when the room has no area name set.
You are deep in $a country now. becomes You are deep in Aurin country now.
$d - the room's title, the short line at the top of a look.
Welcome to $d. becomes Welcome to Gatehouse Square.
$D - the room's full look description. It is usually a paragraph, so use it sparingly; it is mostly useful for scripts that quote the room back at a player.
$l - a comma separated list of every living thing in the room except the host itself.
I can see $l from here. becomes I can see Marla, Tuck from here.
$L - a comma separated list of every loose item in the room.
All this is mine: $L. becomes All this is mine: a bronze key, a leather pouch.
$x and $X - one exit direction from the room, chosen at random. Empty in a room with no exits, and re-rolled at every occurrence, which makes it handy for a bored guard waving travelers vaguely onward.
Try the road $x, perhaps. becomes Try the road north, perhaps.
The Message
$g - the text riding along with the event, forced to lower case. What that text is depends on the trigger: for SPEECH_PROG it is the sentence spoken, for BRIBE_PROG the number of coins handed over, for LEVEL_PROG the new level, for TIME_PROG the hour, for CMDFAIL_PROG the command line that failed. Lower case makes it easy to compare against keywords without worrying how the player capitalised things. If Marla said Open The Gate:
You dare say $g to me? becomes You dare say open the gate to me?
$G - the same text with its original capitalisation kept, for when you want to quote the player exactly.
Your words were: $G becomes Your words were: Open The Gate
The Temporary Slots
$0 through $9 - ten numbered scratch spaces belonging to this one run of this one block. They start empty every time the trigger fires, and whatever you put in them evaporates when the block finishes. Three things write into them: for loops count into one, mpargset stores any text into one, and mploadvar copies a stored variable into one. All three are demonstrated later in this chapter. If mpargset 4 hello has run:
I said $4 already. becomes I said hello already.
Literal Dollars And Typos
$$ - one real dollar sign. Because the dollar starts every code, you need this whenever you actually want the character itself.
The toll is $$10. becomes The toll is $10.
A dollar followed by a letter that is not a code is left exactly as you typed it, so a typo like $z shows up as a literal $z in the output, which makes it easy to spot. But be careful with punctuation after a dollar: the four characters <, %, [, and { each begin a special form when they follow a dollar sign (two of those forms are the subject of the rest of this chapter), so a stray one of those will swallow text. When in doubt, write $$ for a real dollar.
Seeing Substitution Happen
Enough tables. Here are four complete scripts you can attach to a practice mob and fire. To try one: stand in a room with a harmless NPC, use mudprog to attach the script, and then fire the trigger on yourself with the test option. For example, with a mob called dummy:
mudprog dummy set GREET_PROG 100 ; say Hello, $N! ; ~
mudprog dummy test GREET_PROG
One caution when testing: some telnet clients mangle dollar signs typed at the prompt. If your codes seem to vanish before they reach the mud, use the mudprog line editor, or put the script in a file and use scripttest runfile (see help scripttest).
First, names and pronouns together. The say line uses $N for the player's name and $i for the host's own; the emote line shows that substitution works in every command, not just say:
GREET_PROG 100
say Well met, $N! I am $i, and I keep this gate.
emote looks $n up and down slowly.
~
Second, the message codes. This block fires whenever anyone speaks (the word all in the header means match any speech), and quotes the speaker back with $G:
SPEECH_PROG all
say You said $G just now, and I hear every word spoken at this gate.
~
Third, the room codes. $d names the room and $x picks a random exit, so the same two lines of script give sensible directions wherever this mob is standing:
GREET_PROG 100
say Welcome to $d, traveler. If you grow tired of me, the way $x stands open.
~
Fourth, the last-load code. The script creates an item out of thin air with mpoloadroom and then talks about it using $b, without ever needing to know what the item is called:
GREET_PROG 100
mpoloadroom /obj/meal
mpecho A tray slips and $b tumbles onto the floor.
~
Two Jobs: Picking A Person And Printing A Name
Here is a subtlety that confuses everyone once, so let us get it out of the way early. A dollar code can stand in two different places in a line, and it does a different job in each.
In the text of a message, $N means the NAME of the source: letters that get pasted into the sentence. But in the argument position of an mp command, where the command expects to be told WHO, the code hands over the person themself. Look at this line:
GREET_PROG 100
mpechoat $n Only you hear this whisper, $N, and no one else.
~
The first code, $n, is the answer to mpechoat's question "send this to whom?" - it delivers the actual player, and the engine sends the message to them alone. The second code, $N, sits inside the message text, so it becomes the player's name in print. Same person, two jobs: pick them, and name them. Every mp command that takes a who or a what accepts a dollar code (or a plain name like guard) in that position, and it means the object itself, not its name.
Stored Variables: Giving Objects A Memory
Everything so far vanishes the instant the script finishes. The dollar codes read the moment; the slots are wiped after every run. But the best scripts remember. A gatekeeper who knows he has met you before, a counter that ticks up with every visitor, a toll that you only pay once: all of these need a place to keep a value BETWEEN runs.
That place is a stored variable. Picture a small labelled note stuck to an object: the label is a name you choose, and the note holds a piece of text. Any object can carry these notes - a mob, an item, a room, or a player - and every script that can see the object can read its notes.
Three facts to hold onto:
1. A variable lives ON an object. There is no free-floating variable;
you always say whose note it is.
2. A variable holds text. Numbers are simply text made of digits, and
the engine is smart enough to compare them as numbers when both
sides look numeric.
3. Reading a note that was never written gives you an empty value, not
an error. Scripts use this constantly: empty means "not yet".
Storing A Value: mpsetvar
mpsetvar <object> <name> <value>
The object is who to stick the note on: $i for the scripted host itself, $n for the player who fired the trigger, $b for a freshly loaded item, or a plain name like guard for something in the room. The name is one word - keep it to lower case letters and underscores. The value is everything after the name, spaces and all.
Two details worth noticing. First, dollar codes inside the value are substituted at the moment of STORING, so that last line stores the actual name, say Marla, not the code $N. Second, storing to a name that already exists simply overwrites the old value. To erase a note, store nothing: mpsetvar $n toll_paid with no value leaves it empty, and empty counts as "not set" everywhere that matters.
Reading A Variable Inside Text
To read a note back into a sentence, use the angle form:
$<object name>
A dollar sign, an opening angle bracket, the object, one space, the variable name, and a closing angle bracket. Where it stands in the text, the stored value appears. If the note does not exist, nothing appears.
say My mood today is $<$i mood>, since you ask.
If the host stored grumpy earlier, the room hears: My mood today is grumpy, since you ask.
The object part accepts the same references as everywhere else: $i, $n, $t, $b, the words self or me for the host, or a plain name of something in the room. That last option is quietly powerful: any script can read the notes on any object it can see. A guard can read a note stored on the door; a door can read a note stored on the guard.
Reading A Variable Inside A Condition
The angle form is for text. When you want to make a DECISION based on a variable - the whole point of remembering things - use the var function inside an if. It takes the object, the name, and optionally a comparison, all inside one pair of parentheses:
if var($n toll_paid == yes)
say You have paid already.
endif
Note the shape carefully: the comparison sits INSIDE the parentheses, right after the variable name. That is the reliable form; always write conditions this way. Two single quote marks stand for an empty value, so this asks "has this note never been written?":
if var($i visits == '')
mpsetvar $i visits 0
endif
There is an even simpler form for yes-or-no notes. A bare var(...) with no comparison counts as true when the note holds anything at all except an empty value, a 0, the word no, or the word false:
if var($n toll_paid)
say You have paid already.
endif
Store yes into a flag and test it bare: that pair covers half of everything you will ever want a variable for.
Who Keeps A Variable, And For How Long
Where you stick the note decides how long it lasts. This is the difference between a memory that survives a reboot and one that quietly evaporates, so choose deliberately:
Stored on Lasts Good for
a mob or item as long as that particular copy short-term state:
exists - death, a reset sweep, moods, counters,
or a reboot clears it once-per-fight flags
a room until the room reloads, at a state shared by a
reboot or an update scene: a lever pulled,
a trap sprung
a player indefinitely - player notes are long-term memory:
saved with the character and tolls paid, favors
survive logging out and reboots owed, NPCs met
The player row is the special one. Notes stored on a player with mpsetvar $n ... are written into their saved character, so a mob can remember a player across weeks of real time even though the mob itself has died and respawned a hundred times since. The trick is that the MEMORY travels with the player, and every fresh copy of the mob reads it back.
Because every script shares one pocket of notes per object, pick variable names unlikely to collide with someone else's script - bram_toll_paid is safer than paid.
mpsavevar
mpsavevar <object> <name> <value>
On Rogue this is exactly mpsetvar under a second name. It exists because CoffeeMUD scripts use MPSAVEVAR when they want a variable forced into the character save; Rogue saves player notes automatically, so both spellings do the same thing, and pasted CoffeeMUD scripts just work.
Mud-Wide Globals: mpgset
mpgset <name> <value>
This stores a value in the script engine itself rather than on any object. Globals survive reboots and are shared by the entire mud. However, in the current engine there is no dollar code or function that reads a global back into a script - they can be read by server-side code and inspected by admins, but not yet substituted into script text. So for now, when two scripts need to share a value, do not reach for a global: put the note on an object both scripts can see. Attach the script to the room and store on $i, or store on a named mob or item and read it with the angle form from anywhere in the room. The price tag example below does exactly this.
The Slots Again: mpargset And mploadvar
Back to the ten scratch slots, $0 through $9, now that you can see what they are for: they are the workbench where a script lays things out mid-run, as opposed to variables, which are the filing cabinet. Slots are fast and disposable; variables persist. A typical script copies a variable onto the workbench, fiddles with it, and files the result back.
mpargset <slot> <value>
Puts any text into a slot, with dollar codes in the value substituted first. mpargset 1 $N puts the source's name into slot 1, readable as $1 for the rest of the run.
mploadvar <object> <name> <slot>
Copies a stored variable straight into a slot. If you leave the slot off, it lands in slot 0. This exists for CoffeeMUD compatibility and for tidiness; mpargset 2 $<$i mood> does the same job as mploadvar $i mood 2.
Here is both of them in one script, plus a stored variable, so you can watch text flow from cabinet to workbench to speech:
GREET_PROG 100
mpsetvar $i favorite blue
mploadvar $i favorite 2
mpargset 3 $%randnum(6)%
say My favorite color is $2, and today my lucky number is $3.
~
The first line files blue in a note called favorite. The second copies that note into slot 2. The third rolls a die - that is function substitution, explained shortly - and drops the result into slot 3. The say line then reads both slots.
One more slot-filler: for loops count into a slot of your choosing. Always loop into a NUMBERED slot; looping into a letter would collide with the dollar codes you just learned.
GREET_PROG 100
for $2 = 1 to 3
say Rule number $2: the gate closes at sundown.
next
~
The loop runs its body three times with $2 reading 1, then 2, then 3, so the guard recites three numbered rules from one written line.
Worked Example: A Visitor Counter
Now let us build the three classics, slowly. First, a counter: a doorman who announces your visitor number, and whose count keeps climbing with every guest, because it lives in a note on him rather than in a slot.
GREET_PROG 100
if var($i visits == '')
mpsetvar $i visits 0
endif
mpargset 1 $%MATH($<$i visits> + 1)%
mpsetvar $i visits $1
say Welcome in! You are visitor number $1 today.
~
Walk through it line by line:
1. The if asks whether the note called visits on the host is still
empty - that is, whether this is the very first run. If so, the
script files a 0 to start the count from. Doing this makes the
arithmetic on the next line safe.
2. The mpargset line is the heart. Inside it, $<$i visits> is the
angle form reading the current count as text, and $%MATH(...)% is
function substitution asking the engine to do arithmetic on it.
On the third visitor, the inner part becomes MATH(2 + 1), the
engine works it out to 3, and mpargset drops 3 into slot 1.
3. The next line files the new count back into the note, so the
increase survives until the next guest.
4. The say line reads slot 1 to announce it.
Fire the trigger repeatedly and the number climbs: 1, 2, 3. Kill or reset the mob and the count starts over, because the note lived on that copy of the mob. If you wanted a count that survives resets, you would attach the script to the room and store on $i there instead - same script, different host.
Worked Example: An NPC That Remembers You
Second classic: memory of a person. The note goes on the PLAYER this time, which is what makes it permanent - it rides along in their saved character, so this hermit still knows Marla next month:
GREET_PROG 100
if var($n met_hermit)
say Back so soon, $N? I told you I never forget a face.
else
mpsetvar $n met_hermit yes
say Well met, stranger. Out here I never forget a face.
endif
~
The first time Marla arrives, the bare var test finds no note called met_hermit on her, so the else branch runs: the hermit greets a stranger and writes the note. Every arrival after that, forever, the test finds yes and he greets an old acquaintance. Two players get independent treatment automatically, because each carries their own note - you never wrote a line of per-player bookkeeping.
Worked Example: A Price Tag On An Item
Variables can sit on items too, and any script that can see the item can read them. Here the host loads a meal into the room, writes a price note on it using $b (the last-loaded object), and then reads the note back off the meal itself - the note lives on the meal, not on the cook:
GREET_PROG 100
mpoloadroom /obj/meal
mpsetvar $b price 15
say Fresh from my kitchen, that. The tag on it says $<$b price> gold.
~
The pattern to take away: an object's notes are a tiny public notice board. Any other script that can see the meal can read the same note by name, as $<meal price> in text or var(meal price == 15) in a condition. A vendor script can price goods this way, a quest mob can mark an item as blessed, and a room script can check the mark later.
Function Substitution: $%
The angle form reads notes. The percent form asks questions. Between a dollar-percent and a closing percent you write a function call, and the engine replaces the whole thing with the answer:
$%FUNCTION(arguments)%
Functions are the same ones used in if conditions - there is a full catalogue in the functions chapter - but here their answer is pasted into text instead of steering a branch. A few of the most useful, in sentences:
say You look to be about level $%level($n)%.
say That purse of yours holds $%goldamt($n)% gold.
say You call yourself $%name($n)%, a $%race($n)%, do you?
say I rate my own health at $%hitprcnt($i)% percent.
say I roll the die and it shows $%randnum(6)%.
say Two and two make $%MATH(2 + 2)%.
say My mood note reads $%var($i mood)%.
That last line shows that var works in the percent form too - $%var($i mood)% and $<$i mood> produce exactly the same text, and you can use whichever reads better to you.
Rules of the form, all three learned the hard way by someone before you:
1. The text between the percents runs when the LINE runs, freshly each
time. A RAND_PROG that says $%randnum(6)% rolls a new die on every
fire.
2. Everything up to the FIRST closing percent is taken as the function
call, so never put a percent sign inside one. A percent sign
elsewhere in the line is harmless; only the pair of a dollar sign
and a percent sign together opens the form.
3. A misspelled function name quietly becomes 0. If a sentence keeps
saying 0 where you expected a value, check your spelling against
the functions chapter.
Dollar codes nest happily inside the parentheses - $%level($n)% reads the source, $%goldamt($t)% the target - and the angle form nests there too, which is exactly what the counter example did with $%MATH($<$i visits> + 1)%.
Here is a complete script to see several at once:
GREET_PROG 100
say You look to be level $%level($n)% to my eye, $y, and that purse holds $%goldamt($n)% gold.
~
An observant guard: he sizes up each arrival's level, bows with sir or madam as appropriate, and guesses their purse to the coin.
Worked Example: The Toll Gate
Third classic, combining a permanent player note, a function question, and branching. A toll keeper wants ten gold, once, from each traveler:
GREET_PROG 100
if var($n toll_paid)
say Pass freely, $N. Your toll is already settled.
else
if goldamt($n > 9)
mpmoney $n -10
mpsetvar $n toll_paid yes
say Ten gold for the toll, $N, and fairly paid. Go on through.
else
say The toll is ten gold, $y, and you carry only $%goldamt($n)%. Come back with coin.
endif
endif
~
Read it as the toll keeper would:
1. Have you paid before? The bare var test on the player's toll_paid
note answers instantly, and repeat customers are waved through.
2. If not, can you pay? goldamt($n > 9) is a function with its
comparison inside the parentheses, true from ten gold upward - the
reliable way to write "ten or more" is strictly-more-than-nine.
3. If you can: mpmoney takes ten gold (a negative amount takes rather
than gives), the note is written so you are never charged again,
and you are sent through.
4. If you cannot: the percent form tells you, to the coin, how short
you are - a touch of cruelty that costs one substitution.
Notice how little bookkeeping there is. No list of who paid, no ledger to clean up: the fact of payment is stored on the payer, which is the object that will still exist next month.
Putting It All Together
One final script that uses nearly everything in this chapter at once: a gatekeeper who counts every traveler past his post (a note on himself), collects a one-time toll (a note on each player), sizes up purses (the percent form), does arithmetic (MATH with the angle form nested inside), and speaks through the slots:
GREET_PROG 100
if var($i watch_count == '')
mpsetvar $i watch_count 0
endif
mpargset 1 $%MATH($<$i watch_count> + 1)%
mpsetvar $i watch_count $1
if var($n toll_paid)
say Back again, $N? Your toll is settled, so pass as you please.
else
if goldamt($n > 9)
mpmoney $n -10
mpsetvar $n toll_paid yes
say Ten gold, $y, and the toll is met. The road is yours.
else
say No coin, no crossing, $N. The toll is ten gold and you carry only $%goldamt($n)%.
endif
endif
say That makes $1 travelers past my post this watch.
~
Every technique in it should now read like plain language to you. The count lives on the gatekeeper and resets with him; the toll receipts live on the players and never expire; the slots carry this run's number from the arithmetic to the closing line. That closing line runs on every branch, so whether you paid, passed, or were turned away, you still hear the tally tick.
Common Mistakes
A short list of the ways this chapter goes wrong in practice, so you can recognise each one in under a minute.
Case matters. $i is a name and $I is a short description; $g is lower case text and $G is the original. If a substitution looks almost right, check the letter's case first.
Forgetting the object when reading a variable. var(visits) does not read the host's visits note - it goes looking for an OBJECT called visits and finds nothing. Always name the owner first: var($i visits), $<$i visits>.
Comparing with the angle form. The angle form belongs in text. In an if, always use var() with the comparison inside the parentheses: if var($i visits == 3), never an angle-form comparison. When comparing numbers, prefer the strict forms, greater-than and less-than, as the toll gate does with $n > 9.
Expecting mob notes to be permanent. A note on a mob dies with that copy of the mob - reset sweeps and reboots included. Long-term memory belongs on the player; scene-level memory belongs on the room.
Name collisions. All scripts share one pocket of notes per object. Two builders both using a note called count on the same player will trample each other. Prefix your names.
Reading globals. mpgset stores a global, but no dollar code reads one back yet. Share values through notes on a visible object instead.
Forgetting $$ for a real dollar sign, especially before punctuation - a dollar before <, %, [, or { starts a special form and will eat your text.
Treating slots as storage. $0 through $9 are wiped on every run. Anything that must outlive the current trigger fire goes in a note via mpsetvar.
Storing substitutes immediately. mpsetvar $n greeter $I stores the host's short description as it is RIGHT NOW; the note will not update later if the host changes. Usually that is exactly what you want - just know it.
That is the whole of variables and substitution. With the dollar codes, the notes, and the percent form, your scripts can name anyone, remember anything, and answer questions about the live world. The next chapters put those powers behind triggers and conditions, where they belong.
Every script you have written so far runs straight down the page: the trigger fires, the first line runs, then the second, then the third, until the block ends. That is enough for a mob that always says the same thing, but the mobs players remember are the ones that seem to notice things. The innkeeper who charges warriors double. The fortune teller who reads mages differently from rogues. The guard who counts to three before acting. All of that is done with control flow: the small set of words that let a script make decisions and repeat itself. This chapter teaches every one of them from the ground up: if, else, endif, switch, for, while, break, return, and the timing commands mpsleep and mpalarm.
You do not need to have programmed before. Every idea is explained from zero, and every example on this page is a complete script you can paste onto a practice mob with mudprog and fire immediately. All of the examples use GREET_PROG 100 as their trigger, which fires every time a player walks into the room, so you can test each one either by walking out and back in, or instantly with mudprog <mob> test GREET_PROG. Control flow works identically inside every other trigger type.
What A Condition Is
A condition is a question with a yes or no answer. That is the whole idea. When the script reaches an if line, it asks the question on that line. If the answer is yes, it runs the lines underneath. If the answer is no, it skips them. Nothing more mysterious than that is happening anywhere in this chapter.
The questions themselves are asked with functions. A function is a named question the game already knows how to answer, written as the name followed by parentheses, with the details of the question inside the parentheses:
level($n) - What level is the player who set me off?
class($n) - What class are they?
ispc($n) - Are they a real player, not a mob?
isfight($n) - Are they in combat right now?
rand(30) - Roll the dice: yes 30 times out of 100.
isnight() - Is it night on the game clock?
The thing inside the parentheses is usually WHO the question is about, given as a dollar code. Three cover almost everything: $n is the person who set the trigger off (usually the player), $i is the scripted object itself (your mob), and $t is the current target. The full list of codes is in the variables chapter, and the full list of functions is in the functions chapter; this chapter only uses a handful.
Some questions, like ispc($n), are naturally yes-or-no. Others, like level($n), answer with a value - a number or a word - and you turn that into a yes-or-no question by comparing it to something, which is the subject of the operators section below. A function used on its own, with no comparison, counts as yes when its answer is anything other than nothing: the answers 0, an empty answer, and the words no and false count as no; everything else counts as yes.
Your First if
Here is the smallest possible decision. The three key words are:
if <condition> - Ask the question. On yes, run what follows.
else - Otherwise run this part instead. Optional.
endif - The end of the whole decision. Required.
And here it is in a working script:
GREET_PROG 100
if ispc($n)
say A real adventurer walks in. Welcome, $N.
endif
say The door creaks shut behind you.
~
Read it the way the engine does, top to bottom. A player enters, so GREET_PROG fires. The first line inside the block is an if, so the engine asks its question: is $n a player character? For a player the answer is yes, so the line between the if and the endif runs and the mob speaks the welcome. Then execution carries on past the endif and the second say runs as well.
The important thing to notice is the last line. It sits OUTSIDE the decision, after the endif, so it runs every single time, whether the question was answered yes or no. The if only guards the lines between itself and its endif. If a wandering mob set the trigger off instead, the welcome would be skipped but the door would still creak.
The indentation - the four extra spaces before the guarded line - means nothing to the engine. It ignores leading spaces on every line. Indent anyway, always, because in three months you will read this script again and the shape of the indentation is what will tell you at a glance which lines belong to which decision. Also note that the keywords are not fussy about capitals: if, IF, and If all work. This guide writes them in lower case throughout.
Adding else
Often you do not just want to do something on yes; you want to do one thing on yes and a different thing on no. That is what else is for. It splits the decision into two halves, and exactly one of the two halves runs - never both, never neither:
GREET_PROG 100
if ispc($n)
say Customers at last! Come in, come in.
else
say Another wandering creature. Shoo.
endif
~
When a player enters, the question comes up yes, the first half runs, and the second half is skipped entirely. When something that is not a player wanders in, the question comes up no, the first half is skipped, and the second half runs. The endif closes the whole structure either way. Think of it as a fork in the road: the script always takes exactly one of the two paths, and both paths meet again at the endif.
Why endif Matters
Every if must be closed by its own endif. The engine will not error if you forget one - scripts are deliberately built never to crash their host - but the behavior will quietly go wrong: without the closing word, everything to the end of the block gets swallowed into the decision, so lines you meant to always run will only run when the condition happens to be yes. If a script of yours has lines that mysteriously fire only sometimes, count your if words and count your endif words. They must match exactly.
The Comparison Operators
Questions like level($n) answer with a value, and to make a value into a yes-or-no question you compare it against something. The comparison goes right on the if line, after the function. There are seven operators:
== - Equal to.
!= - Not equal to.
> - Greater than.
< - Less than.
>= - Greater than or equal to; at least.
<= - Less than or equal to; at most.
.in. - The left value appears somewhere inside the right value.
Each one gets a worked example below. A note before we start: several of the examples store a small note on the mob first with mpsetvar and then read it back with the var() function or drop it into spoken text with $<$i name>. That is the script variable system, covered fully in its own chapter - here it is just a handy way to have a known value to compare against.
Equal To: ==
GREET_PROG 100
mpsetvar $i password swordfish
if var($i password) == swordfish
say The password is still swordfish, same as it has been for years.
else
say Someone has changed the password again.
endif
~
The first line stores the word swordfish on the mob under the name password. The if line then reads that stored value back and asks: is it equal to the word swordfish? It is, so the first half runs. Word comparisons with == ignore capital letters - Swordfish, SWORDFISH, and swordfish all count as equal. Numbers compare as numbers, so level($n) == 10 is true exactly at level ten. You may also write a single =; it means the same thing as ==.
Not Equal To: !=
GREET_PROG 100
if class($n) != innkeeper
say You are no innkeeper, so you pay full price for a room.
else
say A fellow innkeeper! Half price for you, friend.
endif
~
!= is simply == turned inside out: it answers yes when the two values are different. No player class is called innkeeper, so every visitor hears about the full price. Like ==, it ignores capitals when comparing words. Old CoffeeMUD scripts sometimes spell it <>; that spelling works here too.
Greater Than: >
GREET_PROG 100
if level($n) > 0
say You have trained at least a little. That is a start.
else
say A visitor with no training at all. Remarkable.
endif
~
> answers yes when the left number is strictly bigger than the right one. Strictly means that equal does not count: if the two are the same, the answer is no. Every real player is at least level one, so one is greater than zero and the first half runs.
Less Than: <
GREET_PROG 100
if level($n) < 1000
say Still room to grow, I see. Keep at it.
else
say You have outgrown this little town entirely.
endif
~
The mirror image: yes when the left number is strictly smaller than the right one. Nobody reaches level one thousand, so everyone still has room to grow.
At Least: >=
GREET_PROG 100
if level($n) >= 1
say Level one or better. The guild will see you now.
else
say Come back when you have a level to your name.
endif
~
>= is greater OR equal - read it as at least. The practical difference from > is the boundary itself: level($n) >= 20 includes a level twenty player, while level($n) > 20 excludes them. Off-by-one mistakes at boundaries like this are one of the commonest bugs in scripting, in every language ever made, so whenever you gate something on a level, say the rule out loud - do I mean twenty and up, or above twenty? - and pick the operator that matches. The old spelling => is also accepted.
At Most: <=
GREET_PROG 100
if level($n) <= 500
say The wardens can still protect someone of your strength.
else
say No warden alive could protect you now.
endif
~
<= is less or equal - read it as at most. The boundary is included: at exactly 500 the answer is still yes. The old spelling =< is also accepted.
Contains: .in.
GREET_PROG 100
mpsetvar $i mood grumpy
if var($i mood) .in. grumpy sour vexed
emote glowers at everyone from behind the counter.
else
emote smiles pleasantly at the room.
endif
~
.in. is the odd one out: it works on text, and it asks whether the LEFT value appears anywhere inside the RIGHT value. Here the mob's stored mood is grumpy, and grumpy does appear in the list grumpy sour vexed, so the answer is yes. This makes .in. perfect for is-it-one-of-these checks - the right side is just a list of acceptable words. It ignores capitals. Mind the direction: the small thing goes on the left, the big thing it might be inside goes on the right. Writing it the other way round asks whether your whole word list fits inside one word, which is almost never yes.
Where The Comparison Goes, And Other Fine Print
The comparison sits after the function's closing parenthesis, as in every example above: level($n) >= 20. CoffeeMUD also allows the comparison INSIDE the parentheses - level($n >= 20) - and this engine accepts both forms, so scripts pasted from CoffeeMUD's own guide behave the same here. Use the outside form in new work; it is easier to read.
The right-hand side of a comparison is the rest of the line, so it can be several words: if var($i stew) == oxtail soup compares against the whole phrase oxtail soup. You may wrap the right side in single or double quotes for clarity; the quotes are stripped before comparing. The right side is also substituted, so dollar codes work there: if var($i lastvisitor) == $N asks whether the stored name matches the current visitor.
When BOTH sides look like whole numbers, the engine compares them as numbers. Otherwise it compares them as text. For == and != that is exactly what you want. For the ordering operators it is usually not: > on two words compares them alphabetically, capitals and all, which is rarely the question you meant to ask. Keep >, <, >=, and <= for numbers.
Combining Conditions: and, or, not
One question is often not enough. Is this a player, AND are they strong enough? Is it night, OR is the visitor a rogue? You can chain conditions on one if line with three joining words:
and - Yes only when both sides are yes.
or - Yes when either side is yes, or both.
not - Flips the condition that follows it.
First, and:
GREET_PROG 100
if ispc($n) and level($n) > 0
say A living, breathing adventurer with some training. Welcome, $N.
else
say Hmm. Not quite what I hoped the wind would blow in.
endif
~
Both questions must come up yes for the welcome to fire. If either one is no - not a player, or somehow no training - the else half runs instead.
Next, or:
GREET_PROG 100
if rand(0) or ispc($n)
say Either my dice came up sixes or you are real. Welcome either way.
endif
~
Only one side needs to be yes. Here the first side is a deliberate joke: rand(0) is a zero percent chance, which is always no - but the second side is yes for any player, and one yes is all or needs. Two dice-roll facts worth memorising while we are here: rand(0) never fires and rand(100) always fires, which makes them handy for temporarily forcing a branch on or off while you are testing a script.
Finally, not:
GREET_PROG 100
if ispc($n) not isfight($n)
say You come in peace, so peace you shall have.
endif
if !isfight($n)
emote sets the crossbow back under the counter.
endif
~
not sits between two conditions and flips the one after it: the line reads as a player, and NOT fighting. You can also write it out as and not or or not, which mean exactly what they say. To flip the FIRST condition on the line - or the only one - put an exclamation mark directly in front of it, as the second decision above does: !isfight($n) is yes when $n is not fighting.
Two rules about long chains. First, the chain is worked out strictly left to right, one join at a time, carrying a running yes-or-no along: a and b or c means the result of a-and-b, then joined with c by or. There is no invisible precedence to remember, but a long mixed chain of and and or is still easy to misread. Second, you cannot group conditions with parentheses the way mathematics does - parentheses belong to functions here. When a combination gets too clever to read at a glance, do not fight the line: split it into nested decisions, which brings us to the next section.
Nesting: Decisions Inside Decisions
Any line inside an if can itself be another if. The inner decision only gets asked at all when the outer one already came up yes:
GREET_PROG 100
if ispc($n)
if level($n) >= 1
say You passed both tests, the outer and the inner.
else
say You are real, but you have no training at all.
endif
else
say Creatures never even reach the inner test.
endif
~
Follow a player through it: the outer question, is this a player, comes up yes, so the engine steps inside and asks the inner question, is their level at least one. Yes again, so the first say runs. A wandering mob would fail the outer question and jump straight to the outer else - the inner question is never even asked.
Each endif closes the NEAREST unclosed if above it, and each else belongs to that same nearest if. This is exactly why the indentation convention matters: push each level in by four more spaces and the pairing is visible at a glance. Nesting can go as deep as you like, but if you find yourself four or five levels deep, stop and consider whether a switch or a rethink would say it more clearly.
switch: One Question, Many Answers
An if splits the world in two. Sometimes the world splits further than that: a drink order might be ale, cider, wine, or nonsense, and you want a different response to each. You could chain if/else four deep, but there is a cleaner tool made for exactly this shape:
switch <value> - The value to examine, usually a $ substitution.
case <answer> - Runs when the value matches this answer.
break - Marks the end of that case's lines.
default - Runs when no case matched. Optional.
endswitch - Closes the whole structure. Required.
Here it is working:
GREET_PROG 100
mpsetvar $i order cider
switch $<$i order>
case ale
say One ale, coming up.
break
case cider
say One cider, coming up.
break
default
say We only have water today.
endswitch
~
The switch line works out its value once - here $<$i order> reads the stored order, which is cider - and then walks down the cases looking for the first one whose answer matches. The ale case does not match. The cider case does, so its lines run, and then the script continues after endswitch. If nothing had matched, the default lines would have run instead.
The rules, spelled out: matching ignores capitals, and a case answer can be several words long, like case dark ale. Only ONE case ever runs - the first that matches - after which the engine leaves the switch, so there is no falling through from one case into the next like some programming languages have. That makes the break at the end of each case technically optional, but write it anyway: it marks the end of the case for human readers, and it matches what the same word does in loops. The switch value is usually a substitution - a stored variable as here, or a function result like switch $%class($n)%, which you will see again in the fortune teller at the end of this chapter. One limitation to know: a case matches a whole value exactly. It cannot express a range like 10 to 20 - ranges are if territory, with >= and <=.
The Same Problem, Solved Both Ways
To see when each tool earns its keep, here is one problem - a cook announcing the stew of the day - solved first with chained decisions, then with a switch. First the if chain:
GREET_PROG 100
mpsetvar $i stew rabbit
if var($i stew) == rabbit
say Today it is rabbit stew, thick and brown.
else
if var($i stew) == turnip
say Today it is turnip stew. My apologies in advance.
else
say Today the pot holds a mystery even to me.
endif
endif
~
And now the same behavior as a switch:
GREET_PROG 100
mpsetvar $i stew rabbit
switch $<$i stew>
case rabbit
say Today it is rabbit stew, thick and brown.
break
case turnip
say Today it is turnip stew. My apologies in advance.
break
default
say Today the pot holds a mystery even to me.
endswitch
~
Both scripts do exactly the same thing. But look at the shape of them. The chain version asks the same question twice, nests a level deeper for every extra stew, and needs two endif words that you must keep matched. The switch version reads like a menu. The working rule: two or three branches, or branches that test DIFFERENT questions, or ranges - use if. One value with many possible exact answers - use switch.
for: Counting Loops
Now for repetition. A for loop runs the same lines a fixed number of times, counting as it goes:
for $1 = 1 to 5 - Count from 1 to 5, one lap each.
next - Closes the loop body. Required.
The $1 is one of the ten temporary slots, $0 through $9, that every script run carries. The loop stores the current count in the slot you name, and each lap you can use that slot anywhere in the body, in text or in conditions. Watch it count:
GREET_PROG 100
say Watch me count my copper pots.
for $1 = 1 to 3
say Pot number $1 is accounted for.
next
say All pots present and correct.
~
The body runs three times. On the first lap $1 holds 1, so the mob says Pot number 1; on the second lap it holds 2; on the third, 3. Then the loop is done and the script carries on after next with the closing line. Two details: use a DIGIT slot as the counter, always - the engine accepts a named counter in the header but there is no way to read a named one back in your text, so it is useless in practice - and know that the slots reset every time a trigger fires, so a count never leaks from one firing into the next. If you need a number that survives between firings, store it with mpsetvar instead; the while example below does exactly that.
When the first number is bigger than the second, the loop counts DOWN instead, which is made for countdowns:
GREET_PROG 100
for $1 = 3 to 1
say Launch in $1...
next
mpecho The kettle rocket sputters, tips over, and goes out.
~
Both ends of the count can be substitutions rather than plain numbers - for $1 = 1 to $%level($n)% counts once per level of the visitor. Be sensible with sizes: as the safety nets section explains, a single loop stops itself after 2000 laps and a whole trigger run is capped at 4000 script steps, so a loop that says something 500 times will be cut off long before the end - and will deafen the room long before that.
while: Loops That Test A Condition
A for loop knows in advance how many laps it will run. A while loop does not: it keeps going as long as a condition keeps answering yes, checking again before every lap:
while <condition> - Test. On yes, run the body and test again.
endwhile - Closes the loop body. Required.
The condition is written exactly like an if condition - same functions, same operators, same joining words. And that gives while its one golden rule: something in the body must CHANGE the thing the condition tests, because if nothing changes, the condition will answer yes forever. Here is the classic shape, a counter stored on the mob that the body counts down:
GREET_PROG 100
mpsetvar $i bottles 3
while var($i bottles) > 0
say $<$i bottles> green bottles hang on the wall.
mpsetvar $i bottles $%math($<$i bottles> - 1)%
endwhile
say And now there are none at all.
~
Walk through it slowly, because every piece matters. Before the loop, the mob stores the number 3 under the name bottles. The while tests: is the stored value greater than zero? 3 is, so the body runs. The first body line speaks the current count - $<$i bottles> drops the stored value straight into the sentence. The second body line is the one that keeps us honest: $%math($<$i bottles> - 1)% computes the stored value minus one, and mpsetvar stores the result back. So the loop sings 3, then 2, then 1, and when the test runs a fourth time the value is 0, zero is not greater than zero, the answer is no, and the script moves on past endwhile to the final line. Delete the mpsetvar line and the count would never fall - the engine's loop cap would stop the script for you (see the safety nets section), but the room would get several hundred verses first.
Small print: mpwhile is an accepted alias for while, and done is an accepted alias for endwhile - you will meet both in imported CoffeeMUD scripts.
break: Leaving A Loop Early
Sometimes you find what you were looking for on lap three of ten and there is no point finishing. break jumps out of the nearest enclosing loop at once and carries on after it:
GREET_PROG 100
for $1 = 1 to 10
say Tasting stew pot number $1.
if $1 > 2
say This pot is perfect. No need to taste the rest.
break
endif
next
say The taste test is over.
~
The loop is written for ten laps, but on the third lap the counter passes 2, the if fires, and break ends the loop on the spot - pots four through ten are never tasted, and the script continues with the line after next. Notice the counter being used inside a condition; the slot is just a value like any other.
break always leaves the NEAREST thing that can be left: in a loop it leaves that loop; inside a case it ends the case, as you saw in the switch section; in a loop nested inside another loop it leaves only the inner one. On a line where there is no loop or case around it at all, it simply stops the script, like return.
return: Stopping The Script
return ends the whole prog block immediately, wherever it appears. Its everyday use is the early exit: deal with the case you do NOT want at the top, leave, and then write the main flow underneath without wrapping all of it in an else:
GREET_PROG 100
if isfight($n)
say Come back when you are not bleeding on my floor.
return
endif
say Welcome to the quietest shop in the city.
emote gestures proudly at the empty shelves.
~
A visitor in combat gets one line and the script stops dead - the welcome never runs. A peaceful visitor fails the if, skips the early exit, and gets the full greeting. This guard-then-continue shape keeps long scripts flat and readable, and it is worth stealing for anything with preconditions: wrong class, wrong level, wrong time of day - say so, then return.
One more thing return can do: carry an answer. Written as return <value> inside a FUNCTION_PROG - the named subroutine blocks you call with mpcallfunc - the value becomes the result the caller receives. That belongs to the commands chapter; here, just know the word can take a value.
Taking Your Time: mpsleep and mpalarm
Scripts normally run their whole block in a single instant - the player sees every line arrive at once. Real characters pause, fumble, think. mpsleep <seconds> puts a pause right in the middle of your script: everything above it runs now, then the script itself goes to sleep, and everything below it runs when the time is up:
GREET_PROG 100
say Give me a moment to find my spectacles.
mpsleep 2
say Ah, there they are. Now, how can I help you, $N?
~
The visitor hears the first line, two real seconds pass, and then the second line arrives. The rules: the delay is in whole seconds, and anything less than one becomes one. Only the script sleeps - the mob keeps breathing, fighting, and answering OTHER triggers while it waits, and each firing of a trigger is its own separate run, so a second visitor arriving mid-pause starts their own copy from the top. The world can also change during the pause: the visitor may have walked out before the second half runs, though the dollar codes still remember who they are. mpwait is an accepted alias.
You can chain several sleeps in a row to pace out a monologue, and a sleep inside an if or a case works exactly as you would hope. The one place mpsleep must NOT go is inside a for or while body. Sleeping ends the loop: when the pause finishes, the script carries on AFTER the loop, and the remaining laps are abandoned. A singing countdown with a pause between verses therefore cannot be built as a loop around a sleep - unroll it instead: verse, sleep, verse, sleep, verse, written out in a straight line.
For a single delayed action there is a lighter tool: mpalarm <seconds> <command> schedules one command to run later and lets the rest of the script continue immediately:
GREET_PROG 100
say The kettle is on. It will whistle when it is ready.
mpalarm 3 mpecho The kettle lets out a piercing whistle.
~
The say happens at once; three seconds later the room hears the whistle, long after the script itself has finished. Rule of thumb: pausing a SEQUENCE, use mpsleep; scheduling one future EVENT, use mpalarm. mpbeacon is an alias of mpalarm.
Common Mistakes And Their Fixes
Every one of these comes from a real builder's real afternoon. Check this list first when a script misbehaves.
1. The missing endif. Symptom: lines near the bottom of a block only run sometimes, though they sit outside any decision you remember writing. Cause: an unclosed if swallows everything below it. Fix: count your if and endif words; the totals must match. Indentation makes the orphan easy to spot.
2. The missing tilde. Symptom: your second PROG block never fires at all, and the mob sometimes visibly tries to run its header as a command. Cause: without the ~ line, the next block's header is read as just another command line of the first block. Fix: every block ends with a line containing only a tilde.
3. A question without parentheses. Symptom: an if that should sometimes be no is always yes. Cause: if isnight - without parentheses the engine does not treat the word as a function; it is just a piece of text, and non-empty text counts as yes. Fix: functions always get parentheses, even empty ones: if isnight().
4. .in. written backwards. Symptom: an is-it-one-of-these check never fires. Cause: the operator asks whether the LEFT value appears inside the RIGHT one, and the word list ended up on the left. Fix: small thing on the left, list on the right: if class($n) .in. mage necromancer.
5. The while that never changes. Symptom: a burst of a few hundred repeats, then the script stops mid-thought. Cause: nothing in the body changes what the condition tests, so it is an infinite loop; the engine's loop cap and step budget cut it off. Fix: make the body move the condition toward no, like the bottle-count example - and check /log/script_runaway, where every cut-off script is recorded.
6. mpsleep inside a loop. Symptom: the first lap runs, the pause happens, and the loop never continues. Cause: sleeping abandons the remaining laps by design; the script resumes after the loop. Fix: unroll short sequences into straight-line verse-sleep-verse, or use mpalarm for the delayed parts.
7. Expecting $1 to remember. Symptom: a count kept in a digit slot is empty next time the trigger fires. Cause: the ten slots are scratch paper for one run and reset every firing. Fix: anything that must survive between firings goes in a stored variable with mpsetvar, read back with var().
8. Grouping conditions with parentheses. Symptom: an if line with something like (a or b) and c behaves strangely. Cause: parentheses belong to functions in this language; they do not group logic, and chains simply run left to right. Fix: restructure as nested if decisions, which say the same thing unambiguously.
9. Waiting for fall-through in switch. Symptom: you expected two cases to both run for one value. Cause: only the first matching case ever runs; there is no fall-through here. Fix: if two answers share lines, give each case its own copy, or handle the shared part after endswitch.
10. Ordering operators on words. Symptom: > or < between two words gives baffling answers. Cause: words compare alphabetically, capitals included. Fix: keep the ordering operators for numbers; for words you want ==, !=, or .in..
Worked Example: The Fortune Teller
Time to put the whole chapter in one mob. Madame Sybil reads every visitor's fortune, and her reading branches four separate ways: on the visitor's class with a switch, on their level with an at-least comparison, on the game clock with a boolean function, and on pure chance with a dice roll. Every visitor hears five lines, and almost no two visitors hear the same five:
GREET_PROG 100
emote peers into a cloudy crystal ball as $n approaches.
say The mists part for you, $N. Let us see what fate holds.
switch $%class($n)%
case mage
say A weaver of raw elements. The tower already watches your progress.
break
case warrior
say A bearer of steel. Fortune loves the bold and the well armored.
break
case rogue
say Quick fingers and quicker feet. Keep them out of my pockets.
break
default
say Your calling is written in a hand I read only dimly.
endswitch
if level($n) >= 20
say You have walked far already. The road ahead only grows steeper.
else
say You are young on the road yet. Small steps still carry far.
endif
if isnight()
say Night readings are the truest kind. The stars lean close to listen.
else
say Daylight blurs the signs. Return after dark for a clearer telling.
endif
if rand(50)
say The coin of fate lands bright side up. Luck rides with you today.
else
say The coin of fate lands dark side up. Watch your step today.
endif
emote leans back from the crystal ball, looking drained.
~
Read it as four independent stages, because that is what it is. The opening emote and greeting always run. Stage one is a switch on $%class($n)% - the function result dropped in as the switch value - with a case per class she recognises and a default so that every other class still gets a line; without the default, a cleric would get silence here. Stage two is the at-least boundary from the operators section: level twenty exactly counts as having walked far, because >= includes the boundary. Stage three calls isnight(), parentheses and all, and gives her a reason for players to come back at a different hour. Stage four is rand(50), a fair coin, so even two identical mages at the same hour can walk away with opposite luck. The closing emote always runs, bracketing the reading.
Things worth stealing from Sybil: the stages are flat, not nested inside one another, because the four questions are independent - nesting would wrongly make the later readings depend on the earlier answers. Every if has an else and the switch has a default, so no visitor ever hits silence. And the whole performance is one block with one trigger, so a single mudprog sybil test GREET_PROG exercises all of it. Natural extensions once you have the variables chapter under your belt: remember each visitor with mpsetvar and refuse a second reading the same day, or pace the reading out with an mpsleep 2 between stages - safe here, because none of the pauses sit inside a loop.
The Safety Nets
You cannot hang the mud with a bad loop, so experiment freely. Every trigger run has a budget of 4000 script steps, each single loop stops itself after 2000 laps, and a script that hits the ceiling simply stops and writes a note to /log/script_runaway naming the mob and the trigger - read that file when a script seems to end early. A broken condition never crashes the mob either: a question the engine cannot make sense of just answers no.
Testing What You Build
Fire a specific block on demand with mudprog <target> test <TRIGGER> - for everything in this chapter, mudprog <target> test GREET_PROG with you as the visitor. To experiment with a loop or a condition without attaching anything to a mob, scripttest runs raw script lines on yourself; see help scripttest. One practical warning: some client programs mangle dollar signs typed on a command line, so if a one-line test behaves strangely, put the script in the editor with mudprog <target> edit or use scripttest runfile with a file instead - files carry your dollar codes untouched.
Everything you have scripted so far reacts to things that have ALREADY happened. A player walked in, so GREET_PROG fired. Someone spoke, so SPEECH_PROG fired. The deed was done and your script commented on it afterwards. This chapter is about the other half of the engine: the message bus, which lets a script step in BEFORE an action happens and either stop it cold or quietly watch it go by. With the bus you can build a relic no one can steal, a door that turns strangers away, a shrine where swords refuse to swing, a zone where magic simply fails, and a customs officer who writes down everything that changes hands in his room. No LPC, no recompiling, just a script attached with the mudprog command like any other.
If you have not read help mudprog yet, do that first. This chapter assumes you know what a PROG block is, that blocks end with a line holding only a tilde, and that dollar codes like $n and $N are replaced with live values when a script runs.
Actions As Messages
Inside the game engine, most physical actions are not a single event; they are a little conversation. When a player types get sword, the engine does not just teleport the sword into their hands. First it composes a message that says, in effect, "so-and-so is about to GET this sword", and it shows that message around before anything moves. Only if nobody objects does the sword actually change hands, and at that moment a second message goes around saying "so-and-so IS getting the sword", which observers can react to.
That two-step conversation is the message bus, and each kind of action on it has a short uppercase name called its message code. Picking something up is GET. Dropping is DROP. Wearing armor or wielding a weapon is WEAR. Starting a fight is ATTACK. Walking into a room is ENTER. There are eighteen codes in all, listed a little further down.
Your scripts join this conversation through two special triggers:
CNCLMSG_PROG - the cancel pass. Runs BEFORE the action commits. If your
block matches, the action is cancelled and your script
runs in its place.
EXECMSG_PROG - the observe pass. Runs as the action happens. Your block
sees it but cannot stop it.
The names come from CoffeeMUD, whose scripting language this engine speaks: CNCL as in cancel-message, EXEC as in execute-message. Scripts written from CoffeeMUD's own documentation drop straight in.
The Two Passes
Think of every bus action as passing two checkpoints, in this order:
1. The cancel pass. Before anything changes, the engine walks every
scripted object near the action and asks each one: do you have a
CNCLMSG_PROG block matching this message? The first object that does
wins. Its matching block, or blocks, run immediately, and the action
is thrown away. The sword never moves. The door never opens. The
spell never casts. Nothing further is consulted.
2. The observe pass. If nothing cancelled, the action goes through
normally, with all its usual messages, and then every scripted object
nearby that has a matching EXECMSG_PROG block gets to run it. By the
time an observer runs, the deed is done; an observer can comment,
count, reward, or scheme, but it cannot undo.
The single most important sentence in this chapter is this: a matching CNCLMSG_PROG block does not merely forbid the action, it REPLACES it. Your script runs instead of the action. Whatever your block prints is the only thing anyone sees, because the normal "You get the sword" style messages belong to the action that no longer happens. If your block body is empty, the player types a command and the world says nothing at all, which feels like a bug. So the first rule of cancel blocks: always narrate the refusal. Tell the player what stopped them and, ideally, why.
Why is it built this way, rather than a simple yes-or-no flag? Because a flat "no" is dead air. By making your script the replacement behavior, the engine guarantees that when you seal a door, YOU decide what the sealed door looks, sounds, and feels like. The refusal becomes content.
One safety promise before we go further: the bus can never break the game. Every script run on the bus is wrapped in error protection, and if a script crashes for any reason the engine treats that as permission. A broken cancel block lets the action through rather than locking it shut forever. And in a room with no scripted objects at all, the whole check costs almost nothing, so you never need to worry that using these triggers slows the mud down.
Your First Cancel Block
Here is the smallest possible cancel script. Attach it to any object, a stone on a pedestal say, and while it is attached, every bus action taken near that stone is refused:
CNCLMSG_PROG ALL
mpecho A ripple of grey light washes out from the warden stone.
mpechoat $n The warden stone refuses you. Nothing here obeys your hands.
~
Read it line by line:
The header is the trigger name, CNCLMSG_PROG, followed by a code-spec. The code-spec says WHICH message codes this block should intercept. The word ALL is the wildcard: intercept every code. A completely empty header behaves the same as ALL, but writing ALL out loud is kinder to the next builder who reads your script.
The body is ordinary script. mpecho prints a line to everyone in the room. mpechoat prints a line to one person only, and $n is the person who tried the action, so the second line is a private message to whoever just got refused. Every command and function you know from the other chapters works inside a cancel block: if, switch, mpsetvar, say, emote, all of it.
The tilde ends the block, as always.
While that stone sits in a room, try to pick something up, drop something, open a container, eat a ration, start a fight, or walk out, and instead of the action you get the grey ripple and the refusal. That is the whole mechanism. Everything else in this chapter is about narrowing it down: which codes, which items, which people.
To attach it, stand in the room with your target and use the line editor:
mudprog stone edit
then type the lines, the tilde, and a single dot to save. See help mudprog for the other attachment forms.
Your First Observer
The observe pass looks identical, only with EXECMSG_PROG in the header:
EXECMSG_PROG ALL
mpecho A recording quill scratches a new line into a floating ledger.
~
Attach that to an object in a room and every bus action that actually goes through, every pickup, drop, wear, open, and so on, is followed by the quill scratching. Note the difference in feel: the cancel block above SPOKE INSTEAD of the action; this observer speaks AFTER it, alongside the normal messages. Observers are for flavor, bookkeeping, and consequences, not for prevention.
The code-spec is one word naming the message code to intercept, or ALL for everything. The mask, everything after the code-spec, narrows the match by the text of the message, usually the name of the item involved. Leave the mask off, or write ALL, to match every message of that code. Both parts are case-insensitive; GET, get, and Get are the same word to the engine.
So, reading a few headers aloud:
CNCLMSG_PROG ALL - cancel every bus action
CNCLMSG_PROG GET - cancel every pickup
CNCLMSG_PROG GET ALL - the same, spelled out
CNCLMSG_PROG GET relic - cancel pickups whose message text contains
the word relic
EXECMSG_PROG DROP ALL - observe every drop
CNCLMSG_PROG CAST fireball - cancel casting when the spell name
contains fireball
Important: the header slot of a bus trigger holds a code-spec and a text mask, and nothing else. It does NOT take a percentage chance and it does NOT take a zapper mask, both of which belong to ordinary triggers like GREET_PROG. If you want a cancel that only applies to certain people, you write that logic INSIDE the block body with an if, which we will do shortly.
The Message Codes, One By One
These are the codes that flow through the cancel pass. For each: what is about to happen, and what the dollar codes hold when your block runs. In every case $n is the person acting, $i is the scripted object running your block, and $g is the message text in lower case ($G keeps its original case).
GET - someone is about to pick an item up.
$o the item, $t the taker, $g the item's name.
DROP - someone is about to drop an item.
$o the item, $g its name.
PUT - someone is about to put an item into a container.
$o the item, $t the container, $g the item's name.
GIVE - someone is about to hand an item to someone else.
$o the item, $t the receiver, $g the item's name.
WEAR - someone is about to wear armor or wield a weapon.
$o the item, $g its name.
REMOVE - someone is about to take off or unwield an equipped item.
$o the item, $g its name.
OPEN - a door or container is about to be opened.
$o the door or container, $g its name.
CLOSE - a door or container is about to be closed. Same shape.
LOCK - a door or container is about to be locked. Same shape.
UNLOCK - a door or container is about to be unlocked. Same shape.
EAT - someone is about to eat food. $o the food, $g its name.
DRINK - someone is about to drink. $o the drink, $g its name.
BUY - a player is about to buy from a vendor. $t the vendor,
$g the words the player typed for the item.
SELL - a player is about to sell to a vendor. $t the vendor,
$o the first item offered, $g its name.
CAST - someone is about to use a class skill or cast a spell.
$g the skill or spell name. This is any ability, not only
mage spells; a warrior's skills pass through CAST too.
ENTER - a creature is about to walk into a room. $g is EMPTY.
LEAVE - a creature is about to walk out of a room. $g is EMPTY.
ATTACK - combat is about to begin. $n the attacker, $t the intended
victim, $g the victim's name. This fires both when a player
types the attack command and when an aggressive creature
tries to start a fight, so an ATTACK veto calms monsters as
well as players.
Two of those deserve a second look. ENTER and LEAVE carry no message text at all, so a text mask on them can never match; always write them with a bare code or ALL, and do any narrowing inside the body. Also, a creature being pulled along automatically because it is following its group leader is not re-checked; the leader was checked, and the group moves with them.
The observe pass sees a slightly different code list, because the engine announces some completed actions under a different name than it asked permission for. The codes that actually reach EXECMSG_PROG observers are:
GET DROP PUT WEAR REMOVE OPEN CLOSE LOCK UNLOCK
GIVING - a completed hand-over (the cancel-pass name was GIVE)
CONSUME - completed eating OR drinking (the cancel-pass names were
EAT and DRINK)
CASTING - a completed skill or spell use (an observer header written
as CAST still matches this, see aliases below)
Notice what is missing: ENTER, LEAVE, ATTACK, BUY, and SELL do not fan out on the observe pass. That is not an oversight; the game already has richer triggers for observing those moments. Watch arrivals with GREET_PROG, watch fights with FIGHT_PROG, and watch a vendor's trade with BUY_PROG and SELL_PROG on the vendor itself, all covered in the triggers chapter. The bus observe pass exists for the item-handling verbs where no dedicated observer trigger reaches bystanders.
Aliases
CoffeeMUD scripts use several alternate names for these codes, and the engine accepts them all in the code-spec position. Each line below reads: when this code fires, these header words match it.
EAT matches headers EAT or CONSUME
DRINK matches headers DRINK or CONSUME
ATTACK matches headers ATTACK, FIGHT, or KILL
CAST matches headers CAST, SPELL, or CASTING
CASTING matches headers CASTING, CAST, or SPELL
ENTER matches headers ENTER or ARRIVE
LEAVE matches headers LEAVE, EXIT, or DEPART
CONSUME matches header CONSUME (the completed eat-or-drink)
GIVING matches header GIVING
So CONSUME is the convenient way to write one cancel block that covers both food and drink, FIGHT reads naturally for combat vetoes, and a header written CAST works on both passes even though the observe-side code is technically CASTING. The pair RIDE and MOUNT is also recognised as aliases of each other, reserved for the mount system. Note one asymmetry honestly: a completed hand-over announces itself as GIVING, and a header written GIVE will NOT match it; use GIVING (or ALL) for observers of gift-giving, and GIVE for the cancel side.
Code-Spec Prefixes
CoffeeMUD's own guide decorates code-specs with punctuation: a less-than sign, a greater-than sign, a question mark, or a letter-equals prefix such as S= or T= or O=. In CoffeeMUD those choose which copy of a message you inspect, the source's copy, the target's copy, or the bystanders' copy. On this mud there is only one check per action, so all of these forms are accepted and all of them mean exactly the same as the plain code:
GET <GET >GET ?GET S=GET T=GET O=GET <S=GET
Every one of those intercepts a pickup. The prefixes exist purely so that scripts pasted from CoffeeMUD documentation work unchanged. When writing new scripts, use the plain code and save your eyes.
Keyword Masks
Everything after the code-spec is the mask, matched against the message text, which for most codes is the name of the item involved. The rules:
(nothing) or ALL - match every message of this code.
word word word - match if ANY listed word appears in the message
text. Case does not matter.
p some exact phrase - the letter p, then a phrase; match only if the
WHOLE phrase appears in the message text.
Matching is by substring, which is generous and occasionally too generous: the mask word ring matches ring, but it also matches earring and drinking horn, because those names contain the letters r-i-n-g in a row. Prefer distinctive words. If an item's name is iron band, mask on band or iron, not on a fragment that other items share. The p form exists for exactly this problem: p iron band matches only messages containing that full phrase.
What text you are masking against, by code: for the item-handling codes it is the item's key name, the name a builder gave it with SetKeyName, not the fancy colored short description. For BUY it is whatever words the player typed to name their purchase. For CAST it is the skill or spell name, which makes spell-specific bans one header long. For ATTACK it is the victim's key name, so you can protect one creature by name. For ENTER and LEAVE it is empty, so as said above, masks there never match; leave them bare.
A worked pair to make it concrete. This vetoes taking anything whose name contains relic, and nothing else:
CNCLMSG_PROG GET relic
And this observes any drop at all:
EXECMSG_PROG DROP ALL
Who Gets Asked: The Scope Of The Bus
When an action fires, which scripted objects are consulted? For both passes, the answer is: everything near the action that carries a script. Precisely, and in this order for the cancel pass:
1. The primary object of the event: the item being taken, the door
being opened, the vendor being traded with, the destination room
being entered, the victim being attacked.
2. The room the actor is standing in.
3. Every object in that room, creature or item, that carries a script.
4. The actor themselves, if scripted.
The first scripted object holding a matching CNCLMSG_PROG block wins: its matching blocks run, the action dies, and nothing later in the list is consulted. So the item itself speaks before the room, the room before the bystanders, the bystanders before the actor. If ONE object has several matching blocks, they all run, top to bottom, in script order.
Two useful corollaries. First, a cancel script does not need to live on the thing being protected: a guard MOB can veto every GET in his room, and a scripted ROOM can veto attacks inside itself. Second, an item riding inside someone's inventory is only consulted when it is itself the subject of the event; a cursed ring can refuse to be removed, dropped, or given away, because in those events it is the primary object, but it cannot veto its owner's unrelated actions from inside a backpack, because carried items are not part of the room-level scope.
ENTER is the special case worth spelling out: the primary object is the DESTINATION room, and the scope includes both that room's occupants and the room you are leaving from. A doorkeeper can therefore stand on either side of the door and still say no.
Worked Example: The Unstealable Relic
The classic. A museum piece that no one, player or monster, can pick up. The script goes on the relic itself, so the veto travels with the item wherever it is placed:
CNCLMSG_PROG GET ALL
mpechoat $n The relic flares white-hot and will not leave its plinth.
mpechoaround $n $N grabs at the relic and snatches back a scorched hand.
~
The header: intercept GET, any message text, which on an item's own script means any attempt to take THIS item, since the item is only in scope for its own events. The body sends two different lines, one to the person burned, one to everyone watching, which is the polish that makes a veto feel like world-building instead of an error message. mpechoaround prints to the room EXCEPT the named person, so nobody gets both lines.
Why not just make the item too heavy to lift, or flag it untakeable in the object file? You could, but then the refusal is a stock message. The bus version lets the relic behave: it could whisper to thieves, brand repeat offenders with mpsetvar, or summon the curator on the third attempt. Once the veto is a script, it can grow.
Worked Example: A Door That Says No
Movement vetoes go on rooms. Put this on a room and nobody walks into it; the ward turns them back at the threshold:
CNCLMSG_PROG ENTER
mpechoat $n A ward of pale light hardens across the doorway before you.
mpechoaround $n $N walks into a shimmering ward and staggers back.
~
Remember the two ENTER rules: the message text is empty, so the header stays bare, and the scope is generous, so this script could equally sit on a doorkeeper standing inside the protected room.
Now, the members-only version. Your first instinct will be to write an if that lets members through, and here you must understand the engine honestly: a CNCLMSG_PROG block cancels the action WHENEVER it runs, no matter what its body does. There is no allow command inside the body; the if can only choose what the refusal looks like. So a conditional gate is built the other way around: the block refuses everyone, and for the people you want inside, the body performs the entry itself, by force, using mptransfer $n /path/to/the/inner/room in the allowed branch. The transfer command moves people directly and does not pass through the bus, so it does not re-trigger your own veto. The shape, in prose: if the visitor qualifies, echo a welcome and mptransfer them to the inner room; else, echo the refusal. Test such doors carefully with a non-staff character, and make sure the transfer path is the real file path of the destination room.
For simple cases you often do not need the transfer trick at all: put the veto on the room and put the check in the body only to VARY THE MESSAGE, or better, reconsider whether a locked door with a key item tells the story more simply. The bus is a scalpel; not every door needs it.
Worked Example: The Pacifist Shrine
Attach to a shrine room, or to the shrine's guardian statue, and no fight can start there:
CNCLMSG_PROG ATTACK ALL
mpechoat $n A serene weight settles over your arms. No violence here.
mpecho The shrine bells give a single low chime.
~
Because ATTACK fires for aggressive creatures as well as for players, this shrine really is safe ground: a monster that wanders in and tries to pounce is refused by the same block, and the private line lands harmlessly on the monster. Note what this does NOT do: it stops fights from STARTING. A fight that began outside and spilled in is already running; ATTACK is the moment of initiation, not every blow. Design shrines so that fleeing into one is the story you want.
If you would rather protect one creature than one place, put the same block on the creature; the victim is the primary object of ATTACK, so its own script is consulted first, everywhere it goes. And since the message text carries the victim's name, a room-level script could even protect just one resident by name with a mask like CNCLMSG_PROG ATTACK curator.
Worked Example: The No-Magic Zone
CAST covers every class ability, so this is a null-magic AND null-skill field. Attach to a room:
CNCLMSG_PROG CAST ALL
if level($n) >= 30
mpechoat $n The null field strains, then smothers your $g anyway.
else
mpechoat $n The words of your spell crumble to ash on your tongue.
endif
~
Here you finally see body logic inside a cancel: the if does not decide WHETHER to cancel, that decision was made the moment the block matched, but it decides what the player is told, and the veteran gets the more flattering line. Notice $g inside the body: for CAST it is the name of the skill being smothered, which lets the message name the very spell that failed.
One honesty note: when a class skill is vetoed, the skill system prints its own short failure line after your message, the standard cannot-do-that notice. Your text appears first; write it so the pair read naturally together.
To ban a single spell instead of all of them, use the mask:
CNCLMSG_PROG CAST fireball
mpechoat $n The dead air swallows your fireball before it can form.
~
If both blocks were on one object, a fireball cast would match both, and both would run, in order. All matching blocks on the winning object run; keep that in mind when stacking specific and general blocks together, and put the general one last if you want the specific flavor to lead.
Worked Example: The Customs Officer
An observer this time. Attach to a mob who watches goods move through his room. Nothing is prevented; everything is noticed:
EXECMSG_PROG GET ALL
mpecho The customs officer notes down exactly what $N just picked up.
~
EXECMSG_PROG DROP ALL
mpecho The customs officer prods the discarded $g with his boot.
~
Two blocks, one script, one per code. When anyone in the room picks something up, the pickup happens normally, "You get the lantern" and all, and THEN the officer's line follows. Same for drops, where $g slips the dropped item's name into his reaction.
Observers are where bookkeeping lives. Swap the echoes for mpsetvar and the officer can count contraband; add an if on the item name and he reacts only to certain goods; add mpfaction and dropping stolen wares in front of him has consequences. Because observers can never break an action, you can attach them generously; the worst a buggy observer does is stay silent.
One scope nicety worth knowing: on the observe pass, non-living scripted objects in the room run their EXECMSG blocks too. A statue can watch. The richer per-event triggers like GET_PROG on bystanders fire only for living witnesses; EXECMSG is how furniture gets eyes.
Worked Example: The Cursed Armband
Cursed gear is a bundle of vetoes on the item itself. It goes ON freely, because there is no WEAR block, and then it will not come off, will not be dropped, and will not be handed away:
CNCLMSG_PROG REMOVE ALL
mpechoat $n The iron band tightens on your arm. It will not come off.
~
CNCLMSG_PROG DROP ALL
mpechoat $n Your fingers refuse to open. The band stays with you.
~
CNCLMSG_PROG GIVE ALL
mpechoat $n The band sears your palm the moment you try to hand it over.
~
Each block guards one escape route, and each has its own flavor line, so the curse feels alive rather than like three copies of "no". If you want the curse breakable, give the body an out: check a variable with var, or a title, or the presence of a blessed item with has, and in that branch echo the release and use mpjunk on the band itself. A curse with a key is a quest.
Zapper Masks
Now we leave the bus for a close cousin. Ordinary triggers, the GREET_PROG and FIGHT_PROG family, take a header argument that is usually a percent chance or a set of speech keywords. That same header slot can instead hold a zapper mask, another CoffeeMUD inheritance, which filters WHO is allowed to set the trigger off. The name is CoffeeMUD folklore; think of it as a doorman's checklist.
A zapper mask is a row of clauses. Each clause starts with a dash-word naming a property of the triggering person, followed by the values that qualify. The mask passes only if EVERY clause passes, and a clause passes if the person matches ANY of its values. So dashes are AND, and the values inside one clause are OR.
The clause types:
-class <names> - the person's class must be one of the names.
-race <names> - their race must be one of the names.
-level <spec> - a bare number means at-or-above that level; a form
like 30-40 means within that range inclusive. Give
several specs and any one qualifies.
-sex <values> - their gender, male or female.
-name <names> - their name; anything the person answers to counts,
so short names and full key names both work.
-deity <names> - the deity the person worships, if any.
-player - takes no values; the person must be a player.
-npc - takes no values; the person must be a creature.
Values may be written with a leading plus sign, CoffeeMUD style, and it is simply ignored: -class +mage +necromancer is the same as -class mage necromancer. Everything is case-insensitive. An unrecognised clause type is ignored rather than failing, so a pasted CoffeeMUD mask with exotic clauses degrades gracefully instead of going dead.
A shopkeeper who greets different visitors differently, all in one script:
GREET_PROG -player
say Flesh and blood. Ghosts never buy anything, $N.
~
GREET_PROG -class mage necromancer -level 20
say A seasoned wielder of the arts. The back shelf is open to you.
~
GREET_PROG -race elf dwarf
say The old peoples are always welcome at my fire.
~
GREET_PROG -level 30-40
say You carry yourself like the middle of a long story.
~
GREET_PROG -sex female
say Welcome, my lady. Mind the loose board by the door.
~
GREET_PROG -name aldric
say Aldric! The very man. Your order came in this morning.
~
GREET_PROG -deity sarethis
say I keep a candle lit for Sarethis too, friend.
~
GREET_PROG -npc
emote eyes the creature warily and says nothing.
~
When a level 25 elven mage walks in, the engine checks each GREET block independently, and every block whose mask passes fires: this visitor gets the flesh-and-blood line, the back-shelf line, and the old-peoples line. The second block shows the AND clearly: mage OR necromancer, AND level 20 or higher. A level 12 mage fails it; a level 40 warrior fails it too.
Three rules to keep straight. First, the header holds ONE thing: a percent chance, or keywords, or a zapper mask, never a combination, and the engine decides which you meant by looking at the first character, so a mask must start with its first dash-word. Second, the mask always filters the SOURCE of the trigger, the person who walked in, spoke, or struck, not the scripted mob itself. Third, zapper masks belong to ordinary triggers only. Writing one in a CNCLMSG_PROG or EXECMSG_PROG header does nothing useful, because that slot is parsed as a code-spec; person-filtering on the bus is done in the body, with conditions like if class($n) == mage choosing the response.
Text Masks: IMASK And REGMASK
Two more observer triggers watch not actions but TEXT: the lines of output that scroll past a creature. They are how you script a mob that reacts to what it hears and sees, at the level of raw sentences.
IMASK_PROG <text> - fires when a line produced by the scripted
object's OWN action contains the text. Plain
words, case-insensitive, substring match.
REGMASK_PROG <regex> - fires when ANY line the scripted object sees
matches the regular expression. This is every
line: speech, emotes, combat spam, arrivals.
The I in IMASK is best read as I-myself: it triggers on the echo of things the mob itself did, its own swings landing, its own spells fizzling, its own say lines. REGMASK is the wide net, everything the mob perceives. Color codes are stripped before matching, so write patterns against plain text. A REGMASK pattern is a real regular expression and is case-sensitive; if you do not know regular expressions, ordinary words work fine as patterns, they simply match themselves.
IMASK_PROG a mighty blow
say Ha! Felt that one land all the way up my arm!
~
REGMASK_PROG gold
emote perks up at the talk of money.
~
The first block: when this mob's own combat produces a line containing "a mighty blow", he crows about it. The second: whenever any line he sees contains "gold", someone counting coins, a shout about gold, a gilded item description, he perks up.
Now the one serious trap in this whole chapter, so read this twice. A REGMASK reaction that ITSELF produces text matching its own pattern will see its own output, match again, react again, and loop forever. Look at the example: the pattern is gold, and the reaction says money, not gold, precisely so the mob's own emote cannot re-trigger it. When you write a text mask, always reread the reaction line and make sure the pattern does not occur in it. The engine's step limits will eventually strangle a runaway, but a mob stuttering the same emote is embarrassing enough.
Inside a text-mask block, $g holds the matched line, so a reaction can quote or inspect what was seen, and the trigger context has no separate actor, the mob is reacting to its own perception.
CMDFAIL: Catching Commands That Go Nowhere
When a player types something the game does not understand, they normally get the stock error and that is that. CMDFAIL_PROG turns those dead ends into character moments: it fires on the room, and on scripted mobs in the room, whenever a player's command fails to resolve, with the failed command line riding in $g.
CMDFAIL_PROG all
say Whatever $g may be, nobody does it here.
~
CMDFAIL_PROG pray
say If it is prayer you want, the chapel is east of the square.
~
The header takes the same arguments as other ordinary triggers: all or a percent for how often, keywords to react only to certain failed commands, or a zapper mask to react only to certain people. The second block above uses a keyword: only failed commands containing pray, such as a hopeful pray to the sun in a mud with no such verb, get the helpful redirect. This is a lovely low-effort trick for guide NPCs: catch the commands new players WISH existed and answer them in character. Use it sparingly on busy rooms, though; a mob that pipes up at every typo becomes the town nuisance in about four minutes.
The player still receives the normal error message; your script's reaction appears alongside it, not instead of it. CMDFAIL is an observer, not a veto.
QUEST_TIME: The Ticking Clock
The last trigger in this chapter serves timed quests. When a quest is built with a time limit and a player accepts it, the engine starts a silent countdown and, once per minute, fires QUEST_TIME_PROG on every scripted object in the world that defines it. The header names which quest the block cares about and, optionally, which minute marks:
QUEST_TIME_PROG <quest id> <minutes...>
with the minutes counting DOWN, so 5 means five minutes remaining and 0 means the deadline itself. No minutes listed means fire on every pulse. When the block runs, $n is the player on the clock and $g holds the quest id and the minutes remaining, separated by a space.
QUEST_TIME_PROG cellar_rats 5
mpechoat $n The innkeeper's voice needles at you. Five minutes, no more.
~
QUEST_TIME_PROG cellar_rats 0
mpechoat $n Somewhere a door slams. The innkeeper's patience is spent.
~
Attach that to the innkeeper who gives the cellar_rats quest and the player gets a nudge at five minutes left and a knell at zero. Because this trigger is delivered world-wide, the innkeeper does NOT need to be in the player's room; mpechoat $n reaches the player wherever they are, which is exactly what a voice needling at the back of the mind should do. Use mpecho here only if you want the innkeeper's ROOM to hear him muttering instead.
The pulses stop on their own the moment the quest is completed or dropped, so a punctual player never hears the doom bell. The countdown itself, and what happens when time actually runs out, belong to the quest definition; QUEST_TIME_PROG is the narration layer on top.
A Combined Scene: The Reliquary Keeper
Everything above, in one script on one mob. The keeper of a relic vault: nothing may be taken, no blood may be spilled, no magic may be worked, yet offerings are welcome, and he answers even your fumbled commands in character.
CNCLMSG_PROG GET ALL
mpechoat $n The keeper's hand closes, immovable, around your wrist.
say The relics of this vault are not for taking, $N.
~
CNCLMSG_PROG ATTACK ALL
mpechoat $n Your anger drains away like water into sand.
say Not within these walls. Not ever.
~
CNCLMSG_PROG CAST ALL
mpechoat $n The keeper traces a sigil and your $g dies half-spoken.
~
EXECMSG_PROG DROP ALL
say Leaving offerings, are we? The vault accepts all gifts.
~
CMDFAIL_PROG all
say These halls answer to older words than $g, I fear.
~
Walk through it as the engine would. A visitor reaches for a relic: GET fires the cancel pass, the keeper is in scope as a bystander, his first block matches, the grab never happens, and instead the wrist is caught and the rule is spoken. A visitor draws steel: ATTACK, second block, sand. A visitor mutters an incantation: CAST, third block, and $g names the dying spell. But a visitor who DROPS something sees the drop go through normally, because the fourth block is an observer, not a veto, and the keeper blesses the offering after the fact. And a visitor who types nonsense gets the stock error plus the keeper's fifth block, apologising for the halls.
One mob, five blocks, three different mechanisms, and the room has a personality. That is the message bus used well: not walls of "you can't", but a place with opinions.
Testing Bus Scripts
The honest way to test a cancel or an observer is to perform the real action. Attach the script, then actually try to pick the relic up, walk through the door, or start the fight, with a second character or a willing victim where needed. The bus only speaks during real actions.
The mudprog <target> test CNCLMSG_PROG form does fire the trigger by hand, but be aware of two things. First, a hand-fired cancel block runs as an ordinary script, there is no action in flight, so nothing is actually cancelled; you are checking your body text, not the veto. Second, the hand-fired test supplies the word test as the message text, so a block whose header carries a code and mask, such as GET relic, will not match and the command may report that no block was found; that is the mask doing its job against the wrong message, not a broken script. Blocks headed ALL fire fine under the hand test.
For QUEST_TIME, build a short-limit test quest and accept it; for CMDFAIL, simply type gibberish in the mob's room; for the text masks, do the thing that produces the line. And when a veto seems not to work at all, mudprog <target> shows the parsed trigger list; if your CNCLMSG_PROG is not listed, the block never parsed, and the usual culprit is a missing tilde line.
Quick Reference
CNCLMSG_PROG <code> [mask] - cancel pass; block runs INSTEAD of the
action; always narrate the refusal.
EXECMSG_PROG <code> [mask] - observe pass; runs after the action;
cannot prevent anything.
Cancel codes: GET DROP PUT GIVE WEAR REMOVE OPEN CLOSE LOCK UNLOCK
EAT DRINK BUY SELL CAST ENTER LEAVE ATTACK
Observe codes: GET DROP PUT WEAR REMOVE OPEN CLOSE LOCK UNLOCK
GIVING CONSUME CASTING
Aliases: CONSUME for EAT and DRINK, FIGHT and KILL for ATTACK, SPELL
and CASTING for CAST, ARRIVE for ENTER, EXIT and DEPART for
LEAVE, RIDE and MOUNT for each other.
Prefixes: < > ? S= T= O= are accepted and ignored; ALL matches every
code; an empty header behaves like ALL.
Masks: keywords match any-word substring against the message text;
p phrase matches the whole phrase; ENTER and LEAVE carry no
text, so give them no mask.
Scope: item first, then room, then everything scripted in the room,
then the actor; first match cancels; all matching blocks on
the winning object run.
Zappers: -class -race -level -sex -name -deity -player -npc on
ordinary trigger headers; clauses AND, values OR; filters
the triggering person; not valid in bus headers.
IMASK_PROG <text> - own-action output contains text.
REGMASK_PROG <regex> - any seen line matches; never emit your own
pattern, or you loop.
CMDFAIL_PROG <args> - a player's command failed; failed line in $g.
QUEST_TIME_PROG <quest> <minutes> - countdown pulses; $n the player,
$g the quest id and minutes left; 0 is the
deadline.
Errors always count as permission: a broken script can never seal an action shut. If a veto misbehaves, clear it with mudprog <target> clear and the world returns to normal at once.
This chapter is a recipe book: fifteen complete, working MUDProg scripts you can paste onto a mob, an item, or a room and watch run, each followed by a walkthrough of every line and a set of variations to try. Nothing here assumes you have ever written a script before. If a term is unfamiliar, keep reading; every recipe re-explains what it uses, and the earlier recipes explain the most, so read them in order the first time through.
A MUDProg script is plain text stored on an object. It is made of PROG blocks. Each block starts with a trigger line, which names the game event that sets it off and an argument that filters when it fires. Then come the lines to run, one command per line, and then a line containing only a tilde to close the block. You attach a script with the mudprog command, and the engine reparses it the moment you save; there is nothing to compile and nothing to reboot.
How To Attach A Recipe
Stand in the room with your target and type mudprog <target> edit, where the target is the mob or item's name, or the word here for the room itself. The editor takes lines one at a time: type the script exactly as printed, end each PROG block with a tilde on its own line, and finish with a single dot on its own line. mudprog <target> shows what is attached and which triggers the engine found; mudprog <target> clear removes it. To fire a block on demand while testing, use mudprog <target> test GREET_PROG with yourself as the person who set it off. The indentation inside the recipes is optional; the engine trims every line before reading it. It is there for your eyes.
Ground Rules The Recipes Rely On
A handful of engine facts come up again and again below. They are all worth reading twice.
First, the trigger argument. A number is a percent chance: GREET_PROG 100 fires every single time a player walks in, GREET_PROG 25 one time in four. The word all, or nothing at all, means always. For speech-like triggers the argument is instead a keyword list: SPEECH_PROG work job fires when a spoken line contains work or job anywhere in it, and SPEECH_PROG p pay the toll fires only when the whole phrase pay the toll appears. Keywords are substring matches, so work also matches fireworks; pick distinctive words.
Second, dollar codes. Inside any script line, $n is the name of whoever set the trigger off (usually a player), $i is the scripted object itself, $t is the target of the event, $o is the item involved, $g is the text that rode in with the event (a spoken line, a skill name, an hour), and $b is the last thing the script loaded. Write $$ for a real dollar sign.
Third, memory. Scripts remember things with variables. The command mpsetvar <who> <name> <value> stores a named value on any object, including on a player, where it persists. The function var reads it back in a condition, as in if var($n toll_paid) == 1, and the form $<$i name> splices a stored value into text. A variable that was never set reads back as empty, which conveniently counts as false: if !var($i sprung) is true only the first time. The same-name variable on a different object is a different variable; storing state on $n gives every player their own copy, storing it on $i gives the object one shared copy.
Fourth, the cancel rule, which is the least obvious thing in this chapter. A CNCLMSG_PROG block watches for a game action such as OPEN, GET, ENTER, or SELL. When its action code and its keyword mask both match, the block runs INSTEAD of the action: the script is the replacement behavior, and the action itself is cancelled, every time, no matter what if statements inside the block decide. You cannot allow the action from inside the block; you can only imitate it (recipe 4 does exactly that) or remove the script so the next attempt goes through (recipe 3). Whether a given attempt is vetoed at all is decided by the header mask, not by the body. Also, cancel blocks are in scope for the whole room, so always put the object's own name in the mask (CNCLMSG_PROG OPEN chest, not CNCLMSG_PROG OPEN ALL) or your chest will veto every open in the room, including other people's pouches.
Fifth, hosts differ. A mob can run ordinary game commands: any script line that is not an mp-command is typed by the mob, so say, emote, yell, wield, and socials all work as if the mob were a player. Rooms and items cannot speak or emote; on those hosts use mpecho (to the whole room), mpechoat (to one person), and mpechoaround (to everyone except one person) instead.
Sixth, else stands alone. The engine reads else as a full line. Never write else if on one line; put if on its own fresh line underneath the else, and close both with their own endif. Several recipes below show the shape.
Last, safety. Every command fails soft: a bad argument does nothing rather than crashing the mob. Loops are capped, every trigger run has a step budget, and a runaway script stops itself and writes a note to the script_runaway log. You can experiment freely.
The object paths used below, such as /obj/meal, /obj/torch, /obj/armor and /obj/container, are stock items that exist on this mud, so every recipe works exactly as printed. When you adapt a recipe, swap in files from your own area.
Recipe 1: The Baker's Errand, A Complete Quest Giver
The problem: you want an NPC who offers a small task when asked, remembers who has started it and who has finished it, recognizes the moment the job is done, and pays out a reward exactly once. This recipe runs the whole quest through three blocks and one player variable, with no quest tables needed.
GREET_PROG 100
emote wipes flour-dusted hands on a canvas apron.
if var($n lunch_task) == done
say Good to see you again, friend. The ovens remember their heroes.
else
say Fresh bread, warm ovens, and one small disaster. Ask me about work.
endif
~
SPEECH_PROG work job task errand
if var($n lunch_task) == done
say You have done your good deed. The rest of the day is mine.
else
if var($n lunch_task) == started
say The lunch, friend. It is still sitting out there in the dust.
else
mpsetvar $n lunch_task started
say My lunch rolled clean off the cart this morning.
mpoloadroom /obj/meal
say There it sits. Pick it up and hand it to me, would you?
endif
endif
~
GIVE_PROG all
if isname($o meal) and var($n lunch_task) == started
mpsetvar $n lunch_task done
mpjunk $o
say Saved! You are a marvel and a friend of this bakery.
mpmoney $n 50
mpexp $n 200
mpachieve $n lunch_runner
else
say A kind thought, but that is not mine to take.
mpput $o $n
endif
~
How it works, line by line. The first block is the greeting. GREET_PROG 100 fires every time a player walks into the baker's room; 100 is the percent chance. The emote line is an ordinary game command, typed by the mob, so everyone sees the baker wipe his hands. Then the if line calls the var function: it reads the variable named lunch_task stored on $n, the player who just walked in. If that player has finished the errand before, the value is the word done and the baker greets them as a regular; anyone else, including someone who has never spoken to him, falls to the else branch and gets the hook line. Note that the comparison == done is a plain word; text comparisons ignore capitalization, and you only need quotes when the value contains spaces.
The second block is the conversation. SPEECH_PROG work job task errand fires when anyone in the room says a line containing any of those four words; $g would carry the spoken line if we needed it. Inside, the same variable is checked twice to give three different answers: done gets a polite brush-off, started gets a reminder, and the final else is the first conversation, where the quest actually begins. mpsetvar $n lunch_task started writes the word started onto the player, which is what the other two branches will find later. mpoloadroom /obj/meal clones the stock meal item onto the floor of the room, right in front of everyone, and the two say lines tell the player exactly what to do with it. Notice the shape of the nesting: the else is alone on its line, the inner if begins on the next line, and each if has its own endif.
The third block is the payoff. GIVE_PROG fires when someone hands the mob an item; the item arrives as $o. One engine quirk to memorize: GIVE_PROG pays no attention to keywords in its header, so write GIVE_PROG all and do the filtering inside, which is what the isname($o meal) call does; it asks whether the given object answers to the name meal. The and joins it with the variable check, so the reward path needs both the right item and a player who is mid-errand. On success: the variable flips to done so every other block changes its tune, mpjunk $o destroys the handed-over lunch, and three reward commands run: mpmoney $n 50 puts fifty gold in the player's purse, mpexp $n 200 grants experience, and mpachieve $n lunch_runner flags a named achievement, which prints its own celebratory line. The else branch handles everything wrong: wrong item, or the right item from a player who never took the errand. mpput $o $n moves the object straight back into the giver's inventory, so the baker cannot be used as a trash can.
Variations to try. Give the errand a timer by storing a second variable and checking it later. Move the reward to your real quest system: the commands mpstartquest, mpquestwin and the function questwinner($n quest_id) plug this same skeleton into quest_d, so the task shows in the player's quest journal. Add a GREET line for players who are mid-errand. Or make the fetch target live in another room entirely and let the player discover it.
Recipe 2: The Pit Warden, A Boss With Phases
The problem: a boss fight that changes as the boss gets hurt. At two thirds health it enrages; at one third it summons help and roots its enemy; on death it drops a trophy. The engine offers HITPRCNT_PROG, which fires every combat round in which the mob's health is at or below the header number, and that at-or-below is the trap: at 20 percent health, both the 66 block and the 33 block match every single round. The cure is a phase variable that each threshold advances exactly once.
ONCE_PROG
mpecho The pit warden rises, bone scraping stone.
~
FIGHT_PROG 30
switch $%randnum(3)%
case 1
say Your bones will thicken my walls.
case 2
emote drags a talon along the pit wall with a shriek.
case 3
mpasound A hollow roar rolls up out of the fighting pit.
endswitch
~
HITPRCNT_PROG 66
if !var($i phase)
mpsetvar $i phase 1
say Enough courtesy. Now we work.
mpcondition $i warden_fury buff 30 5 0
mpecho The warden's joints grind faster, spurred by something like fury.
endif
~
HITPRCNT_PROG 33
if var($i phase) == 1
mpsetvar $i phase 2
say Rise and hold the pit!
mpmload /obj/torch
mpset $b short a circling mote of grave-light
mpmload /obj/torch
mpset $b short a circling mote of grave-light
mpecho Two motes of grave-light spin up out of the cracked floor.
mpaffect $t rooted 6
endif
~
DEATH_PROG
say The pit keeps what it takes. Remember that.
mpoloadroom /obj/armor
mpset $b short the warden's rune-etched cuirass
mpecho The warden collapses inward, leaving its cuirass on the sand.
~
How it works. ONCE_PROG runs a single time, when the mob first loads, which makes it the right place for an entrance line; mpecho prints to the whole room without the mob speaking. FIGHT_PROG 30 rolls a thirty percent chance every combat round, and instead of one repeated taunt it switches on $%randnum(3)%, which is the function-substitution form: randnum(3) returns 1, 2 or 3, the $%...% wrapping pastes the result into the line, and switch compares it against each case. One round he threatens, another he scrapes the wall, another the mpasound line pushes a roar into every adjacent room, which is wonderful advertising for a boss chamber.
The 66 block is phase one. The condition if !var($i phase) uses the leading exclamation mark, which means not: it is true only while the phase variable on the boss itself, $i, has never been set. The very first round at or below 66 percent health sets phase to 1, and from then on the block matches every round but its body never runs again. That one-shot latch is the single most important pattern in boss scripting. Inside, mpcondition is the full-control form of the condition system: target, condition id, type, duration in seconds, then magnitude and percent, so the warden gives itself a thirty second buff named warden_fury. The room line sells the change.
The 33 block only runs if phase is exactly 1, so the phases always happen in order even if a heavy hit drops the boss from 70 to 20 in one round; blocks fire in the order they appear, so the 66 block runs first, sets the gate, and the 33 block follows in the very same round. It advances the variable to 2, then summons: mpmload clones a file into the room and remembers it as $b, and mpset $b short immediately rewrites the clone's display line, so a humble stock torch becomes a circling mote of grave-light before anyone reads it. The pair of loads gives two motes; each mpset re-skins the most recent one. Anything a script loads with mpmload is flagged to despawn on room reset, so a wipeout does not litter. Finally, mpaffect $t rooted 6 pins the boss's current combat target, $t, in place for six seconds; mpaffect is the short form and always applies a debuff.
DEATH_PROG fires as the mob dies: a last line, then a real reward, loaded to the floor with mpoloadroom and re-skinned through $b just like the motes.
Variations to try. Point mpmload at a real minion NPC file from your area instead of a re-skinned torch. Add HITPRCNT_PROG 10 with a phase 3 gate that casts a desperation spell through mpcast. Heal the boss between fights with mprejuv $i inside a RAND_PROG that checks isfight($i) is false. Or on death use mpsetvar to mark the killer, $n, and have a second NPC elsewhere react to var($n slew_warden) in its greeting.
Recipe 3: The Trapped Chest, A One-Time Open Veto
The problem: a chest that stabs the first person to open it, then behaves like an ordinary chest forever after. This is the classic use of the cancel rule from the ground rules: a CNCLMSG_PROG OPEN block runs instead of the open. Because a matching cancel block always cancels, the way to make the trap one-time is to make the script remove itself once it has fired, then push the open through on the player's behalf.
LOOK_PROG 100
mpecho Fine scratches ring the hasp of the ironbound chest.
~
CNCLMSG_PROG OPEN chest
mpecho A needle darts from the lock as the lid begins to shift!
mpdamage $n 20 pierce
mpechoat $n Venom burns along your arm.
mpunloadscript
mpforce $n open chest
~
How it works. The LOOK_PROG block is pure foreshadowing: whenever a player looks at the chest, a hint prints to the room. Careful readers get to feel clever. Since the host here is an item, everything uses the mpecho family; an item cannot say.
The trap itself is the second block. The header names the action code, OPEN, and then the mask, chest. The mask is matched against the name of the thing being opened, which is why it matters: with ALL in that spot, this script would cancel every open that happens anywhere in the room, including other players' bags, because cancel blocks watch the whole room. Masked to chest, it only fires for this chest. When someone tries to open it, the open is cancelled and these five lines run instead. The first shows the spring to the whole room. mpdamage $n 20 pierce deals twenty points of piercing damage to the opener; the damage types are blunt, cutting, thrusting, pierce, heat, cold, shock and magic. mpechoat $n sends one private line to the victim, which is where venom flavor belongs; the room already saw the needle. Then the pivot: mpunloadscript deletes this entire script from the chest, taking the trap, and the look hint, with it. The chest is now an utterly ordinary container. The last line, mpforce $n open chest, makes the player perform the open again, and because the script is gone, this second attempt sails through: from the player's point of view they opened the chest and paid for it, all in one motion.
The order of those last two lines is everything. Force first and the veto would catch the forced open too, cancel it, and run the trap again, around and around until the engine's step budget stopped it. Unload first, and the force lands on a defenseless chest.
Variations to try. Raise the damage and add mpaffect $n poisoned 30 for a lingering souvenir. Replace the damage with an alarm: mpasound to warn the dungeon plus mpmload of a guard. If you want a trap that resets rather than a strictly one-time one, skip mpunloadscript and instead gate the venom on if !var($i sprung), with a re-arm block elsewhere clearing the variable; remember the open itself will then stay cancelled every time, which suits a chest that is really a mimic or is welded shut. And keep only the trap on a trapped chest: mpunloadscript removes the whole script, so any flavor blocks you want to survive the spring belong on a different object.
Recipe 4: The Toll Gate, An Enter Veto With Paid Memory
The problem: a bridge you may only enter after paying, with the payment remembered per player, collected in one room and honored in the next. This recipe is two small scripts on two hosts that cooperate through a single variable stored on the player: a clerk who takes the money, and a warden who stands in the protected room and vetoes entry. Movement vetoes carry no text to mask against, so the warden's ENTER block matches every attempt to walk into his room; the paid check happens inside, and the paid path performs the crossing itself with mptransfer, which uses a low-level move that does not re-trigger the veto.
The clerk, standing in the room before the bridge:
GREET_PROG 100
say Hold there, $N. The span past my booth is toll road.
if var($n toll_paid) == 1
say Your toll is paid. The warden on the span will pass you.
else
say Five gold buys one crossing. Say pay the toll when you are ready.
endif
~
SPEECH_PROG p pay the toll
if var($n toll_paid) == 1
say Paid already. Save your coin for the far bank.
else
if goldamt($n) >= 5
mpmoney $n -5
mpsetvar $n toll_paid 1
say Five gold, counted and pocketed. The warden will let you by.
else
say Your purse disagrees. Come back with five gold.
endif
endif
~
The warden, standing inside the bridge room itself:
GREET_PROG 100
emote studies $n from beneath a rain-stained hood.
say Toll paid. Cross quickly and keep to the rail.
~
CNCLMSG_PROG ENTER
if isnpc($n)
mptransfer $n here
return
endif
if var($n toll_paid) == 1
mpsetvar $n toll_paid 0
mpechoat $n The warden steps aside and waves you onto the span.
mptransfer $n here
else
mpechoat $n The warden bars the way. Pay the clerk at the booth first.
endif
~
How it works. The clerk is ordinary conversation scripting. His greeting always announces the rule, then splits on the paid flag so returning paid customers are not nagged. His speech block uses the phrase form of the keyword mask, p pay the toll, which requires those three words together rather than any one of them. Inside, goldamt($n) reads the player's gold, and the payment is simply mpmoney $n -5: a negative amount takes money. Only after the coin moves does mpsetvar write toll_paid 1 onto the player. The order matters; never grant the flag before the charge succeeds.
The warden is where the engine earns its pay. His CNCLMSG_PROG ENTER block is in scope for anyone trying to enter the room he stands in, from any direction, and when it matches, the walk-in is cancelled and this block runs instead. First the guard clause: isnpc($n) is true when the would-be enterer is a mob rather than a player, and for those the script immediately imitates the move, mptransfer $n here brings them in, and return ends the block; without this, wandering NPCs would pile up outside the bridge forever. For players, the paid flag decides. The paid branch spends the flag, setting it back to 0 so one payment buys one crossing, tells the player what happened with a private mpechoat, and then performs the entry: here resolves to the warden's own room, and mptransfer moves the player in. The unpaid branch just reports the refusal; because the action was cancelled, the player is still standing where they started.
Two placement rules to respect. The warden must stand in the room being protected; that is what puts his veto in scope for entries into it, and only entries, so people already on the bridge can leave freely. And the clerk must stand outside it, because a player who cannot enter cannot speak to anyone inside; speech only carries within a room.
Variations to try. Let allies through free by checking factionrep, as in recipe 12. Sell a day pass by storing a count instead of a flag and subtracting one crossing per entry. Give the warden a GREET taunt for big groups using numpcsroom(). Or guard the way out instead with CNCLMSG_PROG LEAVE on a script attached to the room, which fires when anyone tries to leave it; a leave veto plus locked memory makes a fine oubliette.
Recipe 5: Harrow's Sundries, A Reactive Shopkeeper
The problem: a vendor with a personality: he pitches on arrival, haggles exactly once per customer, refuses to buy certain junk, and will not part with one particular item on his own shelf. The refusals ride on the trade veto codes: a SELL veto fires when a player sells to the vendor, masked against the item's name; a BUY veto fires when a player buys, masked against the words the player typed.
GREET_PROG 100
say Welcome to Harrow's Sundries, $N. Everything has a price.
if var($n harrow_discount) == 1
say Your discount stands, as promised. A tenth off while you visit.
endif
~
SPEECH_PROG haggle bargain discount
if var($n harrow_haggled) == 1
say We have haggled once already. My patience is not restocked.
else
mpsetvar $n harrow_haggled 1
if rand(40)
mpsetvar $n harrow_discount 1
say Sharp tongue. Very well, a tenth off while you stand here.
else
say Ha. My prices are carved deeper than your charm.
endif
endif
~
CNCLMSG_PROG SELL torch
say I do not buy half-burned torches. The last one set my awning alight.
~
CNCLMSG_PROG BUY p ledger
say The shop ledger is not stock, whatever the shelf says.
~
How it works. The greeting is the standard pattern from recipe 1, plus a reminder line for anyone carrying the discount flag. The haggle block runs a tiny game of chance: the first if makes haggling once-per-customer by latching harrow_haggled the moment anyone tries, and rand(40) then gives a forty percent success roll; rand(N) is simply true N times out of a hundred. Success writes a second variable, harrow_discount, which the greeting reads on later visits. The discount here is reputation, not arithmetic; scripts cannot reprice a vendor's stock. If you want it to have teeth, pay a rebate after the fact, for example a block that checks the flag and runs mpmoney $n 10, or treat it as pure flavor, which suits most shops fine.
The two veto blocks each replace a transaction with a line of dialogue. The SELL header masks on torch, matched against the name of the item the player is trying to sell, so torches bounce with an anecdote while everything else sells normally; no if needed, the header is the condition. The BUY header uses the phrase mask, p ledger, matched against what the player typed after buy, so buy ledger is refused while every other purchase proceeds. Both are one-line bodies because the cancel itself is the mechanic; the dialogue is just manners.
Variations to try. Mask several refusals at once, one CNCLMSG_PROG SELL block per category of junk, each with its own complaint. Greet big spenders by checking goldamt($n) at the door. React to the time of day with istime or isnight so the shop gets surly near closing. Or let the haggle reset each mud day by storing the day number from datetime(day) next to the flag and comparing on the way in.
Recipe 6: The Chronicler, A Timed Cutscene
The problem: a story told in paced beats, a few seconds apart, that plays once per listener. Scripts normally run top to bottom in an instant; the command mpsleep <seconds> suspends the block where it stands and resumes the remaining lines after the delay, which turns a PROG block into a little stage play.
GREET_PROG 100
emote looks up from a weathered journal as $n approaches.
if var($n heard_bridge_tale) == 1
say Back again? Stones do not fall twice, friend.
else
mpsetvar $n heard_bridge_tale 1
say Sit a moment. I will tell you how the old bridge fell.
mpsleep 3
say Twelve winters past, the river rose black in a single night.
mpsleep 3
emote traces a slow arc in the air with one ink-stained finger.
mpsleep 3
say The center span held to the last, they say, out of pride.
mpsleep 3
say And that is why the ferryman never lacks for work.
emote closes the journal with a soft clap.
endif
~
How it works. The opening emote is unconditional, so the chronicler always reacts to an arrival; then the memory check decides whether this player has heard the tale. Note that the latch, mpsetvar $n heard_bridge_tale 1, is written at the top of the else branch, before the first sleep, not at the end. That is deliberate: if a second GREET fires mid-story, say the same player steps out and back in, the latch is already set and the block takes the short branch instead of starting a second overlapping telling. The story itself is five beats. Each mpsleep 3 freezes this block for three seconds while the rest of the world, and the mob's other blocks, carry on; then the next line resumes exactly where it left off. Mixing say lines with emote beats is what makes it feel performed rather than dumped.
Two cautions. Each firing of a block is its own performance: if two different players arrive seconds apart, two tellings interleave in public; for a one-man show, gate on a variable stored on $i instead of $n so the whole room shares one cooldown. And keep mpsleep out of for and while loops; a sleep inside a loop abandons the rest of the loop when it wakes. Chains of beats belong in a straight line, exactly as above.
Variations to try. End the tale with a reward for first listeners, mpmoney or a keepsake through mpoload plus give. Trigger the performance from SPEECH_PROG tale instead of the greeting so players opt in. Or stage a two-actor scene: this mob's block runs mpforce <other mob> say ... lines between its own, and the sleeps keep both actors in rhythm.
Recipe 7: The Tavern Brawler, Reacting To Socials
The problem: an NPC who notices body language. SOCIAL_PROG fires when someone performs a social or emote nearby, and the acted text arrives in $g, so one block can read the room and answer slaps differently from bows.
SOCIAL_PROG all
emote cracks his knuckles slowly.
if strin(slap $g) or strin(punch $g) or strin(shove $g)
say Oh, you want it rough, do you? Outside. Now.
growl
else
if strin(bow $g) or strin(salute $g)
say Manners, in this place. I am almost touched.
else
say Keep your twitching to yourself, $n.
endif
endif
~
GREET_PROG 40
emote sizes up $n the way a butcher sizes up a hog.
~
How it works. The header is SOCIAL_PROG all, so every social in the room wakes him, and the sorting happens inside with strin, the substring test: strin(slap $g) is true when the word slap appears anywhere in the acted text. Three aggressive words share the first branch through or; the courteous pair gets its own nested if under the else, alone on its line as always; everything else lands on the bottom line, which addresses the actor by name. The bare word growl on the second line of the first branch is not an mp-command, so the mob simply performs the social himself; scripts can use the whole social library the same way. The GREET_PROG 40 block shows the other common brawler touch: a forty percent chance means he only sometimes bothers to look up, which reads as character.
One rule this recipe demonstrates by omission: if you write several SOCIAL_PROG blocks whose masks all match the same event, every one of them fires, in order. That is occasionally what you want, a general reaction plus a specific one, but usually it is double dialogue; with one all block and branches inside, the question never arises.
Variations to try. Escalate: count slaps in a variable on $i using the counter idiom from recipe 10, and have the third one start a real fight with mpkill $n. Make him maudlin after midnight by wrapping the branches in an isnight() check. Or answer specific socials in kind, hug for hug, by performing the social back at $n.
Recipe 8: The Night Watchman, A Clockwork Patrol
The problem: a city that keeps time out loud. TIME_PROG fires when the mud clock reaches an hour listed in its header, using hours 0 through 23, and because the check rides on the mob's own heartbeat, it belongs on mobs, not rooms. This watchman calls the hours, tolls a bell in counted strokes, and sweeps his lantern at night.
GREET_PROG 100
emote touches the brim of his dented helm to $n.
if isnight()
say Keep to the lit streets, friend. The hour is late.
else
say Fine day for honest business. Move along cheerful.
endif
~
TIME_PROG 22
mpasound The night watch calls ten of the clock, and all is well.
say Ten of the clock, and all is well.
~
TIME_PROG 0
say Midnight, and the city sleeps. Mostly.
for $1 = 1 to 3
mpecho The watch bell tolls, stroke $1 of three.
next
mpalarm 6 mpecho The last echo of the watch bell dies away.
~
TIME_PROG 6
yell Dawn bell! Stalls open, shutters up!
~
RAND_PROG 8
if isnight()
emote sweeps his lantern across the shadowed doorways.
endif
~
How it works. The greeting varies with the world clock through isnight(), one of a family that also includes istime(hour), isday, isseason and isweather. Then three TIME_PROG blocks divide the day. The header is a list of hours; TIME_PROG 22 fires once when the clock turns ten at night. Its body pairs mpasound, which speaks into every adjacent room but not this one, with a plain say for the room he is standing in; together they cover the neighborhood, which is exactly how a street cry should carry.
The midnight block introduces the counted loop. for $1 = 1 to 3 runs its body three times, storing the current count in the temporary slot $1, which the mpecho line prints, so the room hears stroke 1, stroke 2, stroke 3. Loops close with next. After the loop, mpalarm 6 schedules one single command to run six seconds later, here a final mpecho; mpalarm is the lightweight cousin of mpsleep, right for a one-line afterthought where mpsleep is right for a paced sequence. Note that an mpalarm line holds exactly one command.
The RAND_PROG 8 block is the idle texture: an eight percent roll on each of the mob's heartbeats, and even then only at night thanks to the guard. Keep RAND percentages low; a heartbeat comes every couple of seconds, and at 50 a mob babbles.
Variations to try. Give him a beat to walk by adding more TIME hours that each run mpwalkto with a direction or two. Announce curfew consequences: after the 22 call, a companion CNCLMSG_PROG ENTER on the tavern could turn away latecomers, recipe 4 style. Or wire DAY_PROG, which fires once at the top of each mud day, to have him mark holidays.
Recipe 9: The Sphinx, A Riddle With A Typed Answer
The problem: ask a question, collect a typed answer without it echoing as a command or a say, judge it, and pay the prize. Three tools combine here: mpprompt asks and quietly captures the player's next typed line into a variable named prompt_answer on that player; mpalarm schedules the judging; and a FUNCTION_PROG block, a named routine that only runs when called with mpcallfunc, does the judging.
GREET_PROG 100
emote regards $n with eyes carved from moonpale stone.
say I keep one riddle and one prize. Say riddle when you dare.
~
SPEECH_PROG riddle
mpsetvar $n prompt_answer
say Then listen once, for stone does not repeat itself.
mpprompt I fade at dawn yet no hand moves me. What am I?
mpalarm 20 mpcallfunc judge
~
FUNCTION_PROG judge
if var($n prompt_answer) == moon
say The moon it is. Take your prize and your cleverness elsewhere.
mpmoney $n 25
else
if var($n prompt_answer) == ""
say Silence is also an answer. The wrong one.
else
say No. The stone keeps its prize.
endif
endif
mpsetvar $n prompt_answer
~
How it works. The greeting advertises the game. The riddle block starts by clearing any stale answer: mpsetvar with a name and no value erases the variable, which matters if the same player has played before. Then the question goes out through mpprompt rather than say, and this is the special part: the very next line the player types is swallowed by the prompt, never reaching the command parser, and lands in prompt_answer on the player. The player answers by simply typing moon and pressing enter, no say needed, and nobody else in the room sees it. The script cannot pause to wait, so it sets an appointment instead: mpalarm 20 mpcallfunc judge runs the judging routine twenty seconds later, answer or no answer.
FUNCTION_PROG judge never fires on its own; it exists to be called. When the alarm lands, it reads the captured answer with var($n prompt_answer) and branches three ways: the right word pays out with mpmoney, the empty string means the player typed nothing in time, and anything else is a wrong guess. Comparing to "" is how you test a variable is empty when the distinction from wrong matters; where it does not, the bare !var(...) form is shorter. The final line clears the answer again so the next round starts clean whatever happened.
One design note: the answer must match the stored text exactly, apart from capitalization, so moon wins but the moon loses. If you want generosity, judge with strin(moon $%var($n prompt_answer)%) instead, which accepts any answer containing the word.
Variations to try. Ask yes or no questions with mpconfirm, which captures into confirm_answer as a clean yes or no. Rotate riddles by storing a number on $i and switching on it. Punish failure with mpdamage, this is a sphinx, after all. Or record champions with mptitle $n the Sphinx-Friend.
Recipe 10: The Bank Vault, Escalating Refusals
The problem: a vault door that cannot be forced, remembers how many times it has been tried, and gets nastier about it. This recipe is the counter idiom: seed a variable to 0 the first time, add one with math, then branch on the total.
LOOK_PROG 100
mpecho A single seam splits the vault door, thinner than a hair.
~
CNCLMSG_PROG OPEN vault
if !var($i alarms)
mpsetvar $i alarms 0
endif
mpsetvar $i alarms $%math($<$i alarms> + 1)%
if var($i alarms) >= 3
mpecho A deep klaxon rolls up through the floor.
mpdamage $n 10 shock
mpechoat $n The seam spits a lash of blue fire across your hand.
else
mpecho The door does not move. Somewhere below, a bell chimes once.
endif
~
CNCLMSG_PROG UNLOCK ALL
mpecho The lock swallows the sound of the attempt. Nothing turns.
~
How it works. The look block plants the dread. The OPEN veto, masked to the vault's own name as recipe 3 taught, permanently cancels every open; unlike the trapped chest there is no mpunloadscript, because this door is never meant to give. What changes is the response. The first three lines are the counter idiom, worth memorizing as a unit: the if seeds alarms to 0 the first time anyone tries, and the mpsetvar line stores the result of $%math($<$i alarms> + 1)%, which reads the current value with the $<object variable> text form, adds one with the math function, and writes it back. Seeding matters because an unset variable reads as empty text, and empty plus one does not arithmetic make. From the third attempt onward, whoever is at the door, the branch turns violent: a room-wide klaxon, ten shock damage, and a private line for the burglar's benefit. The counter lives on $i, the vault, so it counts attempts by everyone; store it on $n instead and each burglar would get their own patience meter.
The UNLOCK veto closes the picks-and-keys loophole with a single line of atmosphere. Its mask is ALL rather than the vault's name, which is a deliberate flourish: this script sits on the vault in a room of its own, and the bank does not care what you try to unlock in there.
Variations to try. Have the third attempt also summon a guard with mpmload and a shout with mpasound. Let the manager reset the tally: a SPEECH_PROG block on the bank manager runs mpsetvar on the vault by name. Grant real access to trusted patrons the recipe 4 way, a teller NPC checks factionrep($n syndicate) and walks the player past a CNCLMSG_PROG ENTER on the strongroom. Or watch openings you did allow elsewhere with OPEN_PROG, as the reliquary in recipe 15 shows.
Recipe 11: The Fisherman, Reacting To Weather
The problem: an NPC whose mood is the forecast. The weather functions read the live weather for the realm the mob stands in: isweather(word) is true when the current conditions contain the word, weather() returns the condition as text you can splice into speech, and isnight(), season() and timeofday() round out the almanac.
GREET_PROG 100
emote glances up from his line as $n approaches.
if isweather(rain)
say Fish bite best when the sky weeps. Pull up a stone and sit.
else
if isnight()
say Night fishing, friend. The quiet ones rise after dark.
else
say Flat calm and $%weather()%. A patient day, this one.
endif
endif
~
RAND_PROG 10
if isweather(storm)
emote hauls his line in and eyes the clouds warily.
else
if rand(50)
emote sends the line whistling back out over the water.
else
emote hums an old tide-song under his breath.
endif
endif
~
SPEECH_PROG fish biting catch
if isweather(rain)
say Aye, they are biting. Rain stirs the shallows.
else
say Slow today. Ask me again when the weather turns.
endif
~
How it works. The greeting funnels through the by-now familiar nested else-if ladder: rain first, then night, then the fair-weather line, which demonstrates splicing a function straight into dialogue with $%weather()%, so he might say flat calm and clear, or flat calm and cloudy, tracking the sky without you writing a case for every condition. The idle block gives his fishing a weather-aware rhythm: storms interrupt it, otherwise a coin flip through rand(50) picks between casting and humming, which is enough variety that he does not loop visibly. The speech block answers the one question every passerby asks a fisherman, from the same forecast.
Variations to try. Add an isseason(winter) branch where he is simply absent, walked home by mpgoto at the season's first greeting. Sell bait when it rains, an offer through mpoload plus give gated on isweather. Or pay out a fish: a SPEECH_PROG on the word fish with a rand roll, an mpoload of your area's fish item, and a proud emote.
Recipe 12: The Doorman, Judging By Faction
The problem: a club door that knows your reputation. The Brinewarren factions, syndicate, tideborn and faceless, score each player from -1000 to 1000; factionrep($n syndicate) reads the raw number for comparisons, the faction function turns it into the tier name for dialogue, and mpfaction moves it.
GREET_PROG 100
emote looks $n up and down without hurry.
if isnpc($n)
return
endif
if factionrep($n syndicate) >= 100
say The house knows its friends. Go in, and drink well.
else
if factionrep($n syndicate) <= -100
say You have a nerve showing that face here. The door is shut to you.
else
say The Gilded Eel is members only tonight. The house counts favors.
endif
endif
~
SPEECH_PROG standing reputation favor
say The ledger names you $%faction($n syndicate)% with the Syndicate.
say Run errands for the house and the door will remember it.
~
GIVE_PROG all
if isname($o meal) and factionrep($n syndicate) < 1000
say For me? The house remembers small kindnesses.
mpjunk $o
mpfaction $n syndicate 10
say Your standing with the Syndicate improves.
else
say I did not ask for this.
mpput $o $n
endif
~
How it works. The greeting opens with an unconditional appraisal, then the return guard: when the arrival is an NPC, return simply ends the block, because a doorman lecturing wandering rats is noise. The main ladder cuts the reputation range into three receptions at plus and minus 100. The speech block turns the number into words: $%faction($n syndicate)% splices the tier name, so the doorman might call you Neutral, Honored or Feared in plain dialogue, and the second line tells players how to move the needle. The give block is the needle: hand him a meal and mpfaction $n syndicate 10 raises Syndicate standing by ten, with the same polite-refusal else as recipe 1 for everything he did not ask for. The extra factionrep < 1000 check just refuses tribute from someone already at the cap.
Variations to try. Make the tiers gate the actual door by pairing this greeting with a recipe 4 warden inside whose paid check is factionrep($n syndicate) >= 100 instead of a toll flag; then reputation literally opens doors. Snub the rival faction with a second ladder on factionrep($n tideborn). Or use a zapper mask instead of functions where the gate is coarse: a header like GREET_PROG -level 20 fires only for level twenty and up, and -class, -race and friends work the same way.
Recipe 13: The Training Dummy, Applauding Technique
The problem: a practice target that notices which skill you used on it. When a player fires a skill, every scripted mob in the room gets a CASTING_PROG event with the skill's name in $g, which makes a switch on $g the whole trick. The dummy also patches itself up when nobody is looking.
CASTING_PROG all
mpecho The training dummy creaks on its post.
switch $g
case bash
mpechoat $n A crisp bash. Squarely in the straw. The guild would approve.
case fireball
mpechoat $n Scorched again. The dummy smolders at you accusingly.
default
mpechoat $n The dummy weathers your $g without complaint.
endswitch
~
DAMAGE_PROG 100
if hitprcnt($i) <= 50
emote sheds a worrying quantity of straw.
endif
~
RAND_PROG 5
if hitprcnt($i) < 100
mprejuv $i
emote stitches itself back together with a faint rustle.
endif
~
How it works. The first block fires on every skill used nearby. After an unconditional creak for the room, switch $g compares the skill name against each case: name the skills you want bespoke reactions for, spelled the way the skill system spells them, and let default catch the rest; note default still praises by name, since $g works inside any text. The private mpechoat lines keep the feedback between dummy and student instead of spamming the sparring hall. The DAMAGE_PROG block fires whenever the dummy takes a hit, and consults its own health with hitprcnt($i), a number from 0 to 100, to decide whether to complain. The RAND block is the janitor: five percent of heartbeats, and only when actually damaged, mprejuv $i restores the dummy to full and the emote explains the miracle. A practice target that quietly refills is friendlier than one that dies mid-lesson.
Variations to try. Track a personal best: store the count of skills seen from each player in a variable on $n and have the dummy congratulate round numbers. React to spells against other targets, the CAST_PROG trigger fires on the mob a spell actually lands on, with the spell name in $g. Or score sessions: seed and increment a counter per CASTING event, recipe 10 style, and report it when spoken to.
Recipe 14: The Graveyard Haunt, Ambience With A Trigger Word
The problem: a presence more than a monster: something that chills arrivals, murmurs at random, and stirs when anyone nearby utters its word. The last part uses REGMASK_PROG, which fires when any text the scripted mob sees, speech, emotes, whatever scrolls past it, matches the pattern in its header.
GREET_PROG 100
mpechoat $n A cold draft finds the back of your neck.
mpechoaround $n $N shivers at something the rest of you cannot feel.
~
RAND_PROG 6
switch $%randnum(3)%
case 1
mpecho A thin mist gathers between the headstones.
case 2
mpecho Somewhere close, soil trickles onto stone.
case 3
mphide $i
mpecho The lanternlight gutters, then steadies.
mpunhide $i
endswitch
~
REGMASK_PROG grave
mpecho The mist coils tighter, as if it were listening.
~
How it works. The greeting splits its audience, which is a technique worth stealing: mpechoat $n gives the arriving player a private chill, while mpechoaround $n shows everyone else that player shivering, using $N for the player's name. Two different sentences, one moment, no one sees both. The ambience block rolls six percent per heartbeat and then picks one of three effects with the switch-on-randnum pattern from recipe 2; the third effect flickers the haunt itself out of sight and back with mphide and mpunhide, which toggle invisibility. The REGMASK block is the ghost story: its header is a pattern, here the word grave, and any line of text the haunt witnesses containing that word, said, emoted or yelled, wakes it.
One serious warning about text masks: the haunt sees its own output too. If the reaction line contained the trigger word, the mpecho would wake the mask again, which would echo again, forever, or at least until the engine's step cap cut it off. Keep the pattern word out of every line the script itself prints, as done here, where the reaction speaks of mist and never of graves.
Variations to try. Make the word dangerous: add mpdamage $n 5 cold to the mask block and the superstition writes itself. Vary the ambience by hour with istime or isnight guards on extra cases. Or give the haunt a grudge: when the mask fires, store the speaker with mpsetvar $n marked 1 and let a GREET block elsewhere in the graveyard treat marked players differently.
Recipe 15: The Sunken Reliquary, A Scripted Mini-Dungeon
The problem: three hosts working as one dungeon: a threshold room that asks for an offering, a warden who honors it, and a reliquary that guards its own treasure. Together they show the room, mob and item sides of the same toolkit, cooperating through one variable on the player. Attach the first script to the entrance room itself with mudprog here edit, the second to the warden mob in the room beyond, the third to the chest in the final chamber.
The threshold room:
GREET_PROG 100
mpechoat $n Cold air sighs up the stair, carrying old incense.
if !var($n crypt_light)
mpechoat $n The sconces here are dark, as if waiting for a gift of flame.
endif
~
DROP_PROG all
if isname($o torch)
mpjunk $o
mpsetvar $n crypt_light 1
mpecho The offered flame leaps from sconce to sconce down the stair.
mpechoat $n The dark accepts your light. Walk freely below.
else
mpecho Dust stirs around the dropped offering, then settles, unmoved.
endif
~
The crypt warden, one room below:
ONCE_PROG
mpecho Bone and bronze assemble themselves into a standing warden.
~
GREET_PROG 100
if var($n crypt_light) == 1
say The stair accepted you. I do not argue with the stair.
else
say Turn back. The dead take no unsworn visitors.
endif
~
FIGHT_PROG 40
emote fights in perfect, practiced silence.
~
DEATH_PROG
mpecho The warden folds down into a neat pile of bronze rods.
mpoloadroom /obj/torch
mpset $b short the warden's still-burning eye
mpecho One pale ember remains where it stood, watching nothing.
~
The reliquary, in the last chamber:
LOOK_PROG 100
mpecho Straps of silver wire bind the reliquary to its plinth.
~
CNCLMSG_PROG GET reliquary
mpechoat $n The reliquary is bound fast to the plinth. It will not move.
~
OPEN_PROG
if !var($i first_open)
mpsetvar $i first_open 1
mpecho The hinges part for the first time in a long age.
mpoloadroom /obj/armor
mpset $b short a saint's gilded breastplate
mpecho Something gilded gleams inside the reliquary.
else
mpecho The hinges part with a familiar sigh.
endif
~
How it works. The room script proves rooms are full citizens: they greet arrivals, and they see items dropped on their floor through DROP_PROG, where the dropped thing is $o. Drop a torch and the room consumes it with mpjunk, brands the player with crypt_light, and answers with a public spectacle plus a private blessing; drop anything else and the dust is unimpressed. Since a room cannot speak, every line is mpecho or mpechoat.
The warden welds the dungeon together without a single veto: his greeting reads the same crypt_light variable the room wrote, so the offering upstairs changes the reception downstairs. Players who skipped it are warned, not blocked, and if they push on, the fight is the consequence; swap the warning for a recipe 4 style CNCLMSG_PROG ENTER if you want a hard gate. His ONCE line dresses the set on load, the FIGHT line gives combat a signature at a modest chance, and his DEATH block plants a keepsake using the load-and-reskin pattern from recipe 2.
The reliquary shows an item defending itself two different ways. The GET veto, masked to its own name, makes it a permanent fixture: every attempt to pick it up is cancelled and answered privately, the immovable-prop pattern, useful for any scenery players keep pocketing. But there is no OPEN veto, so opening works normally, and OPEN_PROG is the observer that fires after a successful open. Its body is a first-time latch, recipe 2's !var gate on $i: the first opener in the chest's life conjures the prize onto the floor, everyone after gets a sigh. Note the difference in kind: CNCLMSG blocks replace an action, underscore-PROG blocks like OPEN_PROG merely react to one that happened. Choosing between those two is most of scripted dungeon design.
Variations to try. Make the offering matter at the door by giving the warden the hard gate on var($n crypt_light). Let the reliquary demand the warden be dead first, his DEATH block can mpsetvar a flag on his killer that the OPEN_PROG checks. Add a TIME_PROG midnight event in the crypt. And when the set pieces multiply, name your variables like a family, crypt_light, crypt_oath, crypt_key, so mudprog view reads like a plan.
Testing, Debugging, And Habits
Fire any block on demand with mudprog <target> test GREET_PROG, which runs it with you as the source; speech-keyword blocks are easier to test by just saying the keyword in the room. To try lines without attaching anything, scripttest runs a raw script against yourself, and because some clients mangle dollar signs typed inline, put anything with $-codes in a file and use scripttest runfile <path>. When a script goes quiet, mudprog <target> shows what the engine parsed and which triggers it found, a missing tilde usually announces itself there as two blocks fused into one. Sprinkle mplog lines to write breadcrumbs to the mudprog log while you hunt, and check the script_runaway log if you suspect a loop hit the step cap. Above all, build the way these recipes do: one block at a time, tested as you go, with memory in clearly named variables and every cancel block masked to its owner's name.
This chapter is the one-stop cheat sheet for MUDProg, Rogue's builder scripting system. It is the page you keep open in a second window while you write. Every trigger, every command, every function, every dollar code, every operator, and every safety limit on this mud is listed here, checked line by line against the live engine. If you have never scripted before, read help mudprog first for the gentle tutorial; come back here whenever you forget a name or an argument order, which is often, and normal.
A quick word on how to read the tables. Angle brackets mean "replace this with your value": <who> means an object reference such as $n, <text> means words of your choosing. Square brackets mean "optional". Everything else is typed exactly as shown. Script text is case-insensitive almost everywhere: GREET_PROG, greet_prog, and Greet_Prog are the same trigger, and mpecho, MPECHO, and MpEcho are the same command.
The Cast Of Characters
Every script runs with a small cast that the dollar codes and object references point at. Learn these five words and every table below makes sense:
host - The object carrying the script: the mob, room, or item the
script is attached to. Referred to as $i in script text.
source - Whoever set the trigger off, usually a player. Referred to
as $n.
target - A second party when the event has one (the recipient of a
gift, the victim of a spell). Referred to as $t. When there
is no separate target it is the same as the source.
item - The object the event was about (the thing picked up, worn,
eaten, given). Referred to as $o.
message - The text riding along with the event: the spoken line, the
spell name, the failed command. Referred to as $g.
Script Shape In Thirty Seconds
A script is one or more PROG blocks. Each block is a trigger header line, a body of statements, and a line containing only a tilde:
GREET_PROG 100
say Welcome to the reference hall, $N.
mpecho A gong sounds softly somewhere below.
~
Rules the parser applies, in full:
The tilde line ends a block. A final block without a tilde is accepted.
Lines starting with # or * are comments and are skipped.
Blank lines are skipped.
A trigger name without _PROG has it added: GREET means GREET_PROG.
You may write several blocks with the SAME trigger; each is checked and
each that passes its header fires, top to bottom.
The engine reparses automatically the moment the script text changes.
There is nothing to reload and no compile step.
Any statement that is not control flow and not an mp command is run by the host as an ordinary game command, exactly as if the mob had typed it. So say, emote, yell, whisper, socials, wield, wear, cast, open, go north, and everything else a player can type all work inside a script with dollar codes substituted first.
The mudprog Command
mudprog <target> - View the script and its triggers. mudprog <target> edit - Enter the line editor (replaces the script). mudprog <target> set <inline> - Set the whole script from one line. mudprog <target> append <inline> - Append one line to the script. mudprog <target> clear - Remove the script entirely. mudprog <target> test <TRIGGER> - Fire a trigger now, with you as source.
The target resolves in this order: the word here or room for the current room, self or me for you, a named object in the room, a named object in your inventory, then an online player by name.
In set and append a semicolon becomes a newline, so a whole block fits on one command line:
mudprog guard set GREET_PROG 100 ; say Halt! State your business. ; ~
In the editor, type lines normally, end each PROG block with a tilde, finish with a single dot on its own line, or type @abort to cancel. The editor replaces the whole script; it does not merge.
mudprog <target> test GREET_PROG calls the block with you as both source and target and the message set to the word test. That matters for keyword-gated blocks: a SPEECH_PROG whose header is a keyword list only fires under test if one of its keywords appears in the word test, so use a header of all while testing, then tighten it.
The scripttest Command
Admin harness for running script bodies without attaching them to anything.
scripttest <lines> - Run raw lines on yourself; ; separates lines. scripttest runfile <path> - Run a script body read from a mudlib file. scripttest fire <TRIG> [on <name>] - Fire a trigger on a scripted target.
For raw runs: the host is you, the source is you, the target is the first other living in your room, $o is the first item in your inventory, and the message is the word test. Because control flow works across semicolons you can test a whole if block inline:
Important gotcha: telnet clients frequently mangle the dollar sign, so any inline test that uses dollar codes may silently misbehave. Put the body in a file and use scripttest runfile instead; that path is escaping-proof.
The Testing Workflow
1. Draft the script in a file, or build it with mudprog edit.
2. Attach it: mudprog <target> set ... or paste into the editor.
3. View it back: mudprog <target> shows the text plus the list of
triggers the parser actually found. If a trigger you expected is
missing from that list, your header line has a typo.
4. Fire it by hand: mudprog <target> test GREET_PROG. Remember the test
message is the word test.
5. Trip it for real: walk out and back in, say the keyword, start the
fight.
6. Watch the logs. mplog writes to /log/mudprog. A script that hits the
step budget is recorded in /log/script_runaway with the host and
trigger name.
7. Edit and re-test freely: changes are picked up automatically on the
next firing.
Trigger Reference
The header argument column shows what goes after the trigger name; see the next section for the argument grammar. The host column is where the script must live for the trigger to fire on a live game event.
Arrival and presence:
GREET_PROG <pct|mask> - A player enters the host's room. On a mob it
fires about a second after entry; on a room
it fires immediately. Source is the player.
ALL_GREET_PROG <pct|mask> - Fires on the same entry event as GREET_PROG.
Kept for CoffeeMUD parity; on this build the
two are equivalent, and stealthy arrivals
fire both.
GROUP_GREET_PROG <pct> - Also fires on entry, once per entering
player (mobs only).
ENTRY_PROG <pct|mask> - On a room script: a player enters the room.
The CoffeeMUD mob-side meaning (the scripted
mob itself arriving) is recognised but not
wired to a live event here.
Speech and sound (the spoken line rides in $g; the speaker is $n):
SPEECH_PROG <keywords> - Someone else speaks in the room (mobs).
ACT_PROG <keywords> - Fires on the same speech event (mobs).
MASK_PROG <keywords> - Fires on the same speech event (mobs).
SPEAK_PROG <keywords> - The scripted being ITSELF says a line, via
say or NPC talk. Host is the speaker.
CHANNEL_PROG <words> - Traffic on a chat channel, anywhere in the
world. $g is the channel name followed by
the text, so putting the channel name in the
header keyword list filters by channel.
Idle time and the clock:
RAND_PROG <pct> - Rolls once per heartbeat while the mob is
active. RAND_PROG 5 is a rare mutter;
RAND_PROG 100 is every beat.
ONCE_PROG - Once, about a second after the mob loads.
Guarded so it never repeats for that copy.
TIME_PROG <hour list> - The mud clock reaches a listed hour (fires
once at the change; mobs with a heartbeat).
$g carries the hour.
DAY_PROG <day list> - A new mud day begins; $g is the day number.
Fires world-wide on any scripted object.
AGE_PROG - A player crosses a new hour of played time;
$g is the hour count. World-wide.
QUEST_TIME_PROG <id> [m..] - Once per minute while a timed quest runs.
Header is the quest id plus an optional list
of minutes-remaining to match; $g is the
quest id and minutes left. World-wide.
Combat (mobs):
FIGHT_PROG <pct> - Each combat round while fighting. Source is
the current enemy.
HITPRCNT_PROG <pct> - Each combat round while the mob's health
percentage is AT OR BELOW the header number.
Note it re-fires every round below the line,
so guard one-shot speeches with a variable.
DEATH_PROG - The mob is dying. Source is the killer.
KILL_PROG - The mob just killed its target. Source is
the victim.
Items changing hands (the item is $o):
GIVE_PROG <keywords> - The scripted mob is handed an item. Source
is the giver.
GIVING_PROG <keywords> - An item is given away; fires on the ITEM,
the room, and witnessing scripted mobs.
Target is the recipient.
GET_PROG <pct|keywords> - The item is picked up. Fires on the item,
the room, and witnessing scripted mobs.
GETTING_PROG - Fires on the TAKER's own script.
DROP_PROG <pct|keywords> - The item is dropped. Same fan-out as GET.
DROPPING_PROG - Fires on the DROPPER's own script.
PUT_PROG - An item is put in a container; fires on the
item AND on the container.
PUTTING_PROG - Fires on the PUTTER's own script; the
container is the target.
WEAR_PROG - The item is worn or wielded.
WEARING_PROG - Fires on the WEARER's own script.
REMOVE_PROG - The item is removed or unwielded.
CONSUME_PROG - The food is eaten or the drink drunk; fires
on the meal, the room, and witnesses.
Looking, doors, containers, vendors:
LOOK_PROG <pct> - A player looks at the scripted mob.
LLOOK_PROG <pct> - Fires on the same look event (mobs).
OPEN_PROG / CLOSE_PROG - The door or container is opened or closed;
fires on it, the room, and witnesses.
LOCK_PROG / UNLOCK_PROG - Likewise for locking and unlocking.
BUY_PROG - A player buys from the scripted vendor; the
goods are $o.
SELL_PROG - A player sells to the scripted vendor.
Magic and skills (the spell or skill name rides in $g):
CAST_PROG <keywords> - A legacy spell resolves on the scripted
target. Source is the caster.
CASTING_PROG <keywords> - The scripted being casts a legacy spell, or
uses any class skill; skill use also fires
on the room and witnessing scripted mobs.
Company:
FOLLOW_PROG - Someone starts following the scripted
leader. Source is the follower.
UNFOLLOW_PROG - Someone stops following.
RIDE_PROG - The scripted MOUNT is mounted; source is
the rider.
RIDING_PROG - The scripted RIDER mounts something.
World-wide player events (fire on every scripted object that defines them, anywhere in the world):
LOGIN_PROG - A player enters the game. Source is the
player.
LOGOFF_PROG - A player leaves the game.
LEVEL_PROG - A player gains a level; the new level rides
in $g. Leave the header blank and test
level($n) in the body: a bare number in a
LEVEL_PROG header is read as a percent
chance, not a level.
Advanced observers:
FUNCTION_PROG <name> - A named routine. Never fires on its own;
run it with mpcallfunc or callfunc().
CNCLMSG_PROG <code> [mask] - Vetoes a game action before it commits and
runs INSTEAD of it. See The Message Bus.
EXECMSG_PROG <code> [mask] - Observes a game action as it executes.
IMASK_PROG <text> - The host's OWN action produced output
containing the text (case-insensitive
substring; blank matches everything).
REGMASK_PROG <regex> - ANY text the host sees matches the regular
expression.
CMDFAIL_PROG <keywords> - A player's command failed to resolve; fires
on the room and scripted mobs in it. The
failed line rides in $g.
Recognised but not wired to a live event on this build (they parse, and you can fire them with mudprog <target> test or scripttest fire, but no game event triggers them yet): SOCIAL_PROG, BRIBE_PROG, DAMAGE_PROG, EXIT_PROG, ARRIVE_PROG, ENTRY_PROG on mobs, and the experimental DELAY_PROG (prefer ONCE_PROG plus mpsleep for a delayed opener).
Trigger Header Arguments
Whatever follows the trigger name on the header line gates whether that block fires. The engine tries these interpretations in order:
(nothing), all, 100 - Always fire.
a whole number - Percent chance to fire, 1 to 99. Zero or
negative never fires.
-type value ... - A zapper mask restricting WHO can set it
off (next section).
p <phrase> - The whole phrase must appear inside the
event message ($g), case-insensitive.
word word word - A keyword list: ANY word appearing inside
the event message fires the block. If the
event carries no message, the block fires.
Four triggers read their header specially: HITPRCNT_PROG treats the number as a health-percent threshold, not a chance; TIME_PROG and DAY_PROG treat it as a list of hours or day numbers to match exactly; QUEST_TIME_PROG expects a quest id followed by an optional minutes list. CNCLMSG_PROG and EXECMSG_PROG have their own two-part header described under The Message Bus. IMASK_PROG takes a plain substring and REGMASK_PROG a regular expression. FUNCTION_PROG takes the routine's name.
Zapper Mask Clauses
A zapper mask is a list of clauses; each clause is a dash-word followed by the values that qualify. ALL clauses must pass, checked against the source of the trigger. Values may be written bare or with a leading plus sign (CoffeeMUD style); both are accepted. An example header:
GREET_PROG -class mage -level 30
fires only for mages of level thirty or higher. The full clause list:
-class <names> - Source's class is one of the names. Alias -classes.
-race <names> - Source's race is one of the names. Alias -races.
-sex <values> - Source's gender matches. Alias -gender.
-name <names> - Source answers to one of the names. Alias -names.
-deity <names> - Source worships a listed deity. Alias -worships.
-level <n | lo-hi> - A bare number means at-or-above that level; lo-hi
is an inclusive range; several values pass if ANY
matches. Aliases -levels, -lvl.
-player - Source must be a player. Alias -pc.
-npc - Source must be an NPC. Alias -mob.
-good / -evil - Source's alignment property matches.
Unknown clause types pass leniently, so a CoffeeMUD mask with an exotic clause degrades gracefully instead of jamming the trigger shut. A live example, safe to copy:
GREET_PROG -level 1
mpecho The level ward hums as someone worthy passes.
~
Dollar Codes
Inside any statement, dollar codes are replaced with live values before the line runs. The table is a faithful port of CoffeeMUD's, so scripts written from CoffeeMUD documentation substitute identically here.
People:
$n $N - The source's name (both cases give the proper name).
$t $T - The target's name.
$i - The host's name. $I - The host's short description.
$q - Same as $i. $Q - Same as $I.
$r $R - A random PLAYER in the room.
$c $C - A random living in the room other than the host.
$f - The host's leader's name. $F - Leader's he or she.
Items:
$o $O - The event item's name (first item).
$p $P - The second item's name, when the event carries two.
$b - The last object cloned by mpmload, mpoload, or mpoloadroom.
$B - That object's short description.
$w $W - The living carrying item one or item two, when carried.
Pronouns (all read the right being's gender):
$e $s $m - Source: he or she, him or her, his or her.
$E $S $M - Target: the same three forms.
$j $h $k - Host: the same three forms.
$H $J $K - Random player: him or her, he or she, his or her.
$y $Y - sir or madam for the source and for the target.
Place:
$a $A - The area name.
$d - The room's title. $D - The room's full description.
$l - A list of the livings here, excluding the host.
$L - A list of the items here.
$x $X - A random exit direction from this room.
Message and slots:
$g - The event message in lower case.
$G - The event message exactly as spoken.
$0..$9 - Ten temporary text slots, set by for loops, mpargset, and
mploadvar.
$$ - A literal dollar sign.
An unknown code is left in the text as-is, so a typo shows itself. Two live examples:
GREET_PROG 100
mpecho Watchers note that $N stands in $d.
~
GREET_PROG 100
mpoloadroom /obj/torch
mpecho Conjured from thin air, $b clatters to the floor.
~
Extended Substitutions
$<obj var> - The value of a stored script variable. obj is any
object reference ($i, $n, a name); var is the
variable's name. Set them with mpsetvar.
$%FUNC(args)% - The result of any function from the function tables,
inserted into the text as words or a number.
$[name] - Best-effort: the name of a matching ITEM in the room
(CoffeeMUD quest-table syntax; resolved by name here).
${name} - Best-effort: the name of a matching MOB in the room.
Both live, in one block each:
SPEECH_PROG all
mpsetvar $i mood cheerful
mpecho The keeper's mood is now $<$i mood>.
~
GREET_PROG 100
say I count $%numpcsroom()% adventurers and $%numitemsroom()% loose items here.
~
Control Flow
if <condition> - Branch. Optional else. Close with endif.
else
endif
switch <value> - Compare a substituted value against cases.
case <value> - First match runs, then the switch ends; no
default fall-through. Matching ignores case. A break
endswitch directly after a case body is allowed.
for $1 = <a> to <b> - Count from a to b (either direction),
next storing the counter in a digit slot.
while <condition> - Loop while the condition holds. endwhile
endwhile closes it; mpwhile and done are accepted
aliases for CoffeeMUD compatibility.
break - Leave the nearest loop or switch case.
return [value] - Stop the script. The value, if given, becomes
the result of a FUNCTION_PROG.
mpsleep <seconds> - Pause the script here and resume the REST of
it after the delay (whole seconds, minimum
one). mpwait is an alias. Sleeping inside a
loop abandons the loop's remaining rounds and
resumes after it.
Two things beginners trip on. First, the for counter must be one of the digit slots $0 through $9; a named counter is accepted by the parser but cannot be read back in the body. Second, blocks nest freely, and every opener needs its closer even when the body is one line.
Live examples of each shape:
GREET_PROG all
if ispc($n) and level($n) >= 1
mpecho The wards recognise a living adventurer.
else
mpecho The wards stay dark and silent.
endif
~
SPEECH_PROG all
mpsetvar $i tally 3
switch $<$i tally>
case 3
mpecho The tally stands at three.
default
mpecho The tally is unknown.
endswitch
~
RAND_PROG 100
for $1 = 1 to 3
mpecho Drumbeat $1 echoes through the hall.
next
~
RAND_PROG all
mpargset 0 0
while number($0) < 5
mpargset 0 $%math($0 + 1)%
if number($0) == 3
break
endif
endwhile
mpecho The count halted at $0.
~
RAND_PROG all
mpecho The ritual begins with a low chant.
mpsleep 2
mpecho The ritual concludes two breaths later.
~
Conditions And Operators
A condition is one or more atoms joined by connectors. An atom is usually a function call, optionally compared to a value. The comparison may sit INSIDE the parentheses, CoffeeMUD's canonical form, or OUTSIDE them; both are correct here:
if level($n > 30) - CoffeeMUD canonical form.
if level($n) > 30 - Also accepted, same meaning.
Comparison operators:
== or = - Equal. Case-insensitive for words, numeric for numbers.
!= or <> - Not equal.
> < - Greater than, less than.
>= or => - At least.
<= or =< - At most.
.in. - True when the left value appears anywhere inside the
right value, case-insensitive.
When both sides look like whole numbers the comparison is numeric; otherwise it is textual. Quotes around the right side are stripped, so class($n) == 'mage' and class($n) == mage are identical.
An atom with no comparison is true when its result is anything other than zero, an empty string, the word false, or the word no. A leading exclamation mark negates one atom: if !isfight($i) reads "if I am not fighting". An empty condition is true.
Connectors, evaluated left to right with no precedence:
and - Both sides must hold.
or - Either side suffices.
not - Between atoms, "a not b" means a and not b.
andnot - Left holds and right does not.
ornot - Left holds or right does not.
Bare comparisons of substituted values also work without any function:
if $<$i sprung> == 1
if $g .in. northern gate password ledger
Command Reference
Object arguments (<who>, <item>, <container>) accept a dollar code, the words self, me, or host, or a name searched first among livings in the room, then items in the room, then the host's own inventory. Text arguments have dollar codes substituted. Bad arguments make the command do nothing; they never crash the script.
Messaging:
mpecho <text> - To everyone in the room.
mpechoat <who> <text> - To one target only. Alias mea.
mpechoaround <who> <text> - To the room except one target. Alias mer.
mpasound <text> - To every adjacent room.
mpchannel <channel> <text> - Onto a chat channel.
mpspeak <text> - The host says it aloud.
mpllm <text> - To all online staff.
mplog <text> - Appends to /log/mudprog.
mpprompt <text> - Ask the source a question; the typed
reply is stored in their prompt_answer
script variable for a later block to
read with $<$n prompt_answer>. Alias
mpchoose.
mpconfirm <text> - Yes-or-no question; the answer lands
in their confirm_answer variable as
yes or no.
mpaccuse <who> - Publicly accuse; costs the target 25
syndicate reputation.
Movement and combat:
mpgoto <room> - Move the host to a room. Rooms are a
file path, the word here, or an object
reference whose room is used.
mpat <room> <command> - Run one command as if standing in
another room, then return.
mptransfer <who> [room] - Move a target to a room, or to the
host's room when omitted.
mpwalkto <dir> [dir ...] - Step the host through directions.
Alias mptrackto.
mpkill <who> - Start a fight with the target.
mphit <who> - Land a single attack.
mpdamage <who> <amount> [type] - Direct damage. Types: blunt, cutting,
thrusting, pierce, heat or fire, cold
or ice, shock or lightning, magic
(the default).
mpheal <who> <amount> - Restore health.
mpcast <spell> [target] - Cast a spell. Alias mpcastext.
mpslay <who> - Kill a living outright.
mprejuv [who] - Restore a living to full health,
magic, and stamina; default the host.
mpreset [who] - As mprejuv for livings; resets a room.
mpstop [who] - End the target's combat.
mpflee - Make the host flee.
mpforce <who> <command> - Force a target to run a command.
mpbeacon <secs> <command> - Run one script line after a delay.
Alias mpalarm. The line may be any mp
command or game command.
mppossess <player> <mob> - Put a player in control of a mob.
mpbehave <flag> - aggressive makes the host hostile,
wander starts wandering, anything else
sets a named flag readable with
isbehave().
mpunbehave <flag> - Clear the flag.
Loading things and shaping the world:
mpmload <path> - Clone an NPC into the room. It is
flagged to despawn on area reset, and
$b points at it afterwards.
mpoload <path> - Clone an item into the host's
inventory; $b points at it.
mpoloadroom <path> - Clone an item into the room.
mploadquestobj <path> - Clone an item into the SOURCE's hands.
mprload <room> - Run a room's area reset now.
mpjunk <item> - Destroy an item (never a living).
mppurge <who> - Destroy a mob or item (never a
player).
mpput <item> <container> - Move an item into a container.
mphide [who] / mpunhide [who] - Turn invisibility on or off; default
the host.
mplink <dir> <path> - Add an exit to the host's room.
mpunlink <dir> - Remove an exit.
mpopen <thing> / mpclose <thing> - The host opens or closes a door or
container, exactly as if typed.
mplock <thing> / mpunlock <thing>- Likewise for locks.
mpoloadshop <path> - Clone an item into the scripted
vendor's stock room. Alias
mpmloadshop.
mpm2i2m - Accepted for CoffeeMUD compatibility;
does nothing (no morph system here).
Character and progression (the target is usually $n):
mpset <who> <field> <value> - Set level, str, agi, con, int, wis,
cha, hp, sp, name, short, or long
with the proper game setter; any
other field becomes a property on the
target. Alias mpsetinternal.
mpexp <who> <amount> - Grant experience; negative removes.
Alias mprpexp.
mpmoney <who> [type] <amount> - Give or take currency; type defaults
to gold.
mptitle <who> <title> - Award a title.
mpfaction <who> <faction> <n> - Shift Brinewarren reputation.
mptrains <who> <skill> [points] - Credit skill progress, default 100
points. Alias mppracs.
mpaffect <who> <id> [seconds] - Apply a condition such as rooted,
stunned, or poisoned; default 60
seconds.
mpcondition <who> <id> <type> <secs> [magnitude] [percent]
- Full-strength condition with type
(buff, debuff, dot...), magnitude and
percent. A Rogue extension.
mpunaffect <who> <id> - Remove a condition.
mptattoo <who> <text> - Mark with a visible tattoo. Alias
mpacctattoo.
mpachieve <who> <id> - Flag an achievement, with fanfare.
mpplayerclass <who> <class> - Change class.
mpsetclan <who> <clan> - Set clan membership.
mpsetclandata <who> <key> <val> - Store a clan-related value.
A safe, self-contained live example:
RAND_PROG 100
mpaffect $i rooted 10
mpecho Vines coil around the keeper's own boots.
mpunaffect $i rooted
~
Quests:
mpstartquest <who> <quest> - Begin a quest with the host as giver.
mpendquest <who> <quest> - Turn the quest in; if it cannot
complete, it is dropped instead.
mpquestwin <who> <quest> - Complete a quest outright.
mpstepquest <who> <event> - Nudge quest progress: kill, visit, or
talk.
mpqset <who> <quest> <key> <val> - Write a field on an active quest.
mpquestpoints <who> <amount> - Award quest points.
mploadquestobj <path> - Give the source a quest item.
Variables and script structure:
mpsetvar <obj> <name> <value> - Store a variable on any object; read
it back with $<obj name> or var().
Alias mpsavevar; player variables
persist across sessions.
mpgset <name> <value> - Set a mud-wide global, saved across
reboots.
mploadvar <obj> <name> <slot> - Copy a stored variable into a digit
slot $0 to $9.
mpargset <slot> <value> - Set a digit slot directly.
mpcallfunc <name> [args] - Run a FUNCTION_PROG block by name;
the args ride in $g inside it.
mpscript <line> - Run one script line immediately.
mpunloadscript - The host deletes its own script.
mpnotrigger - Records a suppress flag on the host.
Reserved: the engine does not consult
it yet.
mpenable <TRIG> / mpdisable <TRIG> - Record per-trigger disable flags.
Reserved likewise.
And the FUNCTION_PROG pairing, live:
GREET_PROG 100
mpcallfunc announce
~
FUNCTION_PROG announce
mpecho A herald announces the visitor to the empty air.
~
Function Reference
Functions are used in if and while conditions and inside $%...% substitution. Where a signature shows <who> or <item> as the first argument it may be omitted, in which case the host is assumed. Every function is safe: a bad argument returns zero or an empty string.
Chance, numbers, and text:
rand(<pct>) - True that percent of the time.
randnum(<n>) - A random number from 1 to n.
rand0num(<n>) - A random number from 0 to n-1.
number(<text>) - The text as a whole number.
isodd(<n>) - True for odd numbers.
math(<a> <op> <b> ...) - Whole-number arithmetic, left to right, with
+ - * / and % (remainder).
eval(<condition>) - Evaluates a whole condition, 1 or 0.
strin(<word> <text>) - True when the word appears in the text.
strcontains(<text> <word>)- The same test with the arguments swapped.
islike(<text> <mask>) - Loose match; asterisks in the mask are
ignored and a substring test is applied.
callfunc(<name> [args]) - Runs a FUNCTION_PROG and yields its return
value.
Who and what someone is:
isnpc(<who>) ispc(<who>) isalive(<who>) isfight(<who>)
isimmort(<who>) ischarmed(<who>) isfollow(<who>)
isservant(<who>) isgroup(<who>) ispkill(<who>) isgood(<who>)
isevil(<who>) isneutral(<who>) iscontent(<who>)
- Yes-or-no tests: NPC, player, alive, in
combat, staff, charmed, following someone,
a companion, grouped, in a player-kill zone,
of an alignment, or at peace.
isspeaking() - True when the trigger carried a message.
sex(<who>) - male, female, or neuter.
position(<who>) - standing, sitting, lying, kneeling, flying,
or swimming.
level(<who>) - Level as a number.
class(<who>) - Class name. baseclass() is the same here.
race(<who>) - Race name. racecat() is the same here.
name(<who>) - The being's name.
deity(<who>) - Worshipped deity, or empty.
mood(<who>) - The mood property, or empty.
hitprcnt(<who>) - Health as a percentage, 0 to 100.
exp(<who>) - Experience points.
questpoints(<who>) - Quest points.
goldamt(<who>) - Gold carried; for an item, its value.
currency(<who>) - Always gold on this mud.
value(<item>) - An item's value in gold.
stat(<who> <stat>) - A stat by name: str, dex or agi, con, int,
wis, cha (CoffeeMUD names map across).
gstat(<who> <key>) - A stat if the key names one, otherwise the
raw property of that name.
isable(<who> <skill>) - True when the being has any rating in the
skill.
expertise(<who> <skill>) - The skill rating as a number. skill() is
the same.
ipaddress(<who>) - A player's network address.
cansee(<who> <target>) - False when the target is invisible.
canhear(<who>) - False when deafened.
affected(<who> <id>) - True while the condition is active; with no
id, the name of the first active condition.
Carrying and wearing:
has(<who> <item>) - The being carries a matching item.
hasnum(<who> <item> <n>) - Carries at least n of them.
itemcount(<who>) - How many things are carried. Alias
numitemsmob.
worn(<who> <item>) - The named item is currently worn.
wornon(<who> <slot>) - Something is worn on the named body slot.
objtype(<item>) - armor, weapon, container, or item.
isopen(<item>) - The container or door is open.
islocked(<item>) - It is locked.
incontainer(<item> <box>) - The item is inside that container.
mobitem(<who> <n>) - The name of the n-th carried item, from 0.
The room and the area:
nummobsroom() - How many NPCs are here. Alias nummobs.
numitemsroom() - How many items are on the floor.
numpcsroom() - How many players are here.
roommob(<n>) - The name of the n-th NPC here, from 0.
roomitem(<n>) - The n-th item's name.
roompc(<n>) - The n-th player's name.
numraces() - Distinct races among the livings here.
nummobsinarea() - Approximated to this room on this build.
numpcsarea() - Players in this whole area.
areapc(<n>) - The n-th player in the area, by name.
inroom(<who> <room>) - The being stands in that room, matched by
file path or by room title.
ishere(<name>) - Something answering to the name is in the
host's room.
inlocale(<who> <path>) - The being's room path contains the fragment.
inarea(<who> <area>) - The being is somewhere in the named area.
The clock and the sky (mud calendar unless marked rl for real life):
istime(<hour>) - It is that mud hour. Alias ishour.
isday(<day>) - It is that mud day of the month.
ismonth(<name>) - The mud month by name.
isyear(<year>) - The mud year.
isseason(<season>) - spring, summer, autumn, winter.
ismoon(<phase>) - The moon phase by name.
isweather(<word>) - The current local weather contains the word.
datetime(<part>) - HOUR, DAY, MONTH, or YEAR as a value.
isrlhour(<h>) isrlday(<d>) isrlmonth(<m>) isrlyear(<y>)
- The same tests against the real-world clock.
Stored variables and tags:
var(<obj> <name>) - A stored script variable's value, or empty.
hastag(<obj> <property>) - True when the named property is set.
Quests, factions, clans, titles:
questwinner(<who> <quest>)- The player has completed that quest.
questscripted(<who>) - The object carries a script.
questobj(<who> <item>) - The item is quest-bound to the player.
qvar(<quest> <key>) - A field from the source's active quest.
faction(<who> <faction>) - The reputation TIER NAME with a faction.
factionrep(<who> <faction>)- The raw reputation NUMBER.
hastitle(<who> [title]) - Holds the title; with none given, holds any.
hastattoo(<who>) - Bears a tattoo. Aliases hasacctattoo,
hastattootime.
clan(<who>) - Clan name, or empty.
clanrank(<who>) - Rank number within the clan.
isbehave(<who> <flag>) - A flag set by mpbehave is on.
isname(<who> <name>) - The being answers to the name.
Vendors:
shophas(<vendor> <item>) - The shop stocks a matching item.
shopitem(<vendor> <n>) - The n-th stocked item's name.
numitemsshop(<vendor>) - How many items are in stock.
Rogue extensions (not in CoffeeMUD):
hp(<who>) maxhp(<who>) - Current and maximum health.
sp(<who>) maxsp(<who>) - Current and maximum spell points.
ep(<who>) maxep(<who>) - Current and maximum stamina.
skill(<who> <skill>) - Skill rating as a number.
factionrep(<who> <fac>) - Raw faction reputation.
weather() - The local weather condition as words.
season() - The current season.
timeofday() - dawn, day, dusk, or night.
isnight() - True at night.
groupsize(<who>) - How many are in the being's group.
Accepted for CoffeeMUD compatibility but always zero or empty on this build: isbirthday, isrecall, isable2, trains, pracs, questmob, isquestmobalive, questroom, questarea, clandata, clanqualifies, explored.
The Message Bus
Beyond the named triggers, most concrete game actions pass through a message bus with a short CODE, and scripts can hook the bus in two ways.
CNCLMSG_PROG runs BEFORE the action commits. If its header matches, the script runs INSTEAD of the action: the pickup does not happen, the door does not open, the blow does not land. Your script is the replacement behavior, so say something, or the player sees silence.
EXECMSG_PROG runs WHILE a matching action executes, purely as an observer; it cannot stop anything.
Both share a two-part header: a code specifier, then an optional mask.
CNCLMSG_PROG <code-spec> [ALL | keywords | p phrase]
EXECMSG_PROG <code-spec> [ALL | keywords | p phrase]
The code-spec is a bus code or the word ALL. CoffeeMUD's decorations are accepted and ignored: leading <, >, or ? characters and the S=, T=, O= prefixes are stripped, so a header pasted from a CoffeeMUD script works unchanged. The mask tests the message text that rides with the action, usually the item's key name, with the same keyword and p-phrase rules as speech triggers; ALL or nothing matches everything.
Codes that reach the cancel pass live on this build:
ATTACK - combat is starting against the object in scope
BUY SELL - vendor trade
CAST - a class skill or legacy spell is being used
ENTER - a living is entering the room in scope
LEAVE - a living is leaving
GET DROP PUT GIVE - items changing place or hands
WEAR REMOVE - equipment going on or off
EAT DRINK - consumption
OPEN CLOSE LOCK UNLOCK - doors and containers
Codes that reach the observer pass live: GET, DROP, PUT, WEAR, REMOVE, CONSUME, OPEN, CLOSE, LOCK, UNLOCK, GIVING, and CASTING.
Accepted aliases inside a code-spec: CONSUME matches EAT and DRINK; FIGHT and KILL match ATTACK; SPELL matches CAST and CASTING; RIDE and MOUNT match each other; SAY and SPEECH match SPEAK; ARRIVE matches ENTER; EXIT and DEPART match LEAVE.
Which scripts are consulted: the object the action is about, the room, every scripted object standing in the room, and the actor. So a doorman mob with CNCLMSG_PROG ENTER on the destination room's floor vetoes arrivals, and a cursed sword with CNCLMSG_PROG REMOVE refuses to leave your grip.
Safety promise: the cancel pass is wrapped in error guards, and any script error counts as ALLOW, so a broken script can never wedge shut a door, an attack, or a pickup.
Two live examples:
EXECMSG_PROG ALL
mpecho The archivist scribbles a note about what just happened.
~
CNCLMSG_PROG GET test
mpecho A warding shimmer stops anything marked for testing.
~
The second cancels picking up any item whose name contains the word test and prints its warding line instead.
Safety Limits
The engine is builder-safe by construction. The numbers:
Step budget - 4000 executed statements per trigger firing. A script
that exceeds it stops silently and the event is
logged to /log/script_runaway with the host and
trigger name. An mpsleep refreshes the budget when
the script resumes.
Loop cap - 2000 iterations for any single for or while loop,
applied on top of the step budget.
Error handling - Every command and function is individually guarded.
A bad argument is a quiet no-op; a runtime error
aborts only that block, never the host, and on the
cancel pass an error always allows the action.
Hard guards - mppurge refuses to destroy players. mpjunk refuses
to destroy livings. mpslay works only on livings.
Idle cost - An object with no script pays one property read per
event. There is no background cost for having the
engine loaded.
CoffeeMUD Compatibility
Identical, so CoffeeMUD's own documentation and script archives translate directly: the PROG block shape with tilde terminators, trigger names, percent and keyword and p-phrase header arguments, zapper mask syntax, the whole dollar-code table, the $<obj var> and $%func()% extended forms, the condition grammar with comparisons inside or outside the parentheses, the connector words, the command names, the function names, FUNCTION_PROG with mpcallfunc, and the CNCLMSG and EXECMSG bus semantics including code-spec decorations.
Extra, on this mud only: mpsleep and mpwait inline pausing (CoffeeMUD needs alarm gymnastics for the same effect), mpcondition for full-strength buffs and debuffs, mpllm for staff-only whispers, and the function set hp, maxhp, sp, maxsp, ep, maxep, skill, factionrep, weather, season, timeofday, isnight, and groupsize.
Accepted but inert, so pasted CoffeeMUD scripts degrade gracefully instead of erroring: mpm2i2m, mpnotrigger, mpenable, and mpdisable are absorbed without effect, and the stub functions listed at the end of the function tables always return zero or empty. A handful of triggers parse but await live wiring, listed at the end of the trigger tables.
Missing outright: nothing. Every CoffeeMUD trigger, command, and function name is recognised by the parser; unknown commands fall through and run as ordinary game commands, and unknown functions evaluate to zero rather than breaking the condition around them.
Where To Go Next
The tutorial with worked scenes lives in help mudprog, and the harness details in help scripttest. A complete example NPC built entirely from a script is at /domains/examples/npc/mudprog_greeter.c if you want a working file to copy. Keep this chapter open, write small, test after every block, and let the tilde be the only ceremony between you and a living world.
This chapter is a workbook, not a lecture. The other chapters of the MUDProg guide explain; this one makes you DO. It holds twenty-seven small exercises, in order, each one a complete little job: give a mob a voice, teach it a word, give it a habit, let it make a choice. Every exercise tells you the goal in plain words, offers hints, then shows the full worked solution with every line explained, and finishes with variations to try on your own. By the last page you will have built, with your own hands, a finished multi-part tavern keeper.
You do not need to have written a script before. You do not need to have written ANYTHING before. If you have skimmed the first half of the mudprog-basics chapter you will move faster, but every idea used here is re-explained the first time it appears, so you can also start cold and let the exercises teach you. What you cannot do is learn this by reading alone. Scripting is typing, watching, and fixing. Type every solution in yourself, even when you peeked. Especially when you peeked. The fingers remember what the eyes forget.
One promise before we start, because it is the promise that makes practice possible: nothing in this workbook can harm the mud. A script with a mistake in it simply does less than you hoped. A command the game does not know fails quietly. A runaway loop stops itself. The very worst thing you can build here is a mob that spams the room, and one command, mudprog <target> clear, ends that. So experiment with a free heart.
Setting Up Your Practice Corner
Every exercise needs a mob to practice on. Any NPC in your own area will do, standing in a quiet room where your experiments bother no one. If you do not have an area yet, ask a senior builder to stand up a practice dummy for you; it takes them a minute. Throughout this workbook the practice mob is called dummy in commands; substitute your own mob's name everywhere you see it.
Scripts are attached with the mudprog builder command. The forms you will use constantly:
mudprog dummy - View the script and its triggers. mudprog dummy edit - Open the line editor. mudprog dummy set <inline> - Replace the script from one line. mudprog dummy append <inline> - Add lines to the end. mudprog dummy clear - Remove the script entirely. mudprog dummy test <TRIGGER> - Fire a trigger right now.
For these exercises, use the editor. Type mudprog dummy edit, then type the script lines exactly as printed in the solution, one per line, including each block's closing tilde, the ~ character, on its own line. Finish with a single period, the . character, alone on a line, and the script is saved and live at once. There is no compiling and no reboot; the engine re-reads the script the moment it changes. If you tangle yourself up mid-edit, type @abort alone on a line to throw the typing away and keep whatever was there before.
The indentation you see in the printed solutions, three spaces before every line and a little more inside decisions, is entirely optional. The engine trims it. It is there to make the shape of the script easy for your eyes to follow, and typing it is a good habit, but nothing breaks without it.
Each new exercise REPLACES the previous script: open the editor and type the new one; saving overwrites the old. Nothing from a previous exercise lingers, with one interesting exception about memory that Exercise 23 will teach you on purpose.
How To Test Your Work
The truest test is always the real event. For a greeting, walk out of the room and walk back in. For a listener, say the word out loud. Do the real test at least once for every exercise; watching your mob react to the actual thing never stops being satisfying.
While you are iterating, though, walking in and out gets old, and that is what mudprog dummy test GREET_PROG is for: it fires the named trigger immediately, with you standing in as the person who caused it. Three honest facts about test, so its results never confuse you:
- You are both the source and the target of the pretend event, so codes that name the source, like $N, show your own name. - The pretend event's message is the single word test. This matters enormously in Part Two: a block that listens for the keyword hello can NOT be fired by test, because hello does not appear in the word test. Keyword blocks are tested by actually saying the keyword in the room. - A percent chance is still a percent chance under test. A block headed with 25 fires one test in four. While testing, raise the number to 100, then set it back before you walk away.
After every save, glance at mudprog dummy and read the Triggers line it prints. That list is the engine telling you what it actually found in your script. If a trigger you thought you wrote is missing from the list, the script did not parse the way you meant, usually because of a missing tilde; Exercise 24 turns that exact situation into a lesson.
How To Work An Exercise
Read the goal. Try to write the script yourself before looking at the solution, using the hints when you stall; a wrong attempt teaches more than a copied answer. Then compare with the worked solution, type the solution in even if yours worked, run the test, and do at least one of the variations. The variations are where the real learning hides: they force you to change something and predict what will happen, which is the entire skill of scripting in miniature.
A note on reading the solutions. A script is made of PROG blocks. Every block has three parts: a header line naming the trigger, the WHEN; body lines holding commands, the WHAT, one per line, run top to bottom; and a closing line holding only ~, the tilde, which is the full stop of MUDProg. Blocks sit one after another in the same script, each with its own tilde. That is the whole grammar. Everything else in this workbook is vocabulary.
Part One: Saying Hello
Imagine a small tavern called the Lantern, freshly built, utterly silent. The rooms are pretty and the staff are statues. Part One gives them a voice. You will learn the two speaking commands every scripter uses a hundred times a day, say and emote, the narrator command mpecho, and the aimed pair mpechoat and mpechoaround, all hung on the friendliest trigger there is: GREET_PROG, which fires when a player walks into the mob's room.
Exercise 1: The One Line Greeter
The goal. Make your mob speak one line of welcome, out loud, every time a player walks into its room. This is the smallest complete script that does anything, and it is three lines long.
Hints. The trigger for "a player just walked in" is GREET_PROG. The number after the trigger name is a percent chance to act; 100 means every single time. The command to speak is say, exactly as a player would type it. Do not forget the tilde.
The worked solution.
GREET_PROG 100
say Welcome in out of the weather, traveler!
~
Line by line.
- GREET_PROG 100 is the header: the WHEN. GREET_PROG names the event, a player entering the room. The 100 means act every time, no dice roll. - say Welcome in out of the weather, traveler! is the body: the WHAT. When the trigger fires, the mob performs this line exactly as if it had typed it at a prompt, so the whole room hears the mob speak. - ~ closes the block. The engine reads everything between the header and the tilde as one set of instructions.
Test it with mudprog dummy test GREET_PROG, then do the real thing: walk out of the room and walk back in. About a second after you arrive, the greeting lands. That small delay is deliberate; it lets the arriving player read the room description before the mob speaks.
Try changing this.
- Change the 100 to 50 and walk in and out several times. Half your entrances now pass in silence. Put it back to 100 when done. - Change say to yell and hear how the same line carries differently. - Change the words entirely. Make it rude. Make it frightened. One line of dialogue is already a personality.
Exercise 2: Greeting By Name
The goal. The same greeting, but personal: the mob should welcome each visitor by their own name. Greet Ashley and it says Ashley; greet Josef and it says Josef, from one script, forever.
Hints. Inside any script line, the two-character code $N is swapped, at the moment the line runs, for the name of whoever set the trigger off. For a greeting, that is the arriving player. You never type a real name into the script at all.
The worked solution.
GREET_PROG 100
say Well met, $N! The fire is warm and the ale is cold.
~
Line by line.
- The header is unchanged: every arrival, no dice. - In the say line, $N is not text; it is a placeholder. Before the mob speaks, the engine replaces it with the arriving player's name. The rest of the line is spoken exactly as written. - ~ closes the block, as it always will.
When you fire this with test, $N shows YOUR name, because under test you are the pretend arriver. That is correct behavior, not a bug.
Try changing this.
- Use $N twice in the same line. It works; every occurrence is swapped. - Try the code $i somewhere in the line. That one is the mob's OWN name; Exercise 17 makes a whole lesson of it. - If you ever need to show a real dollar sign in a message, write two of them: $$. Exercise 18 does exactly that.
Exercise 3: Speech And Gesture
The goal. Make the greeting a small performance: first the mob speaks, then it visibly does something. Two commands, one block, run in order.
Hints. A block's body can hold as many command lines as you like, and they always run top to bottom. The command for a visible action is emote: the text you give it is shown to the room with the mob's name in front, so emote bows deeply becomes, to everyone watching, the mob's name followed by bows deeply.
The worked solution.
GREET_PROG 100
say Ah, a customer at last! Come in, come in.
emote sweeps off his hat and bows low.
~
Line by line.
- The header fires on every arrival. - The say line runs first. The room hears the words. - The emote line runs second. The room sees the gesture, printed with the mob's name leading it. Write emote text in the third person, continuing from the name: sweeps, bows, grins, never I sweep. - ~ ends the block.
Speech plus gesture is the bread and butter of NPC scripting. One without the other reads flat; together they sell a character in two lines.
Try changing this.
- Swap the two lines so the bow comes before the words. Watch how the same content feels different in a different order. - Add a third line, another emote, and confirm all three run in order. - Write an emote for a mob with no hat. What does a nervous cook do when a stranger walks in? A bored guard? The gesture IS the character.
Exercise 4: The Invisible Narrator
The goal. Atmosphere instead of dialogue. When a player walks in, the room itself should seem to tell them something: a line of story text that belongs to no one, with no name in front of it.
Hints. This is your first mp command. The commands you have used so far, say and emote, are ordinary game commands performed by the mob. The special script commands all start with the letters mp. The one you want is mpecho: it prints its text to everyone in the room, plain, as pure narration.
The worked solution.
GREET_PROG 100
mpecho Somewhere above the rafters, a loose shutter bangs twice in the wind.
~
Line by line.
- The header is the same trusty GREET_PROG 100. - The mpecho line prints its text to the whole room with no speaker attached. Nobody said it; it simply happened. This is how you write weather, creaks, smells, dread, and every other thing a room does to the people standing in it. - ~ ends the block.
Notice that this script never mentions the mob at all. The mob is just the hook the trigger hangs on; the words could be about anything. A script like this is often attached to the ROOM instead of a mob, with mudprog here edit, and works exactly the same way.
Try changing this.
- Follow the mpecho with a say line, so the narration sets a mood and then the mob reacts to it. Narrator and actor in one block. - Rewrite the line for a different sense: a smell, a sound underfoot, a chill. Rooms with three senses feel real. - Attach it to the room with mudprog here edit and clear it from the mob, and confirm the arrival still triggers it.
Exercise 5: A Word In Private
The goal. Perspectives. When a player walks in, they alone should read one private line, everyone ELSE in the room should read a different line, and then the whole room, newcomer included, should share one final line. Three audiences, three commands.
Hints. mpechoat sends text to one person only, and mpechoaround sends text to everyone except that person. For both, the FIRST word after the command names the receiver, and the code $n, meaning whoever fired the trigger, is the receiver you want nine times out of ten. Everything after that first word is the message. And you already know mpecho for everyone at once.
The worked solution.
GREET_PROG 100
mpechoat $n The old woman catches your eye and winks, just for you.
mpechoaround $n The old woman sizes up the newcomer and says nothing.
mpecho A log settles in the fireplace with a shower of sparks.
~
Line by line.
- The header fires on every arrival, and $n is set to the arriver. - The mpechoat line goes to $n alone: the newcomer reads about a wink meant just for them. Nobody else sees this line at all. Notice the message is written in second person, your eye, because it speaks directly to one reader. - The mpechoaround line goes to everyone EXCEPT $n: the bystanders read a colder, outside view of the same moment. Room text like this is written in third person, the newcomer, because it describes a scene. - The mpecho line goes to everyone, arriver and bystanders alike; it is the shared world both audiences live in. - ~ ends the block.
To see all three lines you need two viewpoints. Test alone and you will read the mpechoat line and the mpecho line but not the mpechoaround line, because you are the excluded person. Ask a friend to stand in the room, or log an alt in beside yourself, and compare screens. This pair of commands is how you script secrets, pickpocket warnings, whispered tips, and anything else where what YOU see is not what THEY see.
Try changing this.
- Swap which line carries the secret: let the room read something the newcomer misses. - Point mpechoat at $i, the mob itself, and confirm nothing visible happens on your screen. Aiming at the wrong receiver does not error; it simply plays to an empty seat. That silence is a clue you will learn to recognize. - Cut the mpecho line and feel how the moment loses its shared anchor.
Part Two: Listening For Words
A mob that speaks is charming; a mob that ANSWERS is alive. Part Two is about SPEECH_PROG, the trigger that fires when someone talks near your mob. Its header is different from GREET_PROG's, and the difference is the whole lesson: instead of a percent chance, the header carries the WORDS to listen for. The word all means react to any speech at all; one or more keywords mean react only when one of them is heard; and a header starting with the letter p means match the rest as one exact phrase.
One testing warning before you begin, and it is the most common source of confusion in all of MUDProg. The mudprog dummy test command fires a PRETEND event whose message is the single word test. A block headed SPEECH_PROG all fires happily on that. A block headed SPEECH_PROG hello does NOT, because the word hello appears nowhere in the word test, and the engine correctly stays silent. This is not your script being broken. Test keyword blocks the honest way: stand in the room and say the keyword out loud. Your own speech counts; the mob hears you. The only speech a mob never reacts to is its own, which is what keeps it from arguing with itself forever.
Exercise 6: The Parrot
The goal. A parrot: whatever anyone says in the room, the mob repeats it back, wrapped in squawks. This teaches the catch-all header and your first message code.
Hints. The header you want is SPEECH_PROG all. The code $G holds the text of whatever message fired the trigger; for a speech trigger, that is the spoken line, exactly as spoken.
The worked solution.
SPEECH_PROG all
say Squawk! $G! Pretty words! Squawk!
~
Line by line.
- SPEECH_PROG all is the header. SPEECH_PROG means someone in my room just said something, and all means react to every single utterance, whatever the words. - The say line speaks, splicing in $G, the heard sentence, between the squawks. Say hello parrot in the room and the mob says Squawk! hello parrot! Pretty words! Squawk! - ~ ends the block.
Fire it with mudprog dummy test SPEECH_PROG and the parrot squawks the word test, because that is the pretend message the test carries. Then say something real in the room and hear it come back properly.
Try changing this.
- Say several different sentences and watch each come back. One block, infinite parroting. - A catch-all this eager is unbearable on a real NPC; every sentence anyone says gets a reply. Keep the parrot, but remember the feeling. Exercise 11 shows the polite version. - Two catch-all parrots in the same room cannot squawk at each other forever: each ignores its own voice, and each reacts only to players.
Exercise 7: Loud And Soft
The goal. A subtle but important pair: the heard message comes in two flavors, and this exercise makes the mob show both at once so you can see the difference with your own eyes.
Hints. $g, lower case, holds the heard text folded to lower case, which is what you usually want for matching words. $G, upper case, holds it exactly as spoken, capitals and all, which is what you want when repeating it back prettily.
The worked solution.
SPEECH_PROG all
say You said $g. Or, as it truly rang out, $G.
~
Line by line.
- The catch-all header again: every spoken line fires it. - The say line uses both codes side by side. Say HELLO THERE in the room, and the mob replies with hello there for $g and HELLO THERE for $G, in one sentence. - ~ ends the block.
Why two flavors exist: scripts often need to ask questions about what was said, and questions are easier when the text is all one case. The lower-case $g is for thinking; the true-case $G is for quoting. You will meet $g again the moment you start combining speech with decisions.
Try changing this.
- Say something in mixed case, like Open The Gate, and compare the two halves of the reply. - Rebuild the parrot from Exercise 6 using $g instead of $G and hear how shouted words come back flattened. - Say a number. Codes carry digits as happily as letters.
Exercise 8: One Magic Word
The goal. Selective hearing. The mob should stay silent through all conversation EXCEPT when someone says hello, and then answer warmly. This is the single most useful pattern in talking NPCs.
Hints. Replace all in the header with the word to listen for. That is the entire change. And remember the Part Two warning: this block cannot be fired by mudprog test; you must say hello in the room.
The worked solution.
SPEECH_PROG hello
say Hello to you too, $N! Manners cost nothing and pay well.
~
Line by line.
- SPEECH_PROG hello fires only when a spoken line CONTAINS hello, anywhere in it, in any case. Why hello there matches. HELLO! matches. A sentence about the weather does not, and the mob stays quiet. - The say line answers, naming the speaker with $N, which for a speech trigger is whoever spoke. - ~ ends the block.
Test it honestly: say a few sentences WITHOUT the keyword first and enjoy the silence, then say hello and collect your greeting. Silence on the misses is as much a success as the answer on the hit.
One sharp edge to know today rather than discover next month: keyword matching is by SUBSTRING. The keyword hello also matches inside othello, because the letters sit inside the longer word. For most keywords this never matters; for short ones it can surprise you. Pick distinctive words, and when a stray match bites, make the keyword longer or use a phrase, which is the next exercise but one.
Try changing this.
- Change the keyword to your own mob's name, so it perks up whenever it is mentioned. Mobs that notice their own name feel uncannily alive. - Say the keyword buried mid-sentence, as in well hello to you, and confirm it still fires. Position never matters; presence does. - Try to fire it with mudprog dummy test SPEECH_PROG and watch nothing happen. Now you have SEEN the testing rule, which beats reading it.
Exercise 9: Three Ways To Ask
The goal. Real players never phrase things the way you expect. Make a baker who serves a loaf whether the customer says bread, mentions food, or just announces they are hungry. One block, three doors in.
Hints. A keyword header can hold SEVERAL words, separated by spaces, and the block fires if ANY ONE of them is heard. The header is a list of alternatives, not a phrase.
The worked solution.
SPEECH_PROG bread food hungry
say One fresh loaf, still warm from the oven, coming right up!
~
Line by line.
- The header lists three keywords. A spoken line containing bread OR food OR hungry, any one of them, fires the block. It does not need all three; each word alone is a key that fits the lock. - The say line is the same happy answer for every door in. The customer who begged is there any food and the one who barked bread get the same loaf. - ~ ends the block.
This is how you meet players where they are. When you build a talking NPC, sit for a minute and say the request out loud five different ways, then put the load-bearing word of each phrasing into the header. Five minutes of imagining saves a hundred players from silence.
Try changing this.
- Add the keywords eat and meal to the list. There is no practical limit on header keywords. - Say a sentence containing TWO of the keywords, like I am hungry, is there bread. The block fires ONCE per spoken line, not once per matched word. - Split the block into two: bread and food answered with the loaf line, hungry answered with something gentler. Same keys, different doors.
Exercise 10: The Exact Phrase
The goal. Sometimes single words are too loose and you want a passphrase: the mob should respond only when it hears the words safe travels together, in that order, and never to safe or travels alone.
Hints. Start the header argument with the single letter p, then the phrase. The p tells the engine to match everything after it as one whole phrase rather than a list of separate keywords.
The worked solution.
SPEECH_PROG p safe travels
say And safe travels to you as well, friend. May the road be kind.
~
Line by line.
- SPEECH_PROG p safe travels fires only when a spoken line contains the words safe travels together, in order. I wish you safe travels matches. Travels can be safe does not, because the words are apart. - The say line returns the courtesy. - ~ ends the block.
Phrases are for farewells, passwords, oaths, and any ritual wording where the ORDER of the words is the point. The classic use is a secret knock: a door guard that opens only to the exact phrase, ignoring every partial guess, which frustrates and delights players in exactly the right proportions.
Try changing this.
- Say safe on its own, then travels on its own, then the phrase. Only the phrase lands. - Make a two-block guard: a phrase block for the exact password that answers warmly, plus a keyword block for password that answers with a hint. Blocks are checked independently, so both doors can exist in one script. - Remember the p eats the whole rest of the header: p safe travels is one phrase, not the letter p plus two keywords.
Exercise 11: The Catch-All Clerk
The goal. Combine narrow and wide. A shop clerk should answer questions containing price or cost with the actual price, and acknowledge ALL other speech with a polite nod, so nobody talking to the clerk is ever flatly ignored. Two blocks, same trigger, different headers.
Hints. A script may hold many blocks for the SAME trigger. When the event fires, every block whose header matches gets to run, in the order written. A keyword block and an all block can therefore both listen at once, and a spoken line containing the keyword will set off BOTH.
The worked solution.
SPEECH_PROG price cost
say The price is five coins, and worth every one of them.
~
SPEECH_PROG all
emote looks up from the ledger and nods along politely.
~
Line by line.
- The first block is the specialist: it listens for price or cost and answers with the number. - Its tilde closes it. Two blocks means two tildes; this is where forgetting one hurts, and Exercise 24 shows exactly how. - The second block is the generalist: all means every spoken line earns the nod, whatever the words. - Its tilde closes the script.
Now the behavior in full. Say something ordinary and the clerk merely nods. Ask what does it cost and the clerk BOTH quotes the price AND nods, because the spoken line satisfied both headers and both blocks ran, in written order. Sometimes that doubling is charming, an answer plus a gesture; when it is not, the fix is to make the catch-all rarer, for instance by giving it a percent header like SPEECH_PROG 25 so it answers one utterance in four, or to make its action so mild it layers under anything, which is what the nod is doing here.
Fire mudprog dummy test SPEECH_PROG and watch a nice demonstration of the testing rule: the pretend message is the word test, so the keyword block stays silent and ONLY the nod fires. Then ask about the price out loud and see both.
Try changing this.
- Swap the order of the two blocks and ask about the price again. The nod now comes before the answer. Order of blocks is order of performance. - Change the catch-all header from all to 25 and chat at the clerk for a while. A mob that reacts to MOST things, not all things, reads as attentive rather than mechanical. - Add a third block listening for haggle discount cheaper that answers firmly. Specialists stack.
Part Three: A Life Of Its Own
Everything so far reacts to players. But walk into any real tavern and the keeper is doing something BEFORE you arrive: wiping a glass, grumbling at the fire. Part Three gives your mob a life it lives whether or not anyone is performing at it. The tools are RAND_PROG, which rolls its own dice every heartbeat, ONCE_PROG, which fires a single time when the mob comes into the world, and mpsleep, which slows a script down so it breathes like a person instead of firing like a machine.
Exercise 12: The Hummer
The goal. Idle life. Left alone, the mob should occasionally hum to itself. No player does anything to cause it; the mob simply lives.
Hints. RAND_PROG rolls on its own, once every heartbeat, which is roughly every two seconds, with the header number as the percent chance per roll. Start it at 100 so your test shows results at once; you will lower it before you walk away, and that lowering is part of the lesson.
The worked solution.
RAND_PROG 100
emote hums a wandering little tune while dusting the shelves.
~
Line by line.
- RAND_PROG 100 is a header with nobody behind it: no arriving player, no speaker. The mob's own heartbeat fires it, and at 100 the roll succeeds every time, meaning an action every couple of seconds. - The emote is the idle behavior itself, written third person like all emotes. - ~ ends the block.
Stand in the room a moment and the humming starts, again and again, every heartbeat, because 100 never misses. Feel how quickly that charming line becomes torture. That feeling is the real lesson of this exercise: idle flavor must be RARE to be flavor. Now lower the header to 5, which fires about once every forty seconds, and stand in the room again. The hum surprises you, and surprise is the whole trick.
You can also fire a roll on demand with mudprog dummy test RAND_PROG, which is handy once the number is low and waiting gets dull; remember a low header still rolls its dice under test.
Try changing this.
- Try 50, then 10, then 3, standing in the room a minute each time. Calibrate your own sense of what each number feels like; that sense is a builder's tool you will use forever. - Change the emote so it fits YOUR mob: a guard cracks knuckles, a cat washes an ear, a ghost flickers. - Leave the room and listen from next door. Nothing carries; the idle life plays only to the room the mob stands in.
Exercise 13: A Repertoire
The goal. One idle habit is a tic; three are a person. Give the mob a small repertoire: a common fidget, an occasional grumble, and a rare piece of pure atmosphere, each with its own frequency.
Hints. Write three separate RAND_PROG blocks, each with its own percent and its own body. Every heartbeat, EACH block rolls its own dice independently, so different numbers give different rhythms. Keep the first at 100 while testing so the test always shows something; the plan is to lower it afterward.
The worked solution.
RAND_PROG 100
emote straightens a crooked picture frame for the hundredth time.
~
RAND_PROG 9
emote mutters something about the price of candles these days.
~
RAND_PROG 4
mpecho A floorboard creaks somewhere in the back room.
~
Line by line.
- The first block, at 100 for now, is the signature fidget, the thing this mob is always doing. After testing you will drop it to perhaps 12, common but not constant. - Its tilde closes it. - The second block, at 9, is the occasional grumble, arriving maybe once a minute. Note it has its own personality: prices, candles, complaint. Idle lines are where backstory leaks out. - Tilde again. - The third block, at 4, is not the mob at all: an mpecho, pure narration, the building itself creaking. Mixing one narrator line into a repertoire makes the whole ROOM feel inhabited. - Final tilde.
Stand in the room a few minutes. The three rhythms interleave and the mob stops feeling scripted, because no fixed pattern repeats. This layered-percents pattern is the standard recipe for every living NPC you will ever ship: one common action, one uncommon, one rare.
Try changing this.
- Lower the first block to 12 and confirm the room settles into a gentle, believable murmur. - On rare occasions two blocks succeed on the SAME heartbeat and both lines print together. Watch for it; it reads as a small flourish, and it is why you keep idle lines short. - Add a fourth block at 2, something almost never seen, as a reward for patient players. Rarity manufactures delight.
Exercise 14: Opening The Shop
The goal. First impressions. The moment your mob comes into existence, before any player has done anything, it should visibly set up shop: one opening action, performed exactly once in its life.
Hints. The trigger is ONCE_PROG, and it is the simplest header in MUDProg: no argument at all, just the name. It fires about a second after the mob loads into the world. There is no player behind it, so do not use $N in the body.
The worked solution.
ONCE_PROG
emote unlocks the door and flips the little sign to open.
~
Line by line.
- ONCE_PROG stands alone, no number, no keywords. Once means once. - The emote is the mob's first act on entering the world. Setup theater: lighting lamps, taking up a post, cracking a neck. - ~ ends the block.
Since your mob loaded long before you attached this script, the real moment already passed; fire it with mudprog dummy test ONCE_PROG to see it play. In live use, the timing takes care of itself: areas load their mobs when players first approach, so a nearby player often genuinely catches the shop opening. And because a respawned mob is a brand new copy, it runs its ONCE_PROG again, which is exactly what you want: the shop reopens.
Try changing this.
- Make it two lines, an emote then a say, for a mob that opens up and then announces it. Both run once. - Combine this exercise with Exercise 13 in one script: an ONCE_PROG opening followed by your three RAND blocks. That pairing, one entrance plus a repertoire, is half of every good NPC already. - A mob that merely walks between rooms does NOT re-fire ONCE_PROG; only a fresh copy does. Watch a wandering mob to confirm.
Exercise 15: Taking A Breath
The goal. Pacing. A mob that fires several lines in the same instant reads like a machine gun. Make a greeter who speaks, visibly rummages for two seconds, and only then finishes the thought.
Hints. The mpsleep command pauses the script for a number of seconds, then continues with the remaining lines. Put it between the two say lines. The shortest possible pause is one second.
The worked solution.
GREET_PROG 100
say Welcome! One moment while I find my spectacles...
mpsleep 2
say Ah, there they are. Now then, $N, what can I do for you?
~
Line by line.
- The header is our familiar every-arrival greeting. - The first say lands immediately, the moment the trigger fires. - mpsleep 2 stops the script right here for two seconds. Nothing else in the mud waits; players move, fights rage, only this script holds its breath. - The second say arrives after the pause, finishing the little scene and finally addressing the visitor by name. - ~ ends the block.
Fire it with test and FEEL the two seconds. That gap is where the character lives: the fumbling, the age, the friendliness. A wall of instant text tells; a paced scene shows. Sprinkle one or two short sleeps through any long speech and it becomes a performance.
Try changing this.
- Stretch the sleep to 5 and feel how the scene sags. Two to three seconds is almost always the sweet spot; longer reads as broken. - Build a three-beat scene: say, sleep 2, emote, sleep 2, say. Rhythm is yours to compose. - Walk OUT of the room during the pause. The final line still plays to the room behind you; scripts do not chase you down the hall. Knowing that spares you confusion later.
Part Four: The Dollar Toolbox
You have met $N, $g, $G, and glimpsed $i. The dollar codes are a family, and this part tours the members a beginner reaches for weekly: polite address, pronouns, the mob's own name, the room's name, and the real dollar sign. There are a few dozen codes in all, catalogued in the mudprog-variables chapter; these exercises give you the handful that carry most scripts.
Exercise 16: Minding Your Manners
The goal. A maitre d' who addresses every guest correctly without ever knowing their name: sir to the gentlemen, madam to the ladies, and the right pronoun in the gesture that follows.
Hints. $y produces sir or madam according to the gender of whoever fired the trigger. $e produces he or she for the same person. Codes for the SOURCE of the event use lower-case letters; you never look the gender up yourself, the code does it at the moment the line runs.
The worked solution.
GREET_PROG 100
say Right this way, $y. Your table by the window is waiting.
emote pulls out a chair as $e looks around the room.
~
Line by line.
- The header fires per arrival, and every code in the body will be answered by THAT arrival's details. - In the say line, $y becomes sir or madam to fit the guest. One script serves every guest correctly forever. - In the emote, $e becomes he or she, so the room reads a grammatical sentence about the right person: pulls out a chair as she looks around the room. - ~ ends the block.
The relatives you will want soon: $s is him or her, and $m is his or her, both for the source. There is a parallel set for the mob itself, which the next exercise touches. When you are unsure which code you need, write the sentence in plain English first, underline the words that must change per person, and then look up one code per underline.
Try changing this.
- Add a line using $s, such as an emote about showing $s to the table. - Greet with $y and $N together: formal address plus name reads as a maitre d' who checked the reservation book. - Test as yourself and confirm the codes match YOUR character's gender; under test, you are the guest.
Exercise 17: Talking About Yourself
The goal. A mob that refers to itself by name, in narration AND in its own speech, the way self-important shopkeepers do, without you ever typing the name into the script.
Hints. $i is the scripted mob's own name. It works in every command, including inside the mob's own say line, which produces the slightly pompous third-person self-reference this exercise is after.
The worked solution.
GREET_PROG 100
mpecho $i mutters over a cluttered ledger without looking up.
say Nothing happens in this shop without $i hearing of it, $N.
~
Line by line.
- The header is the usual every-arrival greeting. - The mpecho narrates the mob by name: the code $i is replaced by the mob's name, so the narration stays correct even if you rename the mob next week. Never hard-type a mob's name into its own script; the day you rename it, every hard-typed line silently rots. - The say line has the mob speak its OWN name, third person, plus the visitor's name from $N. Two people named in one line, neither typed. - ~ ends the block.
There is a sibling code, capital $I, which produces the mob's short description instead of its bare name, useful when the short is more evocative. And the mob's own pronouns are $j for he or she, $h for him or her, and $k for his or her, the self-directed mirror of the guest codes from Exercise 16.
Try changing this.
- Swap $i for $I in the mpecho and compare the flavor. - Write a narrator line using $k, such as a mention of $k spectacles. - Rename your practice mob, change nothing in the script, and greet again. The lines follow the new name. That is the entire argument for codes over typing.
Exercise 18: Prices, Places, And Real Dollars
The goal. A barker who names the very room it stands in and quotes a price, including one REAL dollar sign printed as text, which requires knowing the one escape rule in MUDProg.
Hints. $d is the name of the room the mob stands in. And because the dollar sign starts every code, printing an actual dollar sign needs a doubled one: $$ prints as a single $.
The worked solution.
GREET_PROG 100
say Meals are five coins flat here at $d, and the old $$5 sign out front is a lie.
~
Line by line.
- The header, as ever, fires per arrival. - In the say line, $d is replaced by the current room's name, so the barker always names wherever it stands; move the mob and the pitch updates itself. Then $$5 prints as the literal text with one dollar sign, because $$ is the written form of a real $. Without the doubling, the engine would read $5 as a code and your sign would vanish from the sentence. - ~ ends the block.
The doubled dollar matters more than it looks: menus, ransom notes, and imported text all carry stray dollar signs, and each one must be doubled or the engine will try to interpret what follows it. When a message prints with a mysterious hole in it, count your dollar signs first.
Try changing this.
- Walk the mob to a different room, greet again, and watch $d change. - Print a lone $$ with nothing after it, then a $$ directly before a word, and study what survives. - Combine $d with $i: the mob naming itself and its shop in one line of patter is the whole barker trade.
Part Five: Choices And Chances
Until now every block does the same thing every time. Characters are made of CHOICES: noticing who walked in, sizing them up, remembering them. The tool is the if line, which asks a question; when the answer is yes the lines under it run, and when it is no the lines under the optional else run instead, and the endif line closes the question the way the tilde closes a block. The questions themselves are asked with small tools called functions, written as a name with parentheses, like ispc($n). Part Five teaches the four you will use daily, plus dice and memory.
Exercise 19: Player Or Creature
The goal. Not everything that walks through a door is a customer. Wandering monsters and stray NPCs enter rooms too, and they fire GREET_PROG just like players do. Make a greeter who welcomes people and merely sniffs at creatures.
Hints. The function ispc($n) answers the question is $n a player character, yes or no. Put it on an if line, give the two behaviors their own lines, and close with endif. The extra indentation inside the if is optional but keep the habit; it makes the two paths visible at a glance.
The worked solution.
GREET_PROG 100
if ispc($n)
say A living, breathing customer! Welcome, $N, welcome!
else
emote sniffs the air once and goes back to sweeping.
endif
~
Line by line.
- The header fires for EVERY arrival, player or not. The sorting happens inside. - if ispc($n) asks whether the arriver is a player. The lines that follow, up to the else, run only on yes. - The say line is the yes path: the warm welcome, by name. - else flips the track: everything from here to endif runs only on no. - The emote is the no path: a wordless dismissal for the rat that wandered in. - endif closes the question. Every if must have its endif, exactly as every block must have its tilde. - ~ closes the block itself.
Under test YOU are the arriver, and you are a player, so the yes path runs. To watch the no path honestly you need a non-player to walk in, which in a quiet test room may take arranging; trust the else for now, and Exercise 21 gives you a choice whose no path you can trigger yourself.
Try changing this.
- Swap the two paths so creatures get the speech, and enjoy how wrong it feels. Casting the right behavior to the right audience is taste, and taste is trained by deliberately doing it backward once. - Use isnpc($n) instead, which asks the opposite question, and swap the paths to match. Same behavior, mirrored logic. - Drop the else and endif down to a bare if with only a yes path. Nothing at all happening on no is often the right choice.
Exercise 20: The Velvet Rope
The goal. Judgement by number. The doorkeeper of a members-only back room should size up each arrival's level: seasoned adventurers are told the door is open to them, newcomers are told to season themselves first.
Hints. The function level($n) produces the arrival's level as a number, and an if line can COMPARE numbers: the symbols >= mean is at least. So if level($n) >= 20 reads is the arrival level twenty or higher.
The worked solution.
GREET_PROG 100
if level($n) >= 20
say The back room is open to you, $N. Few earn that door.
else
say The back room is open to veterans only, friend.
endif
~
Line by line.
- The header fires per arrival. - The if line fetches the arrival's level and compares it to 20. At 20 or above, yes; at 19 or below, no. - The yes path flatters the veteran by name. - else pivots to the no path. - The no path is polite but firm, and pointedly does not use the name; the doorkeeper has not bothered to learn it. - endif closes the question, ~ closes the block.
The comparison family: >= is at least, <= is at most, > is more than, < is less than, == is equal to, and != is not equal to. All of them work anywhere an if works. Numbers are the spine of gating: levels, health, coin counts, group sizes all reduce to a number and a comparison.
Try changing this.
- Set the threshold to a level just ABOVE your own and greet: you get the brush-off. One below: the welcome. Bracketing your own number is the quickest honest test of any comparison. - Add a middle tier by putting a second if inside the else, for instance a special line for levels 10 through 19. Ifs nest happily. - Gate on something else entirely: the function hitprcnt($n) is the arrival's health as a percent, so a healer who fusses over anyone below 50 is this same script with two words changed.
Exercise 21: The Name On The List
The goal. A grudge. The innkeeper has exactly one name on the blacklist, grendel, and throws that one visitor out on sight; everyone else is welcomed and pointedly told they are not on the list.
Hints. The function name($n) produces the arrival's name as text, and == compares text just as happily as numbers, ignoring capital letters while it does. Compare against the name written plainly, no quotes needed.
The worked solution.
GREET_PROG 100
if name($n) == grendel
say You! Out! You know exactly what you did, Grendel!
else
say Come in, friend. You are not on my list of troublemakers.
endif
~
Line by line.
- The header fires per arrival. - The if line fetches the arrival's name and compares it to grendel. The comparison ignores case, so Grendel, GRENDEL, and grendel all match; you can write the script name in lower case and never worry. - The yes path is the eviction, reserved for one soul in the world. - The else and its say serve everyone else, and the line is written to be funny precisely because most visitors never knew there WAS a list. - endif and ~ close question and block.
Test it as yourself: unless your name happens to be grendel, you land in the else, which is the path almost every visitor will take. Then change the blacklisted name to YOUR OWN name, greet again, and get yourself thrown out of your own practice room. Now you have personally walked both paths of an if, which no amount of reading equals.
Try changing this.
- Blacklist a friend's name and invite them to walk in. - Flip the logic with !=, so the if reads name($n) != grendel and the welcome comes first. Same behavior, different emphasis; scripts read best when the COMMON path comes first. - Whitelist instead: one beloved regular greeted by a private joke, everyone else with the standard line. A grudge and a friendship are the same script with the temperature reversed.
Exercise 22: The Coin Flip
The goal. Unpredictability inside a block. Every visitor gets greeted, but the GREETING itself is decided by a coin flip the moment they walk in: heads is a good omen, tails a doubtful one.
Hints. The function rand(50) is true fifty times out of a hundred, a fair coin. Put it on an if line. Anything that should happen REGARDLESS of the flip goes before the if.
The worked solution.
GREET_PROG 100
emote flips a worn copper coin and slaps it down on the counter.
if rand(50)
say Heads! Fortune smiles on this meeting, $N.
else
say Tails. Let us hope the coin is wrong about you.
endif
~
Line by line.
- The header fires per arrival, and the block ALWAYS starts the same way: the emote plays before any decision, so every visitor sees the coin hit the counter. - if rand(50) rolls the dice right there, a fresh flip per visitor. - The yes path is heads, warm and personal. - The no path is tails, and note it works as theater either way; a good random pair is written so both outcomes are in character. - endif and ~ close up.
Compare this with the header percent from Exercise 1. GREET_PROG 50 would greet only half of all visitors, silence for the rest. This script greets EVERYONE and varies WHAT it says. Header chance decides whether the block runs at all; rand() inside decides what happens once it does. You will use both, often together.
Try changing this.
- Fire test five times in a row and tally heads against tails. Small samples wobble; that wobble is what randomness feels like. - Change 50 to 90 for a mob that is optimistic about nearly everyone. - Chain a second flip inside the tails path, an if rand(50) within the else, for a coin that sometimes demands two out of three. Nested chance builds slot machines, fortune tellers, and unreliable oracles.
Exercise 23: First Time, Every Time
The goal. Memory. The tavern should greet a brand new face one way and a returning one another. First visit: announce a free meal. Every visit after: familiar warmth. The mob must REMEMBER between events.
Hints. The command mpsetvar stores a named value on an object: mpsetvar $i lantern_met 1 stores the value 1 under the name lantern_met on $i, the mob itself. The function var reads it back: var($i lantern_met) produces what was stored, or nothing at all if it never was, and nothing fails the == 1 test, which is exactly the hinge this script turns on.
The worked solution.
GREET_PROG 100
if var($i lantern_met) == 1
say The Lantern welcomes you back, $N. Sit anywhere you like.
else
mpsetvar $i lantern_met 1
say A brand new face! The Lantern always feeds a first visit free.
endif
~
Line by line.
- The header fires per arrival. - The if line reads the variable lantern_met from the mob. On the very first firing nothing has ever been stored, the reading comes back empty, empty is not 1, and the answer is no. - So the FIRST run takes the else path, and its first line is the crucial one: mpsetvar writes 1 into lantern_met, planting the memory. - The else's say celebrates the new face. - Every LATER firing finds the stored 1, the if answers yes, and the returning-guest line plays instead. The mob has a past. - endif and ~ close up.
Fire test twice in a row and watch the greeting change between the first and second firing. That change is memory, and it is the doorway to quests, one-time gifts, and mobs that hold grudges.
Two honest caveats at this level. First, this memory is SHARED: the variable lives on the mob, so the second visitor ever is already greeted as a regular. Per-player memory is one change away, storing on $n instead of $i, so each player carries their own flag; the mudprog-variables chapter walks through it. Second, the memory lives on the MOB, not in the script: clearing or replacing the script does not clear lantern_met. If you re-run this exercise and the mob skips straight to the welcome-back line, that is last time's memory talking. Store a fresh name like lantern_met2, or reset the old one with a temporary block whose body is mpsetvar $i lantern_met 0.
Try changing this.
- Change both $i references to $n so the memory rides on the player, and test with a friend: each of you gets your own first-time greeting. One letter, profoundly different behavior. - Add a third state: store 2 on the second visit and greet the third visit differently again. Chained ifs on == 1 and == 2 will carry you until you learn switch in the mudprog-flow chapter. - Let a SPEECH_PROG block set the memory instead, so it is something a player SAYS that the mob never forgets.
Part Six: Graduation Pieces
Four final exercises. One teaches you to FIX a script, which matters more than writing them, because every scripter spends half their life staring at a block that will not fire. One puts a real object into the world. One shows two blocks sharing a trigger with different dice. And the last assembles everything in this workbook into one finished, shippable NPC.
Exercise 24: Fix The Broken Greeter
The goal. Below is a script another builder swears is finished: a greeting, plus an answer for anyone who mentions the weather. It saves without complaint and behaves bizarrely. Attach it AS PRINTED, observe the symptoms, diagnose the fault, and repair it.
The broken script, exactly as handed to you.
GREET_PROG 100
say Good day! Lovely weather for it.
SPEECH_PROG weather
say Aye, lovely weather. I may have mentioned that.
~
Attach it and watch the symptoms. Walk in, and the mob says BOTH lines, back to back, in one breath. Then say the word weather in the room, and nothing happens at all. The greeting is doubled and the listener is deaf.
Now diagnose it, and use the tool, not your eyes: type mudprog dummy and read the Triggers line. It lists GREET_PROG and nothing else. The SPEECH_PROG the builder thought they wrote is not registered, and that is the whole story. There is no tilde after the first say, so the engine never saw the first block end. Everything down to the single ~ is ONE block: a GREET_PROG whose body is three lines, one of which happens to be the TEXT SPEECH_PROG weather. When the greeting fires, the engine runs all three lines: the first say speaks, the SPEECH_PROG line is tried as an ordinary game command and fails silently, and the second say speaks. Hence both lines on entry, and no listener anywhere.
The rule this burns in: a missing trigger on the Triggers line means a missing tilde above it. The engine cannot warn you, because a body line that LOOKS like a header is still a legal body line; only the Triggers list betrays the truth.
The repaired script.
GREET_PROG 100
say Good day! Lovely weather for it.
~
SPEECH_PROG weather
say Aye, lovely weather. I may have mentioned that.
~
One character was added: the tilde ending the first block before the second begins. Attach the repair, check mudprog dummy, and the Triggers line now shows both GREET_PROG and SPEECH_PROG. Walk in for a single clean greeting, then say the weather is dreadful and collect the smug reply.
Try changing this.
- Break it the OTHER way: delete the final tilde instead. The engine quietly forgives a missing tilde at the very end of a script, closing the last block for you, which is precisely why the missing MIDDLE tilde is the dangerous one. - Sabotage a friend's practice script by removing one tilde, and time how fast they find it with the Triggers line. Diagnosis is a skill; drill it while the stakes are zero. - Misspell a trigger, say GREETING_PROG, save, and read the Triggers line. It lists your typo verbatim: registered, plausible, and hooked to no event in the game, the other great silent failure.
Exercise 25: The Free Sample
The goal. Generosity you can pick up. The mob should greet a hungry looking traveler and produce an actual meal, a real object that lands in the room and can be taken, examined, and eaten.
Hints. The command mpoloadroom clones a fresh copy of an item, named by its file path, into the mob's room. The path to a harmless practice meal on this mud is /obj/meal. Wrap the object's appearance in speech and gesture so the moment reads as service rather than conjuring.
The worked solution.
GREET_PROG 100
say You look half starved, $N. Here, this one is on the house.
mpoloadroom /obj/meal
emote sets a steaming meal down on the counter with a flourish.
~
Line by line.
- The header fires per arrival. - The say sets up the gift and names the guest. - mpoloadroom /obj/meal is the working heart: the engine loads the item file at that path and places a brand new copy of it in the room. The command itself prints NOTHING, which is why the lines around it matter. - The emote is the presentation, giving the silent object a visible arrival. Look at the floor afterward and there it sits, a generic meal, real and takeable. - ~ ends the block.
Every greet drops another meal, so a busy doorway becomes a buffet; clear the script when you finish playing, and in real building you would gate generosity behind the memory pattern from Exercise 23 so the free one is truly one. For real areas, ask a senior builder for the item paths that belong to your zone, and practice with harmless objects like /obj/meal and /obj/torch first. A mistyped path does not error; it simply loads nothing, another of those quiet no-ops that the silence itself reports.
Try changing this.
- Swap the path for /obj/torch and rewrite the two flavor lines to match. The pattern is identical for any item in the game. - Use mpoload without the room suffix and the item goes into the MOB's inventory instead of onto the floor, which is how you stock a mob before it formally hands something over. - Move the whole body under SPEECH_PROG bread food hungry from Exercise 9, so the meal answers a spoken request instead of mere arrival, and test it by saying the keyword aloud. That merge of Part Two and this part is real questmaster anatomy.
Exercise 26: The Bored Doorman
The goal. Layered reactions. A doorman should acknowledge EVERY arrival with a professional nod, and for roughly one arrival in four, add a weary remark on top. Certainty and chance, side by side on the same trigger.
Hints. Two blocks, both GREET_PROG, different numbers. When the event fires, each block checks its own header independently: 100 always passes, 25 passes about a quarter of the time. On the lucky arrivals BOTH bodies run, in the order written.
The worked solution.
GREET_PROG 100
emote gives the newcomer a slow, professional nod.
~
GREET_PROG 25
say Busy night. You are the fourth one through that door this hour.
~
Line by line.
- The first block, at 100, is the guarantee: every arrival, without exception, gets the nod. This is the doorman's floor, the behavior that never fails. - Its tilde closes it. - The second block, at 25, rolls fresh dice per arrival. Roughly one visitor in four also hears the remark, always AFTER the nod, because blocks run in written order. - Its tilde closes the script.
Walk in and out eight or ten times. The nod is metronome steady; the remark surprises. This guaranteed-floor-plus-random-sparkle shape is one of the best patterns in NPC design, because the floor makes the mob reliable and the sparkle makes it alive. It is Exercise 13's layering, transplanted from idle time onto a player-driven trigger.
Try changing this.
- Add a third block at 5, a rare confidence for one visitor in twenty. Three layers on one trigger, three distances of intimacy. - Give the 25 block a keyword-style zapper instead: the header GREET_PROG -level 20 fires the remark only for arrivals of level twenty or more, a taste of the mask headers the mudprog-triggers chapter catalogs. - Put the say in the 100 block and the emote in the 25 and feel how wrong it is for the SPEECH to be constant and the gesture rare. Which behavior deserves certainty is a writing decision, not a technical one.
Exercise 27: The Lantern Keeper
The goal. Graduation. Build the keeper of the Lantern: a complete NPC with an opening ritual when she comes into the world, a greeting that tells players from creatures, a kitchen that answers three different words for supper with a real meal, a soft answer to a whole farewell phrase, and a quiet idle habit underneath it all. Five blocks, every part of this workbook, one character.
Hints. You have built every piece already. ONCE_PROG from Exercise 14. The ispc split from Exercise 19. Keywords plus mpoloadroom from Exercises 9 and 25. The phrase from Exercise 10. The low RAND from Exercise 12. All that is new is holding five blocks in one script, five headers, five tildes, and checking the Triggers line shows all five.
The worked solution.
ONCE_PROG
emote lights the lanterns one by one and settles in behind the bar.
~
GREET_PROG 100
if ispc($n)
say Evening, $N. Kitchen is open and the fire is high.
else
emote watches the creature carefully, one hand below the bar.
endif
~
SPEECH_PROG stew bread supper
say One hot supper, coming right up!
mpoloadroom /obj/meal
emote ladles something rich and steaming into a bowl.
~
SPEECH_PROG p good night
say Sleep well, friend. The Lantern will be lit when you wander back.
~
RAND_PROG 6
emote wipes down the bar with a rag that has seen better years.
~
Line by line, block by block.
- The ONCE_PROG is her entrance into the world, played a second after she loads: lanterns, then her post. Fired once per life; a respawn is a new life and a new lighting. - The GREET_PROG receives every arrival and immediately splits it: a player gets the welcome by name, while a wandering creature gets the wary, wordless watch, one hand below the bar. Two receptions, one door. - The first SPEECH_PROG is the kitchen. Three keywords cover three phrasings of hunger, and the body is a three-beat serving: the call, the real meal arriving via mpoloadroom, and the ladling emote that makes the object's silent appearance into a scene. - The second SPEECH_PROG listens for the whole phrase good night, and only that phrase, and sends the guest off with the image the tavern is named for. A phrase block beside keyword blocks is fine; each header judges independently. - The RAND_PROG at 6 is her pulse, a bar wiped roughly once a minute, rare enough to charm. It runs beneath everything else and never collides with it.
Now test like a professional, in this order. First, mudprog dummy: the Triggers line must list ONCE_PROG, GREET_PROG, SPEECH_PROG, and RAND_PROG; if anything is missing, you know from Exercise 24 exactly what to hunt for. Second, fire mudprog dummy test ONCE_PROG to watch the lighting. Third, walk out and in for the greeting. Fourth, say supper aloud and collect the meal, remembering that the kitchen keywords cannot be fired by the test command. Fifth, say good night, whole phrase. Sixth, idle a minute and catch her wiping the bar. Six checks, and she is done: a character built from plain text, by you.
Try changing this.
- Give her the memory from Exercise 23, so first-time guests hear about the free first bowl and regulars are welcomed back by name. - Give her the manners from Exercise 16, addressing guests as $y in the greeting. - Give the kitchen a coin flip from Exercise 22: most suppers arrive promptly, but sometimes the kitchen is behind and she says so, with an mpsleep before the bowl lands. - Then stop polishing and ship her. A finished NPC in the world teaches you more than three perfect ones in your notes.
Where You Stand Now
Count what your hands have done: blocks and tildes, five triggers, speech and gesture and narration, private and public lines, catch-alls, keywords and phrases, idle repertoires, one-time setup, pacing, a toolbox of dollar codes, decisions with if and else, comparisons of numbers and names, dice inside and outside blocks, memory that outlives the moment, a real object loaded into the world, layered chances, and a whole diagnosed-and-repaired bug. That is not a beginner's list. That is a working scripter's kit.
Where to go next, in the order most people enjoy:
- mudprog-triggers catalogs all sixty-odd triggers, including combat, items given and taken, rooms, doors, and vendors. - mudprog-variables goes deep on dollar codes, per-player and permanent memory, and splicing stored values and function results into text. - mudprog-commands tours every mp command, from movement and combat to quests and money. - mudprog-flow adds switch, while, and the rest of control flow to your if. - mudprog-cookbook is fifteen complete NPCs and rooms to pillage for parts.
The reference card for everything is help mudprog. Now go back to that first three-line greeter you wrote an hour ago, smile at how small it was, and put a keeper in every empty room you own.
This is the second workbook of the MUDProg guide: thirty guided exercises at the intermediate level, each with a complete worked solution and a walkthrough of every line. Where the first workbook practiced single triggers and simple speech, this one practices the skills that turn a talking mob into a living one: memory that survives between visits, counters that tick, loops that count and hunt, questions asked of the live world, items and coin changing hands, real status conditions, scenes that unfold over seconds instead of instants, and finally scripts made of several blocks that cooperate through shared memory.
You do not need to have programmed before, but you should have read the basics chapter, and you will get much more from these pages if you have at least skimmed the variables and flow chapters, because this workbook leans on both constantly. Everything it uses is re-explained briefly the first time it appears, so if you meet something unfamiliar, keep reading rather than stopping; the worked solution will show it in action.
How To Work An Exercise
Every exercise has the same shape. First comes the task, written the way a head builder might hand it to you: a description of the behavior wanted, in plain words. Then come one or two hints, pointing at the tools the solution reaches for. Then the worked solution, a complete script you can attach exactly as printed, followed by a walkthrough and a set of variations to try on your own.
The honest way to use this workbook is to read the task, close the page, and try to write the script yourself before looking at the solution. Struggling for ten minutes teaches more than reading for an hour. When you do look, compare shapes rather than words: your script and the solution can be worded completely differently and both be right. The solution is one good answer, never the only one.
Your Practice Bench
Work these exercises on a harmless practice mob in a quiet room. Stand with it and use the mudprog command:
mudprog <mob> edit - Type the script in, tilde after each
block, a single dot to finish.
mudprog <mob> - Show the script and its triggers. mudprog <mob> test GREET_PROG - Fire a trigger now, with you as the
person who set it off.
mudprog <mob> clear - Remove the script.
A few of the solutions load practice objects out of thin air. They use only the standard harmless test items: /obj/meal, /obj/torch, /obj/armor, and /obj/container. Nothing in this workbook touches real areas, real loot, or other players.
One caution that bites everyone once: some telnet clients mangle dollar signs typed at the prompt. If your codes vanish before they reach the mud, use the mudprog line editor, or put the script in a file and run it with scripttest runfile (see help scripttest).
Two Facts About The Test Bench
Both of these matter for almost every exercise below, so read them before you start.
First, the test option is a stage rehearsal, not a real event. When you fire a trigger with mudprog <mob> test, you are the source, you are also the target, there is no real item in the event, and the message slot carries the single word test. So $N and $T both read as your name, $o reads as the word something, and $G reads as test. A block that inspects a given item will therefore always take its no-item branch under test; exercise 19 turns that quirk into a lesson. To rehearse the real thing, do the real thing: walk out and back in for GREET_PROG, speak the words for SPEECH_PROG, hand over the item for GIVE_PROG.
Second, memory persists between firings, which is the whole point of this workbook and also its main testing nuisance. A note stored on the practice mob with mpsetvar stays there until that copy of the mob is destroyed, even if you clear and replace the script. A note stored on YOU is saved with your character and lasts forever. So a counter keeps counting across your test firings, and a once-per-player flag written during yesterday's testing is still on you today. When an exercise seems to be skipping its first-time branch, that is almost always why. Three clean ways out: spawn a fresh practice mob; erase a note by storing nothing over it, as in mpsetvar $n flag with no value; or give the script a reset block, a trick exercise 3 demonstrates and the finale repeats.
Part One: Remembering Things
Four exercises on stored variables, the notes any object can carry. The commands and forms practiced here: mpsetvar to write a note, the angle form $<owner name> to read one into text, and the var() function to read one inside a condition. If the variables chapter is fresh in your mind, treat these first four as a warm-up lap; they set up idioms every later exercise assumes.
Exercise 1: The Mood Board
The task. An innkeeper announces her mood to each arrival. The mood is not written into the sentence; it is stored in a note called mood on the innkeeper herself, and the greeting reads the note. To prove the note is truly live, the script then changes the mood and announces it again, in the same breath.
Hint. Storing is mpsetvar $i mood <word>; reading into text is the angle form, a dollar sign and angle brackets around the owner and the name.
A worked solution:
GREET_PROG 100
mpsetvar $i mood cheerful
say Ask anyone, my mood today is $<$i mood>.
mpsetvar $i mood stormy
say And just like that, my mood is $<$i mood>.
~
Walkthrough. The first line files the word cheerful in a note called mood on the host, $i. The first say reads the note back with $<$i mood>, so the room hears the word cheerful spoken aloud. Then the third line overwrites the note, no ceremony needed, storing over an existing name simply replaces the old value. The final say reads the same note again and gets the new value. One note, two reads, two different answers, because the angle form is evaluated at the moment its line runs, not when the script was written.
What if you fire it twice? Exactly the same output both times, because the script seeds the mood itself at the top of every run. That seeding habit, set the state you depend on before you depend on it, is the single best trick for writing scripts that behave predictably under test, and half the solutions below use it.
Variations to try. Move the second mpsetvar above the first say and watch the first announcement change. Store a two-word mood such as quietly furious and see that the value keeps its spaces, everything after the name is the value. Read the note from a DIFFERENT script by attaching a second mob and having it say $<innkeeper mood>, naming the owner instead of using a code.
Exercise 2: The Doorstep Ledger
The task. A doorman remembers the name of the last person who came through the door. Each arrival is told who came before them, and then becomes the remembered name themselves. The very first arrival, when no one has been remembered yet, should be told they are the first.
Hint. A note that was never written reads back as empty, and two single quote marks stand for the empty value in a comparison. Test emptiness FIRST, then update the note last, so the current visitor does not overwrite the answer before it is spoken.
A worked solution:
GREET_PROG 100
if var($i last_guest) == ''
say You are the first face I have seen all day, $N.
else
say Before you, $N, the last face through that door was $<$i last_guest>.
endif
mpsetvar $i last_guest $N
emote scratches a fresh line into a well-worn doorpost.
~
Walkthrough. The condition var($i last_guest) == '' asks whether the note is still empty, which is only true before anyone has been remembered. On the first firing the yes branch runs. On every later firing the note holds a name and the else branch reads it into the sentence with the angle form. Only after the decision does the script store the current visitor's name, and here is the detail that matters: $N inside the stored value is substituted at the moment of storing, so the note holds the actual letters of the name, not the code. The closing emote runs on every branch, a habit worth copying, because a line that always runs gives you something reliable to watch for when testing.
The classic mistake here is putting the mpsetvar line above the if. Do that and every visitor is told that the last face through the door was their own, which is technically true and completely useless. Order of lines is logic, not decoration.
Variations to try. Remember the time too, storing a second note, and speak both. Remember the last THREE visitors by shuffling three notes, last into second-last into third-last, and notice how quickly that gets clumsy, which is a good motivation for the counters of Part Two.
Exercise 3: The Fortune Teller Who Never Repeats Herself
The task. A fortune teller reads each customer's fortune exactly once, ever. A returning customer, even weeks later, even after the teller herself has died and respawned, is refused. You also want a way to make her forget someone, for testing and for mercy.
Hint. Memory that must survive the mob's own death cannot live on the mob. Store the flag on the PLAYER, whose notes are saved with their character. For the forgetting, remember that storing nothing erases a note, and that a second PROG block can listen for a keyword.
A worked solution:
GREET_PROG 100
if var($n fortune_told)
say The cards do not speak twice, $N. You have had your reading.
else
mpsetvar $n fortune_told yes
say Sit, $N. The cards see a long road and a stubborn heart.
endif
mpecho The candles gutter as the fortune teller falls silent.
~
SPEECH_PROG forget
mpsetvar $n fortune_told
say Very well, $N. I wipe the cards clean for you.
~
Walkthrough. The bare test if var($n fortune_told), with no comparison, counts as yes when the note holds anything except emptiness, a zero, or the words no and false. The first visit finds nothing, takes the else branch, writes yes onto the player, and reads the fortune. Every visit after that, forever, finds the note and refuses. Notice where the memory lives: on $n, the player. The teller can be killed, reset, and respawned a hundred times; the receipt travels with the customer, and every fresh copy of the teller reads it back.
The second block is the eraser. SPEECH_PROG forget fires when someone speaks a line containing the word forget near the teller, and its one working line is mpsetvar $n fortune_told with no value at all, which stores emptiness over the flag, and emptiness counts as not set everywhere that matters. Say forget, walk out, walk back in, and you get a fresh reading. Keep a reset block like this on anything you are testing that writes player notes; your future self will thank you.
One more habit on display: the mpecho line about the candles sits after the endif, so it runs on both branches. Refused or read, the scene ends the same way.
Variations to try. Store the fortune itself rather than a yes, picking one of three texts with the randnum function, and have the refusal quote their original fortune back at them from the note. Gate the forget block with a zapper mask so only staff can trigger it.
Exercise 4: The Price Tag
The task. A baker conjures a fresh loaf, writes a price and a maker's mark on the loaf itself, and then reads both back from the loaf, not from her own memory, when she announces it. The point of the exercise: notes can live on items, and any script that can see the item can read them.
Hint. After a load command, the freshly created object is $b for the rest of the run. Notes go on it exactly as they go on anyone else.
A worked solution:
GREET_PROG 100
mpoloadroom /obj/meal
mpsetvar $b price 15
mpsetvar $b baker Old Hobb
say Fresh from the oven. The tag says $<$b price> gold, baked by $<$b baker>.
mpecho A hungry porter snatches it up at once, coins left on the sill.
mpjunk $b
~
Walkthrough. mpoloadroom creates a meal on the floor of the room, and from that line on, $b means that meal. Two notes are stuck to it: price holds 15, and baker holds Old Hobb, two words, because the value is everything after the name. The say then reads both notes off the meal with the angle form. Neither value passed through the baker's own notes at all; the meal is carrying its own paperwork. The closing pair is housekeeping: a line of fiction to cover the disappearance, then mpjunk $b to destroy the loaf so your practice room does not slowly fill with bread. Cleaning up what you conjure is good manners in shared rooms and good discipline everywhere.
Why this pattern matters beyond bread: an item's notes are a public notice board. A vendor script can price goods this way, a quest mob can mark an item blessed, and a completely different script, on the room or on a guard, can read the same note later by naming the item, as in var(meal price == 15). Data travels with the thing it describes.
Variations to try. Load two meals with different prices and read each back, noticing that $b only ever means the LATEST load, so you must read the first meal's notes before conjuring the second, or read them later by name. Put the price note on the ROOM instead and see how the meaning changes: now it is the room's price for any meal, not this meal's own price.
Part Two: Counters, The Heartbeat Of Memory
A counter is just a note holding a number, plus one line of arithmetic to move it. That small pattern, read the note, add or subtract, store it back, powers visitor tallies, limited stock, punch cards, boss phases, and half the clever NPCs you have ever met. The arithmetic is done by the MATH function inside the percent form, and the result is usually parked in a temporary slot with mpargset so the rest of the run can use it cheaply. Three exercises, three classic shapes: count up forever, count down to empty, count per player to a reward.
Exercise 5: The Turnstile
The task. A gate clerk announces each arrival's guest number: the first visitor is number 1, the next number 2, and so on, climbing forever, surviving between firings.
Hint. The count must live in a note, because slots are wiped every run. And before you do arithmetic on a note, make sure it holds a number: arithmetic on an empty, never-written note gives nonsense.
A worked solution:
GREET_PROG 100
if var($i tally) == ''
mpsetvar $i tally 0
endif
mpargset 1 $%math($<$i tally> + 1)%
mpsetvar $i tally $1
say Welcome in. You are guest number $1 by my count.
~
Walkthrough. The opening if is the seeding step, and it is not optional. The first time this ever runs, the note called tally does not exist, and the MATH function fed an empty value does not helpfully assume zero; it produces a wrong answer. So the script checks for emptiness and files a 0 to start from. Every later run finds a number and skips straight past.
The mpargset line is the whole engine of the exercise, and it is worth reading from the inside out. Innermost, $<$i tally> reads the current count as text, say 4. Around that, math computes 4 + 1. Outermost, mpargset drops the answer, 5, into temporary slot 1, where it is readable as $1 for the rest of this run. The next line files 5 back into the note, so the increase survives until the next guest, and the say announces it from the slot. Cabinet to workbench to cabinet: that round trip is THE counter idiom, and you will write it from muscle memory by the end of this workbook.
Common stumble: trying to keep the count in $1 alone. Slots are scratch paper for one run; next firing, $1 is blank again. Anything that must survive between firings goes in a note. Second stumble, subtler: a note holding 0 tests as NO in a bare var() check, because the engine treats the text 0 as false. That is exactly why this script compares against the empty value with == '' instead of using a bare test; a counter that has legitimately reached zero is not the same thing as a counter that was never started.
Variations to try. Announce something special every tenth guest by adding a second condition. Make the count survive the mob's death by storing on the room instead: attach the script to the room with mudprog here edit and the note on $i then lives on the room itself.
Exercise 6: The Last Three Bowls
The task. A camp cook has exactly three servings of stew. Each visitor gets one bowl and is told how many remain. When the pot is empty, later visitors are turned away, and the count must never fall below zero.
Hint. Same counter idiom as the turnstile, but counting down, with a branch guarding the empty case. Decide carefully which comparison separates has stew from has none.
A worked solution:
GREET_PROG 100
if var($i servings) == ''
mpsetvar $i servings 3
endif
if var($i servings) > 0
mpargset 2 $%math($<$i servings> - 1)%
mpsetvar $i servings $2
say One bowl of stew for you, $N. After this bowl I have $2 left.
else
say The pot is scraped clean, $N. Come back tomorrow.
endif
mpecho Steam drifts from the cook's iron pot.
~
Walkthrough. Seeding first, as always, but this time to 3, the starting stock. Then the guard: var($i servings) > 0 compares the note as a number, and only when at least one serving remains does the ladle move. Inside the yes branch the counter idiom runs in reverse, minus one instead of plus one, and the customer hears the remainder from the slot: 2, then 1, then 0. The fourth visitor finds the note at 0, fails the guard, and is refused. Because the subtraction only ever happens inside the guard, the count cannot go negative, no matter how many hungry visitors arrive; the shape of the script enforces the rule, so you never need to check for minus one anywhere.
Notice the pot never refills. The note sits at 0 forever, or until the mob dies and a fresh copy spawns with no notes at all, which is a perfectly good model for daily stock in the wild: mobs reset, notes on them reset too. If you want a refill lever while testing, you know the tool from exercise 3: a second block, SPEECH_PROG refill, containing one line that stores 3 over the note.
Common stumble: writing the guard as a bare if var($i servings). That reads as yes while stock remains and no at zero, which happens to work, but it also reads as no if the note was never seeded, silently skipping the seeding branch you forgot. Explicit comparisons make explicit mistakes; bare tests make quiet ones. While you are learning, prefer the explicit form for numbers.
Variations to try. Refill automatically by checking a second note holding the last-refill visitor number from a turnstile-style counter. Sell the stew instead of giving it, borrowing the coin handling from exercise 20 to charge two gold a bowl while stock lasts.
Exercise 7: The Punch Card
The task. A bakery runs a loyalty card: every visit earns the visitor one stamp, and on the third stamp the card is cleared and a free meal lands directly in their hands. Each player has their own card, and the card must survive between sessions.
Hint. Per-player and permanent means the note lives on $n. The reward handout with no chance to fumble it is mploadquestobj, which conjures an item straight into the source's inventory.
A worked solution:
GREET_PROG 100
if var($n bakery_stamps) == ''
mpsetvar $n bakery_stamps 0
endif
mpargset 3 $%math($<$n bakery_stamps> + 1)%
mpsetvar $n bakery_stamps $3
emote stamps a little card with a flour-dusted thumb.
if var($n bakery_stamps) >= 3
mpsetvar $n bakery_stamps 0
mploadquestobj /obj/meal
say Three stamps, $N! A free meal, straight into your hands.
endif
~
Walkthrough. This is the turnstile again with two twists. Twist one: every note here is on $n, the player, so each customer carries their own private count, saved with their character, and the script never keeps a list of anyone. Ten players, ten independent cards, zero bookkeeping: that is the per-player pattern, and it is why seasoned scripters reach for player notes by reflex.
Twist two: the reward-and-reset. After the stamp, a second decision checks whether the card has reached three. On the third visit it has, so the script FIRST clears the count back to zero, then conjures the meal into the customer's hands with mploadquestobj, no give command, no item on the floor, no chance of it being grabbed by someone else. The reset before the reward matters: the card is spent the moment it is redeemed, so the fourth visit starts a fresh card at stamp one.
Note the variable name, bakery_stamps rather than stamps. Every script that touches a player shares one pocket of notes on that player, and a bland name like count or stamps is asking to collide with some other builder's script years from now. Prefix your names with something yours. This habit costs nothing and prevents the least findable bugs in the whole system.
Variations to try. Announce the current stamp count on every visit, reading the note into the emote. Make the reward escalate, a meal at three stamps and a torch at ten, by checking two thresholds. Add a SPEECH_PROG card block that reports the customer's balance when they ask.
Part Three: Loops And Switches
Six exercises on repetition and many-way choices. The tools: for counts a slot through a range and next closes it; while repeats as long as a condition keeps answering yes and endwhile closes it; break leaves the nearest loop early; and switch walks one value down a menu of case answers with an optional default, closed by endswitch. Two safety nets stand behind everything here: a single loop stops itself after 2000 laps, and a whole trigger run is cut off after 4000 script steps, with a line written to /log/script_runaway so you can see it happened. The nets mean an infinite loop embarrasses you rather than harming the mud, but the room still hears a lot of noise before the net catches, so treat the nets as seatbelts, not as a driving style.
Exercise 8: The Drill Sergeant
The task. A drill sergeant counts a recruit through five push-ups, one line per count, with an opening bark before and a grudging verdict after. The counting must be one written line, not five.
Hint. A for loop stores its count in a numbered slot; use the slot in the spoken line.
A worked solution:
GREET_PROG 100
say Recruit on deck! Count off with me.
for $1 = 1 to 5
say Push-up number $1, and make it crisp!
next
say Five of five. Not hopeless after all.
~
Walkthrough. The loop header names slot 1 as the counter and the range 1 to 5. The body, everything between the header and next, runs five times, and on each lap the slot holds the current count, so the one written line speaks five different sentences. When the range is exhausted, execution continues after next with the verdict. The opening and closing lines sit outside the loop and run exactly once each; getting straight which lines are inside a loop and which are outside is most of what this exercise is for, and the indentation shows it at a glance even though the engine ignores the spaces.
Two facts to file away. First, always count into a NUMBERED slot, $0 through $9; the engine accepts a named counter in the header but gives you no way to read a named one back, so it is useless in practice. Second, if the first number is larger than the second, the loop counts DOWN instead, which is exactly what a countdown wants and what the next exercise's variations play with.
Variations to try. Count down from five to one by swapping the range ends. Let the recruit's level set the workload with for $1 = 1 to $%level($n)%, then think about why that is a terrible idea for a level ninety visitor, and cap it with a fixed range instead.
Exercise 9: The Crate Search
The task. A dockhand searches through eight numbered crates for his hammer, narrating each crate as he opens it. The hammer is always in crate five; when he finds it, he stops searching immediately, crates six through eight untouched, and grumbles about the pattern afterward.
Hint. The loop counter is an ordinary value; you can test it in a condition inside the loop body, and break leaves the loop on the spot.
A worked solution:
GREET_PROG 100
say Somewhere in these crates is my good hammer.
for $1 = 1 to 8
emote pries open crate number $1.
if $1 == 5
say There it is! Crate five, as always.
break
endif
next
say Every search ends at crate five. Every single time.
~
Walkthrough. The loop is written for eight laps, but it never takes more than five. Each lap opens a crate, then a decision compares the counter itself against 5. Note the shape of that condition: $1 == 5 is a bare comparison of two values, no function needed, because the slot substitutes to a number and numbers compare as numbers. On the fifth lap the comparison answers yes, the discovery line runs, and break ends the loop instantly; execution lands on the line after next, and the last three crates stay shut.
This search-and-stop shape, loop over candidates, test each, break on the first hit, is one of the most reused patterns in all of scripting. You will see it again whenever something hunts through possibilities: picking the first empty slot, finding the first matching word, walking a patrol until an obstacle.
Common stumble: putting the grumble line INSIDE the loop, before the break. Then it runs on lap five just before leaving, which happens to look right, until someone edits the range and the sentence stops matching reality. Lines about the WHOLE search belong after the loop; lines about ONE crate belong inside it.
Variations to try. Randomize the hammer's crate by parking $%randnum(8)% in slot 2 before the loop and comparing $1 == $2 inside. Count how many crates were opened by pairing the loop with the counter idiom from Part Two.
Exercise 10: The Overfilled Barrel
The task. A water carrier fills a ten gallon barrel using a three gallon pail, announcing the running total after each pour, and stops only when the barrel is full. Since three does not divide ten, he ends up with twelve gallons in a ten gallon barrel, and should say something rueful about it.
Hint. The number of laps is not known in advance, three pours or four, so this is while territory: keep pouring WHILE the total is under ten. The total is a note; the body must change it or the loop never ends.
A worked solution:
GREET_PROG 100
mpsetvar $i gallons 0
while var($i gallons) < 10
mpsetvar $i gallons $%math($<$i gallons> + 3)%
say Another pailful. The barrel holds $<$i gallons> gallons now.
endwhile
say Twelve in a ten gallon barrel. That last pail was a mistake.
~
Walkthrough. The note is seeded to 0 unconditionally this time, not guarded with an emptiness check, because this scene should start from empty on EVERY firing; seeding versus guarding is a choice you make per script, and now you have seen both. Then the while tests: is the note under ten? At 0 it is, so the body pours, three gallons at a time, and announces. The totals go 3, 6, 9, each still under ten, so the test keeps answering yes. After the fourth pour the note reads 12, the test finally answers no, and execution moves past endwhile to the rueful closing line.
Two lessons hide in the numbers. First, the golden rule of while: the body MUST move the condition toward no. Delete the mpsetvar line and nothing changes between laps, the loop spins until the 2000-lap safety net cuts it off, and the room hears several hundred pours first. Second, the overshoot: the test happens BEFORE each lap, not during it, so the pour that crosses the threshold still completes in full. When crossing exactly matters, test the crossing inside the body and break, or pour smaller amounts near the top; when it does not, embrace the overshoot and write a better closing line, as our carrier does.
Variations to try. Make the pail size random with randnum(4) inside the math and watch the pour count vary between firings. Stop at exactly ten by adding an inner if that breaks when the note plus three would pass ten, and feel how much more careful that version has to be.
Exercise 11: Throwing For A Six
The task. A gambler throws a die until it comes up six, counting his throws silently, then announces in one line how many attempts it took. No narration per throw; the room only hears the final count.
Hint. A while loop whose condition tests a SLOT, refilled with a fresh random roll each lap, plus the counter idiom to tally attempts. Seed the slot before the loop so the first test is predictable.
A worked solution:
GREET_PROG 100
mpargset 1 0
mpsetvar $i throws 0
while number($1) != 6
mpargset 1 $%randnum(6)%
mpsetvar $i throws $%math($<$i throws> + 1)%
endwhile
say A six! It only took me $<$i throws> throws this time.
~
Walkthrough. Slot 1 is seeded to 0, so the first test, is the slot not six, answers yes and the loop starts. Each lap rolls a fresh die with $%randnum(6)%, a random number from 1 to 6, drops it into the slot, and ticks the throw counter. Eventually a lap rolls a six, the next test answers no, and the final say reads the tally from the note. Fire it several times and the count wanders, one throw on a lucky day, a dozen on a bad one, which is exactly the charm of loops driven by chance rather than by a fixed range.
The condition is worth a second look: number($1) != 6 uses the number function to treat the slot's text as a number before comparing. In this particular comparison the engine would compare numerically anyway, but saying what you mean costs nothing and reads clearly a year later.
Could this loop run forever? In principle a die could dodge six indefinitely; in practice the odds of even a hundred misses are microscopic, and the 2000-lap net stands behind the odds. A loop that terminates by probability is fine when the probability is overwhelming; a loop that terminates only by the safety net is a bug. Know which one you are writing.
Variations to try. Announce a special line when the count comes out 1, a hole in one, by adding a decision after the loop. Roll two dice per lap and hunt for a double six, and notice how the average count changes.
Exercise 12: The Tea Menu
The task. A tea seller keeps her current order written in a note, and serves it by name: mint tea gets one response, blackroot tea another, and anything unrecognized falls back to water. Multi-word teas must work.
Hint. One value, several exact answers: that is switch shaped, not if shaped. Case answers may be several words long.
A worked solution:
GREET_PROG 100
mpsetvar $i order blackroot tea
switch $<$i order>
case mint tea
say One mint tea, light and sweet.
break
case blackroot tea
say Blackroot tea. A bold choice for this hour.
break
default
say I do not know that brew, so water it is.
endswitch
~
Walkthrough. The script seeds the order note, then the switch line reads it once, and the engine walks down the cases hunting for the first exact match, capitals ignored. The value blackroot tea skips the mint case, matches the blackroot case, runs its lines, and leaves; the default never runs. Change the seeded order to mint tea and refire, and the other branch speaks. Change it to nettle tea and the default catches it, which is what defaults are for: the answer you did not plan.
Three switch facts, worth reciting. Only ONE case ever runs, the first that matches, with no falling through into the next case. Case answers match the WHOLE value exactly, so a case cannot express a range like ten to twenty; ranges belong to if with >= and <=. And the break ending each case is technically optional but should be written anyway, as a full stop for human readers.
Common stumble: switching on a value that could never match any case because it carries invisible extra text. If you build the switch value out of substitutions, keep it simple, one code or one note read; a sentence fragment will match nothing and fall to default every time, mysteriously.
Variations to try. Take the order from the customer instead of the note by switching on $g inside a SPEECH_PROG all block, and see how speech becomes a menu. Add a case for water itself with a different response than the default, and check which of the two answers fires when a customer actually orders water.
Exercise 13: Three Sayings And A Stool
The task. An old harbor watcher offers one of three sayings, chosen at random on every firing, then settles back onto his stool, the same closing motion no matter which saying came up.
Hint. Switch does not have to examine a stored value; it can examine the answer of a function, freshly computed. And a line placed after endswitch belongs to no case at all.
A worked solution:
GREET_PROG 100
switch $%randnum(3)%
case 1
say Some days the road is kind. Today feels like one of them.
break
case 2
say Rain before nightfall, or my knee is a liar.
break
case 3
say Hush a moment. The gulls are arguing again.
break
endswitch
emote settles back onto his stool by the door.
~
Walkthrough. The switch value is $%randnum(3)%, the percent form asking for a random number from 1 to 3, computed fresh at the moment the switch line runs. Whichever number comes up selects its case, and the closing emote, sitting after endswitch, runs every time regardless. Fire it repeatedly and the sayings shuffle while the stool line never misses: variety inside the structure, reliability outside it.
This random-switch is the standard recipe for variety, and it beats the alternative of three separate RAND_PROG blocks in one important way: exactly one saying fires per event, never zero and never two, because a switch always picks exactly one path. When you want a mob to feel alive without flooding the room, reach for this shape.
No default case is written here, and none is needed: randnum(3) can only answer 1, 2, or 3, all covered. Add a fourth case and it will simply never fire, which is a quiet way to disable a line without deleting it, a trick worth remembering while drafting.
Variations to try. Weight the choices by switching on randnum(6) and giving the kind-road saying cases 1 through 3, rain 4 and 5, gulls 6. Move the whole thing into a RAND_PROG 10 block for a mob that mutters spontaneously, and keep the chance low; ambience whispers, it does not shout.
Part Four: Asking The World Questions
Conditions get their power from functions, the named questions the game already knows how to answer: who is this, how strong, how hurt, what class, what do they carry, what do they hold in coin. Four exercises here practice the everyday ones inside if lines, and two idioms carry through all four: combining questions with and, or, and not, and putting a reliable always-runs line outside the branches so the scene ends the same way no matter which answer came back. The full catalogue of functions lives in the functions chapter; nothing here uses one that chapter does not explain.
Exercise 14: The Velvet Rope
The task. A club doorman sorts arrivals by level: level ten and up are directed to the grand hall, everyone else to the lesser hall. Then, provided the visitor is a real player and is not in the middle of a fight, he unhooks the rope and waves them through, whichever hall they were assigned.
Hint. Two separate decisions, not one: the sorting is if-else, the waving-through is its own if with two conditions joined by and, one of them flipped with an exclamation mark.
A worked solution:
GREET_PROG 100
if level($n) >= 10
say The grand hall stands open to you, $y.
else
say The lesser hall for now, $y. Come back stronger.
endif
if ispc($n) and !isfight($n)
emote unhooks the velvet rope and waves you through.
endif
~
Walkthrough. The first decision asks level($n) >= 10, the at-least comparison, and speaks one of two assignments; the $y code addresses the visitor as sir or madam by their gender, a free touch of manners. The second decision is independent, and its condition is a chain: ispc($n), true for a real player, joined by and to !isfight($n), where the exclamation mark flips the answer, so the pair reads as a player, AND not fighting. Both must be yes for the rope to move. A brawling visitor gets their hall assignment and no admission; so does a wandering mob, though the mob at least is spared the small talk.
Why two decisions instead of nesting the rope inside each hall branch? Because the rope rule is the same for both halls. Writing it twice, once per branch, is the classic duplication trap: the two copies drift apart the first time someone edits one and forgets the other. When a rule applies regardless of an earlier choice, hoist it out to its own decision after the endif.
Boundary check, worth a habit: >= 10 admits the level ten visitor to the grand hall. If the design brief had said above level ten, the operator would be >, and level ten would wait outside. Say the rule aloud, then pick the operator that matches the words.
Variations to try. Add a third tier at level thirty with a nested if inside the first branch. Refuse mobs entirely with an early return guard at the top, the shape exercise 21 uses. Swap the level test for a class test with .in., which is the whole of exercise 17.
Exercise 15: The Field Medic
The task. A medic looks each arrival over and reports their exact health, current of maximum. The unhurt get a compliment; anyone carrying damage gets a small patch-up on the spot, ten points of healing. Either way she closes her satchel with a snap.
Hint. Exact numbers come from the hp and maxhp functions inside the percent form; the hurt-or-not decision is cleaner on hitprcnt, the rounded percentage. Healing is mpheal <who> <amount>.
A worked solution:
GREET_PROG 100
mpechoat $n The medic counts your hurts, $%hp($n)% health of a possible $%maxhp($n)%.
if hitprcnt($n) >= 100
say Not a scratch on you. My favorite kind of patient.
else
mpheal $n 10
say Hold still. There, a little better already.
endif
mpecho The medic snaps her satchel shut.
~
Walkthrough. The opening line is private, sent only to the visitor with mpechoat, and it splices two live readings into the sentence: $%hp($n)% and $%maxhp($n)%, the visitor's actual current and maximum health at this instant. The decision then uses a different instrument for a different job: hitprcnt($n) answers a percentage from 0 to 100, and >= 100 cleanly separates untouched from hurt without caring whether the visitor's maximum is fifty or five thousand. In the hurt branch, mpheal restores ten points, a real heal through the real systems, and the patter covers it. The satchel line runs on both branches, the reliable closer again.
Why not compare hp($n) == maxhp($n) instead? You could, and it would work. The percentage form is preferred here because it states the INTENT, is anyone hurt at all, rather than the mechanism, and because percent thresholds generalize: change 100 to 50 and the medic triages only the badly wounded, no other edits needed. Choose the function whose answer is shaped like your question.
Variations to try. Scale the heal to the damage by computing $%math($%maxhp($n)% - $%hp($n)%)% into a slot, then capping it with a comparison. Report spell points too with sp and maxsp. Refuse to treat anyone currently fighting, borrowing the !isfight guard from the doorman.
Exercise 16: The Picnic Audit
The task. A cook packs her own bag for a picnic: conjure two meals into her pack, confirm she is holding at least two with an inventory check, then tidy up by destroying both, and confirm the pack is empty again. The whole exercise is the mob interrogating her own belongings.
Hint. The carrying questions are has, does an object carry an item by this name, and hasnum, does it carry at least so many. mpoload loads into the host's own inventory; mpjunk destroys one item by name.
A worked solution:
GREET_PROG 100
mpoload /obj/meal
mpoload /obj/meal
if hasnum($i meal 2)
say Two meals packed. A picnic is possible after all.
else
say My pack came up short. No picnic today.
endif
mpjunk meal
mpjunk meal
if has($i meal)
say Strange. One meal still left after my tidy-up.
else
say Pack empty again. Tidy cook, tidy kitchen.
endif
~
Walkthrough. Two mpoload lines put two meals straight into the host's pack, not the room. Then hasnum($i meal 2) asks: does the host carry at least two things answering to the name meal? Note the argument shape, three values separated by spaces, no commas; a comma here quietly breaks the question. With both meals aboard, the answer is yes. Then two mpjunk meal lines destroy one meal each, and the final has($i meal) confirms nothing remains.
One honest subtlety about mpjunk: when you name an item by word rather than by a code like $b, the engine looks around the ROOM before looking in the host's own pack. In a clean practice room that changes nothing; in a room where someone dropped a meal on the floor, the floor meal dies first and one pack meal survives, and the script's second check will notice and say so. That is not a bug in your script; it is a reason to test in tidy rooms and to prefer $b when junking the exact thing you just loaded.
The larger lesson: hasnum answers AT LEAST, not exactly. Carrying three meals still passes hasnum($i meal 2). When exactly-two matters, test at-least-two and then not-at-least-three, two questions joined with and not.
Variations to try. Point the same questions at the visitor, has($n meal), and have the cook comment on their packing instead; remember you cannot conjure into their pack with mpoload, that is mploadquestobj's job. Audit the ROOM instead with numitemsroom and watch the count move as you drop and fetch things.
Exercise 17: Robes Or Steel
The task. A barkeep sizes up each arrival's profession at a glance. The robed classes, mage, necromancer, healer, cleric, get a warning about the candles in the back room; everyone else gets a warning about the low beam over the bar. Both kinds of customer are told to wipe their boots.
Hint. Is-it-one-of-these is the .in. operator: the small value on the LEFT, the list it might appear in on the RIGHT.
A worked solution:
GREET_PROG 100
if class($n) .in. mage necromancer healer cleric
say A robe wearer. Mind the candles in the back room.
else
say Steel and leather. Mind the low beam over the bar.
endif
say Either way, $N, wipe your boots.
~
Walkthrough. class($n) answers the visitor's class as a word, and .in. asks whether that word appears anywhere in the list written on the right, capitals ignored. A healer matches, a warrior does not, and the else branch catches every class not on the list, including classes added to the game years after you wrote the script, which is exactly the graceful aging you want. The boots line closes both branches from outside the decision.
Mind the direction, because it is the classic .in. mistake: the small thing goes on the left, the big thing it might be inside goes on the right. Written backward, the condition asks whether your entire word list fits inside one class name, which is almost never yes, and the branch simply never fires, with no error to point at the cause.
One more honest note: .in. is a substring test, so a list has to be chosen with a little care. The list above is safe, but a hypothetical class named rog would match inside rogue. Multi-word lists with short words deserve a second look; when in doubt, chain == comparisons with or instead.
Variations to try. Sort by race instead of class. Use a switch on $%class($n)% when each class deserves its OWN line rather than a shared one, and feel where the boundary lies: .in. groups, switch distinguishes.
Part Five: Items And Money Changing Hands
Four exercises on property. Handing things out: mploadquestobj conjures an item directly into the source's inventory, the fumble-proof quest handout. Receiving things: GIVE_PROG fires when a player hands the scripted mob an item, with the item riding as $o. Coin: mpmoney gives with a positive amount and takes with a negative one, and the goldamt function reads what someone carries so you never take coin that is not there. The rule of the whole part: check before you take, and clean up what you conjure.
Exercise 18: The Quartermaster
The task. A tunnel quartermaster issues every arrival a torch, whether they want one or not: the item appears directly in their hands, they get a private line about it, everyone else sees the handover from the outside, and the quartermaster explains himself aloud.
Hint. After the load, $B names the new item, so your messages need not hard-code what it is called. Three audiences means three message commands: mpechoat, mpechoaround, and plain say.
A worked solution:
GREET_PROG 100
mploadquestobj /obj/torch
mpechoat $n The quartermaster presses $B into your hands before you can object.
mpechoaround $n The quartermaster hands $B to the newest arrival.
say Regulations. Nobody walks my tunnels dark.
~
Walkthrough. mploadquestobj creates the torch inside the source's inventory, no give command, no floor, no interception. Then the scene is told from three angles: the recipient reads a private second-person line, the rest of the room reads a third-person version of the same instant, and the closing say is public speech from the quartermaster himself. This three-voice pattern, at, around, aloud, is how handovers, secrets, and pickpocketing are staged everywhere in the game, and it is worth practicing until the three commands come without thought.
Notice $B in both echo lines: the display name of the last-loaded object. If you later swap the torch for a lantern by editing one path, every message updates itself. Hard-coded item names in messages are a maintenance debt; $B pays it off.
Variations to try. Issue the torch only to arrivals who do not already carry one, guarding with !has($n thing), and note the item's name words matter for has. Issue armor to warriors and a meal to everyone else by combining this with the class sorting of exercise 17.
Exercise 19: The Meal Collector
The task. A notice outside a collector's stall offers twenty five gold for a meal, delivered by hand. When a player gives him a meal, he destroys it and pays out. When a player gives him anything else, he declines it. This is your first GIVE_PROG.
Hint. GIVE_PROG fires on the scripted mob when a player hands it an item; the item is $o. The isname function checks what an object answers to. Payment is mpmoney with a positive amount; disposal is mpjunk pointed at $o.
A worked solution:
GIVE_PROG 100
if isname($o meal)
mpjunk $o
mpmoney $n 25
say A proper meal at last! Twenty five gold, $N, as the notice promised.
else
say That is not the meal my notice asked for, $N. Back it goes.
endif
~
Walkthrough. When a player gives the mob anything, the trigger fires with the giver as $n and the item as $o. The condition isname($o meal) asks whether the item answers to the name meal, which a real meal does. On a match, the collector consumes the goods, mpjunk $o destroys exactly the item that was handed over, and pays with mpmoney $n 25, twenty five gold appearing in the giver's purse through the real currency system. On anything else, the refusal line runs. One honesty note about the refusal: the item has still physically arrived in the mob's pack, because the give itself already happened before the trigger fired; a fussier collector would follow the refusal with a plain give line to hand it back, or quietly drop it. Keeping the mob's pockets honest is part of the craft.
Now the test-bench lesson promised in the introduction. Fire this with mudprog <mob> test GIVE_PROG and you will ALWAYS see the refusal, never the payout, because a rehearsed trigger carries no real item, so $o is empty and isname finds nothing. That is correct behavior, not a bug. To see the yes branch, do it for real: conjure a meal for yourself, hand it over with the ordinary give command, and watch the gold arrive. Rehearsal for the shape, reality for the branches: test both ways, always.
Variations to try. Pay only once per player by combining this with the fortune teller's player-note receipt. Accept two different items at two different prices with a second isname branch. Reward with an item instead of coin, mploadquestobj in place of mpmoney.
Exercise 20: Old Wenn's Cup
The task. A beggar asks each passerby for two gold. If they can afford it, the coins move, out of their purse and INTO HIS, and he blesses them. If they cannot, he is gracious about it. Either way he rattles his cup at the next prospect.
Hint. Read before you take: goldamt tells you what the visitor carries. Taking is a negative mpmoney on them; receiving is a positive mpmoney on the host.
A worked solution:
GREET_PROG 100
if goldamt($n) >= 2
mpmoney $n -2
mpmoney $i 2
say Two gold for old Wenn! May your purse never lighten but for kindness.
else
say Not a coin to spare? Then a kind word costs nothing, friend.
endif
emote rattles a dented tin cup hopefully.
~
Walkthrough. goldamt($n) answers the gold the visitor is carrying, and the at-least-two guard means the taking line can never drive a purse negative; the shape of the script enforces the rule, the same lesson the stew pot taught about stock. Inside the yes branch, two mpmoney lines make the coins genuinely move: minus two on the visitor, plus two on the host, so a curious player who kills poor Wenn later will find his takings on the corpse, which is the kind of consistency that makes a world feel solid. The no branch costs nothing and chides no one, and the cup rattles for everyone.
Two facts about money worth filing. First, goldamt counts CARRIED gold, not bank balances, so a wealthy visitor with an empty purse takes the else branch, which is exactly what a beggar would experience. Second, mpmoney's type defaults to gold; other coinages are named explicitly, as in mpmoney $n silver 200, when you want them.
Variations to try. Escalate: ask for two gold from arrivals under level twenty and five gold from the rest, combining the level test of exercise 14. Remember donors with a player note and thank repeat benefactors by name. Have Wenn refuse charity past ten total gold by tracking his takings with a counter.
Exercise 21: The Wishing Well
The task. A wishing well takes a five gold stake, double or nothing: half the time it returns ten gold, half the time nothing. A visitor who cannot cover the stake is told so and nothing is taken. This solution is written for the ROOM as host, so it also practices the early-return guard.
Hint. Guard first: check goldamt and return out before any coin moves. Chance is the rand function, a percent likelihood of yes. A room cannot say, so every message is an echo.
A worked solution:
GREET_PROG 100
mpecho Weathered letters on the rim promise double or nothing for five gold.
if goldamt($n) < 5
mpechoat $n You cannot cover the stake, and somehow the water knows it.
return
endif
mpmoney $n -5
mpecho A five gold piece spins down into the dark, and the water goes still.
if rand(50)
mpmoney $n 10
mpechoat $n Two coins bob up gleaming. Your stake, doubled.
else
mpechoat $n A single bubble rises to the surface, and that is all.
endif
~
Walkthrough. Attach this to the room itself with mudprog here edit if you want the full effect; on a mob it works identically. The opening echo sets the scene for everyone. Then the guard: if the visitor carries less than five gold, they get one private line and return stops the whole script dead, so the taking line below can never touch them. This guard-then-continue shape keeps the main flow flat and readable, and it is the polite way to write any script with an entry price: check, refuse, leave, and only then let the paying customers through.
Past the guard, the stake is taken unconditionally, minus five, and then rand(50) flips the coin: a fifty percent chance of yes, rolled fresh every firing. The yes branch pays TEN, not five, because the well already holds the stake; returning the stake plus winnings is one payment of double the stake. Off-by-one thinking applies to money as much as to levels. The no branch pays a single bubble.
While testing, remember the two dials from the flow chapter: rand(100) is always yes and rand(0) is always no, so you can temporarily force either branch to check its lines, then set the odds back.
Variations to try. Track the well's net profit in a room note, adding five on a loss and subtracting five on a win, and let a caretaker mob report it. Limit each player to one wish a day the honest way this engine allows: a player note set on wishing, cleared by a SPEECH_PROG block only staff can reach.
Part Six: Real Conditions
Scripts can apply the same status conditions that skills and spells apply, through two commands. mpaffect <who> <id> [seconds] is the quick form: sixty seconds unless you say otherwise, typed as a debuff, removable early with mpunaffect. mpcondition <who> <id> <type> <seconds> [magnitude] [percent] is the full-control form, where the type is most usefully buff or debuff. The affected function reads whether a condition is currently on someone. Manners matter here: ids like rooted and stunned are REAL and mechanically bind players, so while practicing, invent harmless ids of your own, keep durations short, and remove what you apply where the fiction allows. The display name a player sees is made from the id, so choose ids that read decently.
Exercise 22: Stage Fright
The task. A theater's house lights pin each newcomer where they stand. The script applies a brief, harmless condition called stage_fright to the visitor, proves it is really there by testing for it, lets the host comment, then removes it and releases the moment.
Hint. Apply with mpaffect and a short duration, verify with the affected function in an if, remove with mpunaffect. Stage the private and public views with the three-voice pattern from exercise 18.
A worked solution:
GREET_PROG 100
mpaffect $n stage_fright 20
mpechoat $n Every eye in the room turns to you, and your mind empties.
mpecho The house lights swing around to pin the newest arrival.
if affected($n stage_fright)
say There it is. That famous deer in the lantern light look.
endif
mpunaffect $n stage_fright
mpechoat $n The lights move on, and your thoughts creep back to you.
~
Walkthrough. The first line applies a real condition: id stage_fright, twenty seconds, on the visitor. It appears in their status displays like any spell effect for as long as it lasts, which is why the duration here is short and the id invented; nothing in the game engine gives stage_fright mechanical teeth, so it is pure story, exactly right for practice. The two echo lines stage the moment inside and outside the visitor's head. Then the proof: affected($n stage_fright) answers yes while the condition sits on them, so the host's comment fires, and you have seen a script READ a condition as well as write one, which is how traps, wards, and healers coordinate. Finally mpunaffect lifts it early, twenty seconds notwithstanding, and the closing line releases the scene.
The reading half is the part to remember. A guard who checks affected($n marked_by_watch), a healer who only treats affected($n poisoned), a door that refuses affected($n cursed): one script applies the mark, another reacts to it, possibly days later, possibly written by another builder. Conditions are notes the whole combat system can read.
Variations to try. Skip performers: guard the whole scene with a condition of your own, applying stage_veteran to anyone who has been pinned once, and testing not affected before pinning, so the lights only ever catch each visitor's first entrance. Watch what happens if you never remove a short condition: it simply expires on its own, which is why durations exist.
Exercise 23: The Road Blessing
The task. A roadside priest blesses each traveler: a proper buff, politely typed as such, thirty seconds long, with a modest magnitude, and a warm line to carry it.
Hint. This is what mpcondition is for: you choose the type. The argument order is who, id, type, seconds, then the optional magnitude.
A worked solution:
GREET_PROG 100
say Kneel a moment, $N. The road is long, and you will want this.
mpcondition $n road_blessing buff 30 5
mpecho A brief warmth settles over the traveler like a dry cloak.
~
Walkthrough. Read the mpcondition line left to right: on the visitor, a condition with the id road_blessing, typed as a buff, lasting thirty seconds, with a magnitude of five. The type matters more than it looks: it decides how the effect is presented and grouped in status displays, a kindness on their screen rather than an affliction. The magnitude and the optional percent after it are numbers carried BY the condition for systems and scripts that scale from them; a plain story blessing like this one carries them without spending them, and that is fine, they are simply part of the note.
Compare the two commands now that you have used both. mpaffect is shorthand: id and seconds, debuff assumed, perfect for a trap's sting or a moment's daze. mpcondition is the deliberate form: when the effect is a gift, or needs its type, strength, or duration chosen precisely, spell it all out. Under the hood they write to the same condition system, so mpunaffect and the affected function work identically on both.
Variations to try. Bless only the unblessed by guarding with not affected($n road_blessing), the polite way to prevent stacking spam on someone walking in and out. Scale the magnitude with the traveler's level by parking $%level($n)% in a slot first. Give the priest a SPEECH_PROG bless block so the blessing is asked for rather than automatic.
Part Seven: Scenes That Take Their Time
A script normally runs its whole block in one instant. Real moments have pacing, and two commands provide it. mpsleep <seconds> pauses the script itself mid-block: everything above has happened, everything below waits. mpalarm <seconds> <command> schedules ONE command for later and lets the script continue immediately. The rules that matter: a sleep of less than one second becomes one; only the script sleeps, the mob keeps living and its other triggers keep answering, each firing being its own separate run; and mpsleep must never sit inside a for or while body, because sleeping abandons the remaining laps by design. Three exercises: a paced monologue, the two timing tools side by side, and the busy-flag pattern that keeps overlapping scenes from tangling.
Exercise 24: The Shortest Story Ever Told
The task. A storyteller tells a three-beat story, with a genuine two-second pause between beats, so the room reads it as telling, not as a wall of text.
Hint. Say, sleep, say, sleep, say. That is the whole shape.
A worked solution:
GREET_PROG 100
say Sit, sit. This one is short, I promise.
mpsleep 2
say A fisherman once netted a boot, and in the boot he found a key.
mpsleep 2
say He never found the lock. He wears the boot on Sundays.
~
Walkthrough. The first line lands the moment the trigger fires. Then mpsleep 2 suspends the script, and for two real seconds nothing more comes; the room breathes. The second beat lands, another pause, the third beat closes. Total elapsed, about four seconds, and the difference in feel between this and the same three lines fired at once is the entire reason the command exists. Fire it and watch your own eyes: the pause is where the listener imagines the boot.
What the pause does NOT do is freeze the world. The storyteller keeps breathing; if attacked mid-story he fights; if a second visitor walks in during a pause, GREET_PROG fires AGAIN and a second, independent telling starts from the top, interleaving with the first. For a storyteller that is charmingly chaotic. For scenes where overlap would be wrong, exercise 26 has the cure, so finish this part before shipping anything long.
Pacing craft, briefly: two seconds is a stage beat, three is dramatic, five feels broken unless something visibly ongoing justifies it. And put your strongest line immediately AFTER the longest pause; the wait is an ear-opener.
Variations to try. Re-pace the same story with one-second and then three-second gaps and feel the difference. Add a fourth beat that only plays for an audience, guarding it with numpcsroom() > 1. Let the final beat name a random onlooker with $r for a storyteller who recruits his listeners.
Exercise 25: The Kettle Keeps Its Own Time
The task. A tea vendor puts the kettle on and keeps chatting. The kettle whistles two seconds later, by itself, AFTER the vendor has already moved on to other remarks. The point: see mpalarm and straight-line speech run side by side, and understand why the output arrives in a different order than the script is written.
Hint. mpalarm takes a delay and one command, schedules it, and does not wait.
A worked solution:
GREET_PROG 100
say Kettle is on. It whistles when it decides to, not when I do.
mpalarm 2 mpecho The kettle erupts in a long, piercing whistle.
say See? I keep talking, and the kettle keeps its own time.
~
Walkthrough. Line one speaks at once. Line two SCHEDULES the echo for two seconds from now and immediately moves on, so line three speaks in the very same instant as line one, and only afterward, mid-silence, does the whistle arrive. Written order: say, whistle, say. Heard order: say, say, whistle. That reordering is not a bug; it is the definition of scheduling, and reading an mpalarm line correctly means reading it as a promise, not an action.
When to use which tool, once and clearly: mpsleep paces a SEQUENCE, holding the script's remaining lines back, right for monologues and rituals where later lines depend on earlier ones having landed. mpalarm schedules one EVENT and lets everything else proceed, right for afterthoughts, delayed consequences, and background noises that should not hold up the conversation. The kettle is an event; the story of exercise 24 is a sequence. Mixing them up produces scenes that either stall for no reason or blurt their ending early.
One technical note: the command an mpalarm carries has its dollar codes filled in at SCHEDULING time, not at firing time. An alarm written with $N bakes in the name of whoever fired the trigger, even if they have long left the room when the alarm goes off, which is usually what the fiction wants.
Variations to try. Stack three alarms at two, four, and six seconds for a kettle that builds from hiss to rattle to whistle, all scheduled in one instant. Then rebuild the same scene with mpsleep and notice the vendor now cannot talk over it: same output, entirely different feel.
Exercise 26: The Lamplighter's Ritual
The task. A lamplighter performs a slow four-second lighting ritual. If someone else arrives WHILE the ritual is running, the script must not start a second overlapping copy; the newcomer just gets a quiet private line, and the ritual in progress continues undisturbed.
Hint. Each firing of a trigger is an independent run, so the two runs can only coordinate through something shared: a note on the host. Set a busy flag before the scene, clear it after, and bounce any firing that finds it set. That is the busy-flag pattern, and it guards every long scene you will ever ship.
A worked solution:
GREET_PROG 100
if var($i lamp_busy) == 1
mpechoat $n The lamplighter is mid-ritual. You keep respectfully quiet.
return
endif
mpsetvar $i lamp_busy 1
say Watch now, this is the delicate part.
mpsleep 2
mpecho The wick catches, and a warm glow pushes the shadows back.
mpsleep 2
say There. One more night held off at arm's length.
mpsetvar $i lamp_busy 0
~
Walkthrough. The first thing every firing does is check the flag. A run that finds lamp_busy at 1 knows another run is mid-scene, sends its visitor one private line, and returns immediately; total cost, nothing. A run that finds the flag clear claims the stage, setting the flag to 1 BEFORE the first slow line, then performs the ritual with its two sleeps, and its very last act is clearing the flag back to 0. Between the set and the clear, about four seconds, every other firing bounces. After the clear, the next arrival starts a fresh ritual. Walk two characters in a few seconds apart and watch the second one get the quiet line while the first ritual plays out untangled.
The discipline the pattern demands: EVERY path out of the scene must clear the flag. This script has one path and clears it at the end; a scene with an early return in the middle must clear the flag before that return, or the mob is stuck busy forever, bouncing everyone until the mob dies or you store a 0 over the note by hand. When a long scene of yours mysteriously refuses to start, check its flag first; a stuck busy flag is the most common late-stage scripting bug there is, and now you know both the disease and the cure.
Variations to try. Report how long the ritual has left by storing a beat number the sleeping run updates between sleeps and the bounced run reads. Guard a two-mob scene with a flag on the ROOM instead, so either mob's scene blocks both, a small taste of Part Eight.
Part Eight: Blocks That Work Together
Everything so far lived in one block. But a script may hold many blocks, for many triggers, and they run against the same host with the same pocket of notes, which means they can COOPERATE: one block writes, another reads, and the mob starts to feel like one mind with several senses instead of a stack of reflexes. The three shapes practiced here: a writer block and a reader block sharing a note; a background block that drifts state while a foreground block presents it; and the FUNCTION_PROG subroutine, a block that never fires on its own but is called by name from other blocks so shared lines live in exactly one place. The finale stitches all of it together with money, conditions, and timing from the earlier parts.
Exercise 27: The Front Desk Ledger
The task. A desk clerk notes the name of everyone who passes, and will report the most recent entry when someone asks about the ledger. Two triggers, one memory: the greeting writes, the speech reads.
Hint. The note lives on the host, where both blocks can see it. The reader must handle the empty case, before anyone has been logged.
A worked solution:
GREET_PROG 100
mpsetvar $i desk_lastname $N
say Noted, $N. I keep track of everyone who passes this desk.
~
SPEECH_PROG ledger
if var($i desk_lastname) == ''
say The ledger is empty. You are ahead of the news today.
else
say The last name entered here is $<$i desk_lastname>, since you ask.
endif
~
Walkthrough. Two complete blocks, each with its own header and its own closing tilde, living in one script. The first fires on arrivals and does one thing: it writes the visitor's name into the note desk_lastname on the host. The second fires when someone speaks a line containing the word ledger, and reads the same note back, guarding the never-written case exactly as exercise 2 did. Walk in, then say ledger, and the clerk quotes you back to yourself; have a friend walk in after you and ask again, and the answer has moved on. Neither block knows the other exists; they simply agree on where the note lives and what it is called. That agreement IS the cooperation.
This writer-reader split is the seed of every larger design: a DEATH_PROG that writes and a room that reads, a lever room that writes and a door room that reads, a boss whose FIGHT_PROG writes a phase and whose HITPRCNT_PROG reads it. Whenever a design brief says remembers X and later Y happens, hear it as one block writes a note, another reads it.
The name desk_lastname carries the prefix habit from exercise 7 into shared territory, where it matters even more: with several blocks reading the same pocket of notes, a bland name is a collision waiting across your OWN script, not just other people's.
Variations to try. Count entries as well as remembering the last, combining the turnstile. Let the clerk refuse to gossip to the very person whose name is in the ledger, comparing the note against $N in the reader block.
Exercise 28: The Weathervane Innkeeper
The task. An innkeeper's mood drifts on its own over time, swinging between sunny and gloomy, and each arrival is greeted according to whatever the mood happens to be at that moment. Two triggers: a background block that flips the mood now and then, and a greeting block that presents it. Arrivals before the first flip get a neutral introduction.
Hint. The background pulse is RAND_PROG, rolling its chance on each of the mob's heartbeats. The presenter is a switch on the stored mood, with default catching the not-yet-set case.
A worked solution:
GREET_PROG 100
switch $<$i keeper_mood>
case sunny
say Beautiful day, $N! First drink is nearly free.
break
case gloomy
say What do you want, $N. Make it quick.
break
default
say Afternoon, $N. My moods turn with the harbor bells.
endswitch
emote taps the little brass barometer beside the till.
~
RAND_PROG 20
if var($i keeper_mood) == sunny
mpsetvar $i keeper_mood gloomy
emote scowls at nothing in particular as the weather in him turns.
else
mpsetvar $i keeper_mood sunny
emote brightens for no reason anyone can see.
endif
~
Walkthrough. Start with the second block, the engine room. RAND_PROG 20 rolls a twenty percent chance on each of the mob's heartbeats, and when it fires, it flips the note: sunny becomes gloomy, anything else, including the never-set emptiness of a fresh mob, becomes sunny, with a small emote selling the turn. Left alone in a visited room, the innkeeper's weather wanders by itself.
The first block never changes anything; it PRESENTS. The switch reads whatever the mood note holds at that instant: sunny and gloomy each get their greeting, and default catches the fresh-mob case before the first flip has ever happened, introducing the premise instead of glitching on emptiness. The barometer tap after endswitch runs whatever the weather, the reliable closer one last time.
The design idea here is separation of duties: one block owns CHANGING the state, another owns SHOWING it, and they meet only at the note. You can now tune the innkeeper's temperament by touching only the RAND block, or reword every greeting by touching only the GREET block, and neither edit can break the other. That separation is what lets scripts grow past a screenful without collapsing.
Variations to try. Add a third mood, wistful, to both blocks, and notice each block changes in exactly one obvious place. Let paying customers cheer him up: a GIVE_PROG that sets the mood sunny on any gift, a third writer joining the same agreement. Slow his swings by dropping the RAND chance to 5.
Exercise 29: The Herald And The Trumpet
The task. A herald sounds a trumpet fanfare for each arrival, and also on request, when someone speaks the word herald. The fanfare must be written once, in one place, and used from both triggers.
Hint. A FUNCTION_PROG block never fires on its own; it runs only when another block calls it by name with mpcallfunc. Text passed after the name arrives inside the function as $G.
A worked solution:
GREET_PROG 100
mpcallfunc fanfare $N
say The court will receive you now.
~
SPEECH_PROG herald
mpcallfunc fanfare $N
say Yes, yes. Anything for an encore.
~
FUNCTION_PROG fanfare
mpecho A dented trumpet sounds a brave, wobbly note for $G.
~
Walkthrough. Three blocks. The third is the subroutine: FUNCTION_PROG fanfare sits inert until called, then runs its lines and returns to the caller. The first two blocks each call it with mpcallfunc fanfare $N, passing the visitor's name along; inside the function that passed text is readable as $G, so the trumpet line names whoever the CALLER was announcing. Then each caller continues with its own follow-up line. Walk in: fanfare, then the receiving line. Say herald: fanfare, then the encore line. One trumpet, two doors into it.
Why bother, for one line of fanfare? Because the alternative, pasting the mpecho into both blocks, starts a debt that compounds. The day the fanfare grows to three lines with a pause in it, you edit one copy and forget the other, and the herald develops two subtly different trumpets that no one can explain. The working rule: the moment the same lines appear in two blocks, move them into a FUNCTION_PROG and call it from both. Functions can also hand an answer BACK, a return line in the function becomes the result of the callfunc() form, but the pure shared-scene use you have just built is the everyday one.
Variations to try. Grow the fanfare into a three-beat scene with an mpsleep, and enjoy editing it in exactly one place. Add a third caller, a GIVE_PROG that sounds the fanfare for generous gifts. Pass richer text, such as $N of the $%race($n)% delegation, and let the function stay ignorant of where its words come from.
Exercise 30: The Gate Warden
The task. The finale, combining the whole workbook. A gate warden administers a first-crossing oath: a paced two-beat ceremony that must not overlap itself, sworn at most once per player forever, with a small blessing bestowed, a running tally of oaths chalked by the door, and a spoken release that lets a sworn player take the oath anew. Memory, counters, conditions, timing, and cooperating blocks, all in one mob.
Hint. Read your own toolbox before peeking: the busy flag is exercise 26, the once-ever player note is exercise 3, the counter is exercise 5, the blessing is exercise 23, and the release block is the eraser from exercise 3 again.
A worked solution:
GREET_PROG 100
mpecho The gate warden looks up from a ledger thick with chalk marks.
if var($i gate_busy) == 1
mpechoat $n The warden is mid-oath with someone else. You wait your turn.
return
endif
mpsetvar $i gate_busy 1
if var($n gate_oath)
say Sworn already, $N. The small door is yours at any hour.
mpsetvar $i gate_busy 0
return
endif
say Approach, $N. First crossings are paid in oaths here, not coin.
mpsleep 2
mpecho The warden raises a battered lantern between the two of you.
mpsleep 2
mpsetvar $n gate_oath yes
mpcondition $n warden_favor buff 30 5
say Sworn and sealed. Pass, $N, and mind the third step down.
if var($i oath_count) == ''
mpsetvar $i oath_count 0
endif
mpargset 5 $%math($<$i oath_count> + 1)%
mpsetvar $i oath_count $5
mpecho The warden chalks one more mark, the tally now $5 strong.
mpsetvar $i gate_busy 0
~
SPEECH_PROG release
mpsetvar $n gate_oath
say Released, $N. Your next crossing begins the oath anew.
~
Walkthrough, in layers, because that is how it was written. The outermost layer is the busy flag: checked first thing, set before anything slow, cleared on EVERY exit, and count the exits carefully, there are three, the busy bounce which returns before ever setting the flag, the sworn-already path which clears it before its return, and the main path which clears it at the very end. The sworn-already path is exactly the place the lamplighter walkthrough warned about: an early return inside a claimed scene, and it clears the flag before leaving. Miss that line and the second sworn visitor would jam the gate forever.
The next layer is the once-ever check, straight from the fortune teller: a bare var test on the player note gate_oath, refusing the ceremony to the sworn. Then the ceremony itself, the storyteller's pacing: a beat, two seconds, a beat, two seconds, and the payoff. The oath is recorded on the PLAYER, permanent, and the blessing goes on with mpcondition, typed buff, thirty seconds, magnitude five, the priest's touch from exercise 23. Then the turnstile plays out on the host, seed, add one through math, store back, announce from the slot, so the warden's chalk tally climbs across visitors and firings. And the second block is the eraser: say release, and the oath note is stored empty, opening the ceremony again, which you will use constantly while testing this very script.
One design note worth carrying out of the workbook: the note the oath writes and the note the release erases have to agree on OWNER and NAME, gate_oath on $n, or the two blocks silently talk past each other. When a writer-reader pair misbehaves, check the agreement first: same owner, same spelling. It is the multi-block equivalent of counting your endifs.
Variations to try. Charge a two gold oath fee with the beggar's guarded taking. Refuse the ceremony to anyone currently fighting. Let the tally unlock ceremony variations, a wearier warden past twenty oaths, by switching on ranges with if. Move the busy flag to the room and add a second warden who shares it. Each of these is one exercise from this workbook, grafted on; that is the whole method.
Habits To Take With You
Thirty exercises leave fingerprints. These are the ones worth keeping deliberately. Seed before you count, and remember that arithmetic on a never-written note gives nonsense. Prefer explicit comparisons for numbers, and remember a stored 0 tests as no in a bare var check. Put a reliable always-runs line outside your branches. Prefix every variable name with something yours. Check before you take, coin or otherwise. Clean up what you conjure. Keep mpsleep out of loops, guard every long scene with a busy flag, and clear the flag on every exit. Rehearse with mudprog test but verify branches with the real event, because the test bench carries no real item and the word test for a message. And when two blocks must cooperate, write the agreement down in the variable's name.
Where To Go Next
The cookbook chapter is fifteen finished builds in this same spirit, each bigger than an exercise and smaller than a project; you now have every technique its recipes assume. The triggers chapter turns the handful of triggers this workbook leaned on into the full catalogue of about sixty. The bus chapter teaches the veto layer, scripts that REPLACE actions rather than react to them, which is the one major system this workbook has not touched. And when a script of yours misbehaves in the wild, the reference chapter is the fastest path from symptom to cause. Take the gate warden with you: nearly everything you build from here is that mob with different clothes.
This is the advanced workbook: a set of twenty guided exercises that take everything the reference chapters teach one piece at a time and make you USE it, combined, under pressure, the way real building demands. The earlier workbooks drilled greetings, speech, memory, and control flow. This one drills the deep machinery: the message bus that lets a script veto an action before it happens, the zapper masks that filter who may set a trigger off, the text masks that give a creature ears for raw sentences, the FUNCTION_PROG routines that turn a pile of scripts into a library, the quest commands and their ticking clock, and the world-wide triggers that let one object hear the whole mud breathe. The final exercise builds a complete scripted encounter from an empty mob, using nearly all of it at once.
You do not need to have coded before. You DO need the foundations: read help mudprog-basics first if PROG blocks, tildes, and dollar codes are not yet second nature, and skim help mudprog-bus and help mudprog-functions so the vocabulary in this workbook rings a bell. Every exercise here still explains what it uses, from zero, because explaining twice is cheaper than confusing once. But the pace is quicker than the core chapters, and the exercises assume you can attach a script, view its trigger list, and clear it without thinking.
How to work each exercise. Every one follows the same shape. The brief states a building problem the way a head builder would hand it to you. Think it through poses the questions you should ask before touching the keyboard. The solution shows a complete, working script, exactly as you would type it. Why it works walks the script line by line. Where it goes wrong covers the mistakes people actually make, what each one looks like in game, and the fix. Push it further offers variations to build on your own. The honest way to use the workbook is to stop after the brief, attempt your own script, and only then read on. Nobody is watching; the mud does not grade you; but the attempt is where the learning lives.
Setting Up Your Workbench
You want a quiet room, a practice mob, and a few harmless props. Any out-of-the-way room in your own area does; if you have admin access, the test npc command conjures a sparring dummy on the spot, and a dummy takes scripts as happily as any hand-built mob. Every item path used in this workbook is a stock object that exists on this mud, chosen so each script works exactly as printed: /obj/meal, /obj/torch, /obj/armor, and /obj/container. When an exercise says attach to an item, clone one of those and script it; when it says attach to a mob, use your dummy.
Attach with the editor, as always:
mudprog dummy edit
then the lines, each block's tilde, and a single period to save. View with mudprog dummy and read the Triggers line every single time; at this level, scripts hold four and five blocks, and the Triggers line is your proof that every block parsed. Clear a botched experiment with mudprog dummy clear and start over.
Two safety habits worth making automatic before you work at this level. First, never point the destructive movers and killers at a real person while practicing: mpslay, mppurge, mptransfer, and mpgoto belong aimed at props and at things your own script loaded, not at $n, and no exercise in this workbook ever aims them at a player. Second, remember the engine's standing promise: a broken script never blocks a game action and never crashes anything. On the bus, an error counts as permission. The worst you can do is silence or spam, and one clear fixes either.
Honest Testing At This Level
The hand test, mudprog dummy test GREET_PROG, fires a trigger with YOU standing in as the source and with the single word test as the pretend message. That was a footnote in the basics chapter; at this level it decides what you can and cannot check by hand, so it is worth spelling out precisely.
What hand-fires cleanly: blocks whose header is a percent, blank, or the word all; zapper-masked blocks whose mask YOU pass, since the mask is checked against you as the source; bus blocks whose header is exactly ALL. What does NOT hand-fire: keyword and phrase headers whose words do not appear in the word test, which is nearly all of them; bus blocks whose header names a code, such as CNCLMSG_PROG GET, because that header slot is a code-spec matched against message text, and the word test is not it; QUEST_TIME_PROG blocks, whose pretend message lacks the quest id and minute the header demands; and DAY_PROG blocks, since the word test is not a day number.
None of that means those blocks are broken. It means the hand test can only PARSE-check them, and the real check is the real event: actually try to take the relic, actually walk into the warded room, actually say the password, actually accept the timed quest. Each exercise below tells you which parts show up under the hand test and which demand the real thing. When a script seems dead, check the Triggers line first, then ask whether you are testing with the real event or with the word test.
One more habit for bus work in particular: test with a second character or a willing friend when you can. Vetoes read differently from the outside, and half the polish in a good veto is the line the BYSTANDERS see.
Part One: Saying No, And Watching
The message bus in six exercises. The one-sentence recap, from the bus chapter: before most physical actions commit, every scripted object near the action is asked whether a CNCLMSG_PROG block matches, and the first match RUNS IN PLACE OF the action; after an action commits, EXECMSG_PROG observers get to see it go by. A cancel block replaces; an observer watches. Everything else is aim.
Exercise 1: The Warden Stone
The brief. The head builder hands you a carved stone and says: while this sits in a room, nothing in that room obeys anyone. No taking, no dropping, no opening, no eating, no fighting, no leaving. Make the refusal feel like the stone's doing, not like an error message.
Think it through. Which trigger stops an action before it happens? What code-spec matches every action at once? And the question that separates working vetoes from embarrassing ones: when the action is cancelled, who says so, and to whom?
The solution. Attach to the stone:
CNCLMSG_PROG ALL
mpecho A ring of grey light flares around the warden stone.
mpechoat $n The warden stone denies you. Your hand closes on nothing.
~
Why it works. The header names the cancel trigger and the wildcard code-spec ALL, so this one block matches every message code the bus carries: GET, DROP, WEAR, OPEN, EAT, ATTACK, ENTER, LEAVE, all of them. When anyone in the room attempts any of those, the engine finds this block before the action commits, cancels the action, and runs the body as the replacement. The body is the manners: mpecho gives the whole room the stone's flare, and mpechoat gives the refused person a private line, because $n inside a bus block is always the person who tried the deed. Nothing else prints, because the normal you-get-the-sword messages belong to an action that no longer happens.
Where it goes wrong. The classic first attempt is a cancel block with an EMPTY body. It works, in the sense that nothing can be done in the room, and it fails completely as building, because players type commands and the world says nothing back, which reads as a bug, not a ward. Rule: a cancel block always narrates its refusal. The second classic is leaving the stone in a room people actually need to use; an ALL veto is a sledgehammer, and the moment it is attached, even YOU cannot pick the stone back up to remove it. You do not need to: clear the script with mudprog stone clear and the room breathes again. That is also your reassurance against the deepest worry here, locking yourself out forever: a veto is only ever one clear away from gone, and if the script errors, the engine counts the error as permission anyway.
Testing. This is the one bus header that hand-fires: the header is exactly ALL, so mudprog stone test CNCLMSG_PROG runs the body and shows you both lines. Remember what that test is: a costume rehearsal of the WORDING. No action was in flight, so nothing was cancelled. The real test is to drop the stone in a room and try to pick something up.
Push it further. Give the stone a memory with mpsetvar, counting refusals, and let the tenth refusal change the wording. Or narrow the sledgehammer into a scalpel, which is exactly the next four exercises.
Exercise 2: The Offering Bowl
The brief. A chapel keeps a brass offering bowl. During services the curate lifts it and passes it along the pews, hand to hand, and the bowl should chime softly each time it changes hands. But wherever the bowl comes to REST, it must be impossible to simply take: anywhere, forever, even if someone carries the whole altar off to another room.
Think it through. If the veto must travel with the bowl wherever it goes, where does the script live: on the room, or on the bowl? What code-spec narrows the warden stone's ALL down to just pickups? And which ordinary trigger fires on an ITEM when one person hands it to another?
The solution. Attach to the bowl:
GIVING_PROG 100
mpecho The offering bowl chimes softly as it changes hands.
~
CNCLMSG_PROG GET ALL
mpechoat $n The offering bowl grows searing hot in your grip.
mpechoaround $n $N snatches a hand back from the offering bowl.
~
Why it works. Two blocks, two mechanisms. GIVING_PROG is an ordinary trigger from the triggers chapter that fires on the ITEM ITSELF when someone hands it to someone else; $n is the giver, $t the recipient, and $o the bowl. So passing the bowl along the pews is not merely allowed, it is blessed with a chime, which teaches the congregation the right way to move it. The second block is the veto. Its header reads: cancel pass, code GET, any message text. On an item's own script that combination means any attempt to take THIS item, because an item is only consulted by the bus when it is itself the subject of the event. The body plays both audiences: the thief gets the searing grip, and mpechoaround shows everyone EXCEPT the thief the flinch, so nobody reads both lines and the moment lands from every seat in the chapel. Because the script lives on the bowl, the protection follows it into any room, any container, any future area.
There is a quiet accuracy lesson in the choice of GIVING_PROG, and it is worth pausing on because it decides half of all item scripting. An item does not hear every ordinary trigger. Arrival triggers reach rooms and mobs; the look trigger reaches mobs; speech triggers reach mobs. An item hears the events that happen TO it, being given, taken, dropped, put, worn, removed, plus the whole message bus, where every scripted object in the room is in scope. Before you attach an ordinary trigger to an item, check the triggers chapter for who the event actually reaches; a block on a host that never hears its event parses cleanly, shows up in the Triggers line, and does nothing, forever, which is the quietest possible failure.
Where it goes wrong. First mistake: writing the veto on the ROOM instead, as CNCLMSG_PROG GET bowl. It works while the bowl is in that room and evaporates the moment the bowl moves, and the mask brings a subtler bug: masks match by substring against the item's key name, so a mask of bowl also catches a punchbowl someone carries in. Prefer the item-side script for protecting an item; save room-side masked vetoes for policing a place. Second mistake: expecting the hand test to prove the veto. The header GET ALL is a code-spec plus mask, the hand test's message is the word test, they do not match, and the test reports nothing fired. That is the mask doing its job against the wrong message. Hand-fire the GIVING block, whose header is an ordinary percent, then walk up and honestly type get bowl for the real thing. Third, remember what a carried item can and cannot do: riding in an inventory, the bowl can still refuse to be dropped, given, or put somewhere, because it is the subject of those events, but it cannot veto its carrier's UNRELATED actions; it is not in scope for them.
Push it further. Add a CNCLMSG_PROG DROP ALL block with its own flavor and the bowl also refuses to be set down anywhere but its altar, at which point you have built the mirror of the bus chapter's cursed armband: an item that can be taken but never shed is a curse, an item that can be passed but never pocketed is a relic. Or count attempts per thief by writing a note on $n inside the veto, and have the third attempt summon the curate by name.
Exercise 3: The Sealed Sanctum
The brief. A doorkeeper stands inside the inner sanctum of a temple. Nobody walks in while he stands there; would-be entrants are turned back at the threshold with a firm palm, and everyone inside sees them turned away. He should also answer anyone who speaks near him with a flat refusal to discuss it.
Think it through. Which bus code fires when a creature is about to walk into a room? Where can the script live so the doorkeeper does the stopping: must it be on the room itself? And a trap from the bus chapter: what does the message text hold for a movement event, and what does that mean for masks?
The solution. Attach to the doorkeeper, standing in the sanctum:
SPEECH_PROG all
say The sanctum is sealed. Speak your business elsewhere.
~
CNCLMSG_PROG ENTER
mpechoat $n The doorkeeper's flat palm stops you at the threshold.
mpechoaround $n $N is turned back at the sanctum door.
~
Why it works. ENTER is the movement code, and its scope is deliberately generous: the primary object of an ENTER is the DESTINATION room, and the engine consults that room, the occupants of that room, and the room being left from. A scripted doorkeeper standing INSIDE the sanctum is therefore in scope for every attempt to enter it, and his block fires before the walker crosses, cancelling the move. The walker stays where they are and gets the palm; the sanctum's occupants see the turning away. The speech block is ordinary furniture, there to make him feel like a person rather than a wall.
Two rules about ENTER worth engraving. First, movement events carry NO message text, so a mask on an ENTER or LEAVE header can never match anything; write the bare code, as above, and do any narrowing inside the body. Second, a creature dragged along automatically because it follows its group leader is not re-checked; the leader was checked and the group moves as one. Design doors knowing the whole party comes through together or not at all.
Where it goes wrong. The big one: trying to write a members-only door by putting an if in the body and expecting the yes-branch to allow the entry. Read this twice, because it is the honest heart of the cancel rule: a CNCLMSG_PROG block cancels WHENEVER it runs. There is no allow command; the if can only choose what the refusal looks like. The members-only pattern is built inside out: the block refuses everyone, and in the branch for people you want inside, the script performs the entry ITSELF, by force, with mptransfer aimed at the inner room's real file path; the transfer does not pass through the bus, so it cannot re-trigger the veto. That pattern is powerful and easy to fumble, so build it with a test character, never a bystander, and ask first whether a locked door with a key item tells your story more simply. The small mistake: hand-testing this veto. The header is the bare code ENTER, which is a code-spec, not the word all, so the hand test reports nothing; the SPEECH block hand-fires fine, and the real test is walking at the door with a second character.
Push it further. Vary the message by visitor: an if on isnpc($n) can give creatures a different, rougher turning-away. Move the same block onto the ROOM and the ward works with no doorkeeper at all; then the refusal should sound like the room. Or veto LEAVE instead and you have built the other classic, the room that does not let you go, which is a horror set-piece in one block; use it kindly.
Exercise 4: The Shrine Of The Stilled Blade
The brief. A hilltop shrine is consecrated ground: while the consecration holds, no fight can BEGIN there, not by players, not by wandering monsters. Arrivals should feel the calm settle over them as they enter. A would-be aggressor should feel their arms refuse, and the shrine should answer with a single chime.
Think it through. Which code fires at the moment combat is initiated? Does it fire for aggressive creatures too, or only for players typing the attack command? Whose script is consulted when an ATTACK happens in a room? And a design question: is the shrine truly safe ground, or only mostly safe ground?
The solution. Stand in the shrine and attach to the room itself, with mudprog here edit:
GREET_PROG 100
mpechoat $n The shrine's calm settles over you like deep water.
~
CNCLMSG_PROG ATTACK ALL
mpechoat $n Your arms refuse the violence. The shrine holds them still.
mpecho A single low chime rolls through the shrine.
~
Why it works. ATTACK is the initiation code: it fires when a player types the attack command AND when an aggressive creature tries to start a fight, so one block calms both. The room is always in scope for every bus action inside itself, so the room's own script polices every fight attempted there: the block matches, the fight never starts, the aggressor gets the stilled arms, the shrine gets the chime. And rooms hear the arrival trigger, GREET_PROG fires on a scripted room when someone walks in, so the same script delivers the deep-water calm to each newcomer, no doorkeeper needed.
Could the veto live somewhere else? Yes, and knowing where is Exercise 2's lesson again from the other side. Bus scope consults EVERY scripted object in the room, living or not, so the ATTACK block would work exactly as well on a guardian statue standing by the altar; furniture can police a room. But the GREETING could not move with it, because ordinary arrival triggers reach rooms and mobs only, and a statue is neither. Split a build across hosts and you must give each host only the triggers it can actually hear.
The honesty paragraph, because good builders design with the limits. ATTACK is the moment of initiation, not every blow. A fight that started OUTSIDE the shrine and spills in through the door is already running and is not re-checked, so a fleeing player can drag their pursuer onto consecrated ground mid-battle. Decide whether that is the story you want; if the shrine must also smother running fights, that is a job for the room's design and a senior builder, not for this block alone.
Where it goes wrong. Aiming the protection at the wrong scope. If the brief had been protect THIS one creature rather than this one place, the block belongs on the creature: the victim is the primary object of an ATTACK, so the victim's own script is consulted first, everywhere it goes. A room-level script can also protect one resident by name, because the ATTACK message text carries the victim's key name and masks match against it; a mask of the curator's name on the shrine's script guards just the curator. And the testing trap repeats: ATTACK ALL is a code-spec header, the hand test will not fire it; hand-fire the GREET block, then genuinely attack your dummy with a spare character, or lead an aggressive mob in and watch it stand down.
Push it further. Count refusals on the aggressor with mpsetvar $n and let a third attempt earn a real condition through mpaffect, a few seconds of rooted contemplation. Or pair this script with Exercise 3's ENTER veto, narrowed in the body to creatures only, and the shrine turns violence away at the door before it can even try.
Exercise 5: The Null Vault
The brief. The mages guild stores dangerous things in a vault where magic simply fails. Every class ability, every spell, smothered. But the guild has a grudge: fireball, the spell that burned the old vault, gets its own special, more humiliating failure line. Visitors should feel the deadness of the air as they walk in.
Think it through. Which code covers ability use, and is it only mage spells? If two cancel blocks both match one cast, what happens? And in the failure line, how do you name the very spell that just died?
The solution. Attach to the vault room itself:
GREET_PROG 100
mpechoat $n Dead air presses in here. Magic feels very far away.
~
CNCLMSG_PROG CAST fireball
mpechoat $n Your fireball collapses into a cough of cold soot.
~
CNCLMSG_PROG CAST ALL
mpechoat $n The null field swallows your $g half-spoken.
~
Why it works. CAST fires for every class ability, not only mage spells; a warrior's skills pass through it too, so this vault is a null-SKILL zone, which is what a vault wants. The first cancel block carries a mask: it matches only when the message text, which for CAST is the skill or spell name, contains fireball. The second matches everything. Here is the rule this exercise exists to teach: when one object holds SEVERAL matching cancel blocks, they ALL run, top to bottom, in script order. A fireball cast matches both blocks, so the caster reads the soot line and then the general line. Order is therefore a writing tool: the specific flavor leads because it is written first. Inside the general block, $g holds the name of the smothered ability, so the line names whatever just failed without you writing a block per spell. And if you would rather the ward be an object, the two CAST vetoes would ride a warding brazier just as well, bus scope again, but the greeting would have to stay on the room or move to a mob; Exercise 2's rule about who hears what follows you everywhere.
One honesty note from the bus chapter bears repeating: when a class skill is vetoed, the skill system prints its own short cannot-do-that notice after your text. Your line lands first; write it so the pair read naturally together.
Where it goes wrong. Writing the general block FIRST and wondering why the fireball flavor reads as an afterthought; order within the script is the only ordering there is. Expecting a percent or a zapper mask in a bus header; that slot is a code-spec and text mask, nothing else, and person-filtering on the bus is done in the body with conditions. Testing by hand: the GREET block fires under the hand test, both CAST headers are code-specs and do not, so the real test is casting something, then casting fireball, in the room, with any class handy.
Push it further. Let veterans feel the field strain: an if on level($n) in the general block choosing between the field strains, then smothers your $g anyway and the crumble-to-ash line gives the place a sense of graded power. Or invert the whole build: a shrine where only HEALING works is a CAST ALL veto whose body checks $g against a shortlist and, for the allowed names, tells the caster the shrine permits it, while a companion block does not exist to actually allow it; remember the honest cancel rule from Exercise 3, and design around what cancel can and cannot do.
Exercise 6: The Brass Ledger
The brief. A counting-house keeps a brass ledger on a stand. It prevents nothing and forgets nothing: every action that happens in the room writes another numbered entry, out loud, and the number keeps climbing for as long as the ledger exists.
Think it through. Which bus pass observes without preventing? Where does a number live so it survives between firings? And the arithmetic wrinkle every counter meets on its first run: what is an unset variable plus one?
The solution. Attach to the ledger:
EXECMSG_PROG ALL
if var($i entries) == ''
mpsetvar $i entries 0
endif
mpsetvar $i entries $%math($<$i entries> + 1)%
mpecho The ledger scratches out entry number $<$i entries> by itself.
~
Why it works. EXECMSG_PROG is the observe pass: the action goes through normally, with all its usual messages, and THEN this block runs. With the wildcard ALL it observes every completed bus action in the room, pickups, drops, wearings, openings, all of it. The body is the counter idiom, worth memorizing as a unit. The if seeds: var($i entries) reads a script variable stored on $i, the ledger itself, and comparing it to two quote marks with nothing between them asks is it still empty, which is only true before the first entry; if so, the count is filed as 0. The next line does the arithmetic: $<$i entries> is the angle form that reads the stored count as text, $%math(... + 1)% asks the engine to add one, and mpsetvar files the result back. Seeding matters because an unset variable reads as empty text, and empty plus one does not arithmetic make. The echo then reads the fresh value with the same angle form. Fire after fire: entry number 1, 2, 3.
Where it goes wrong. Three ways. Expecting the observer to PREVENT something: it cannot, ever; by the time it runs the deed is done, and that is precisely why you can attach observers generously, since the worst a buggy one does is stay silent. Forgetting which codes reach observers: the observe pass sees GET, DROP, PUT, WEAR, REMOVE, OPEN, CLOSE, LOCK, UNLOCK, plus GIVING for completed hand-overs, CONSUME for completed eating or drinking, and CASTING for completed abilities; ENTER, LEAVE, ATTACK, BUY, and SELL do not fan out here, because richer triggers already watch those moments, GREET_PROG and FIGHT_PROG and the vendor triggers. And storing the count in a numbered slot like $1 instead of a variable: slots live only for one script run, so the ledger would proudly announce entry number 1 forever.
Testing. A rare treat: the header is exactly ALL, so mudprog ledger test EXECMSG_PROG hand-fires it, and firing repeatedly shows the number climb. Then do it honestly: drop a torch, pick it up, and watch two entries write themselves. Note who is watching, too: on the observe pass even non-living scripted objects in the room run their blocks. This ledger is furniture with eyes, which is a thing the per-event triggers alone cannot give you.
Push it further. Narrow ALL to DROP and the ledger becomes a donations book. Store per-person counts by filing onto $n instead of $i and it remembers each visitor's habits. React only to certain goods by adding a mask after the code, remembering masks match the item's key name by substring. Or pair it with mpfaction and let dropping contraband in front of it carry consequences.
Part Two: Choosing Your Audience
Four exercises on aim. Zapper masks filter WHO may fire an ordinary trigger. The text masks, IMASK and REGMASK, listen not to events but to sentences. Both are small tools that make scripts feel uncannily attentive when pointed well, and both have one signature trap each.
Exercise 7: The Doorman's Checklist
The brief. A private club employs a doorman with opinions. Players get a warm word just for being real. Anyone who has walked at least a mile of the road, level one and up, gets a nod to their travels. Guests in the middle chapters, levels ten through twenty, get respect for the dangerous stretch. Elves and dwarves get the old-peoples welcome. And creatures get the door barred. All in one script, no if lines at all.
Think it through. Where does a filter live if not in the body? What does a header like -level 10-20 mean, exactly? If one guest matches three blocks, how many lines do they hear?
The solution. Attach to the doorman:
GREET_PROG -player
say Flesh and blood at my door. Welcome, $N.
~
GREET_PROG -level 1
say You have walked at least one mile of the road.
~
GREET_PROG -level 10-20
say The middle chapters. A dangerous stretch, $N.
~
GREET_PROG -race elf dwarf
say One of the old peoples honors my little club tonight.
~
GREET_PROG -npc
emote bars the door firmly against the creature.
~
Why it works. The header slot of an ordinary trigger, the same slot that holds a percent or speech keywords, can instead hold a zapper mask: a row of clauses, each a dash-word naming a property of the person who fired the trigger, followed by the values that qualify. The mask passes only if EVERY clause passes, and a clause passes if the person matches ANY of its values; dashes are AND, values inside a clause are OR. Here each block carries a one-clause mask. -player takes no values and asks only that the arrival be a real player; -npc is its mirror. -level with a bare number means at-or-above that level; with a form like 10-20 it means within the range, inclusive. -race lists races, any of which qualifies. When a level fifteen dwarf player walks in, the engine checks every GREET block independently and fires each whose mask passes: this guest hears the flesh-and-blood line, the mile-of-road line, the middle-chapters line, AND the old-peoples line, four blocks, in script order. A wandering mob gets only the barred door.
The full clause list, for your notes: -class, -race, -sex, -name, -deity, -level, -player, -npc, plus -good and -evil, accepted for CoffeeMUD compatibility though this mud does not track alignment. Values may carry a CoffeeMUD-style leading plus sign, which is simply ignored, and everything is case-insensitive. A clause type the engine does not recognize is skipped rather than failed, so pasted CoffeeMUD masks degrade gracefully.
Where it goes wrong. Three rules cover nearly every zapper bug. First, the header holds ONE thing: a percent, or keywords, or a mask, never a combination; the engine decides which you meant by the first character, so a mask must begin with its first dash-word, and a header like 50 -player is not half chance, half filter, it is nonsense read as keywords. Exercise 8 shows the right way to combine. Second, the mask always filters the SOURCE, the person who walked in or spoke, never the scripted mob itself. Third, zappers belong to ORDINARY triggers; written in a bus header they are read as a code-spec and do nothing useful, so on the bus, filter people in the body with conditions.
Testing. Hand-firing GREET_PROG checks each block's mask against YOU: you will hear the player line, the level lines your level satisfies, and the race line only if you happen to be an elf or dwarf. The -npc block stays silent, correctly. For the full effect, walk a mob through or borrow a friend of the right race, and read which lines fire.
Push it further. A self-check, worked. Question: which lines does a level twenty-five human player hear? Answer: flesh and blood, and the mile of road; twenty-five is outside 10-20, human is not on the race list, and they are not an NPC, so exactly two lines. Now add your own block for a named regular with -name and watch the doorman recognize one person in the whole world. Multi-clause masks are just more dashes on one header: a block headed with -class mage -level 20 fires only for mages of level twenty and up, both clauses at once.
Exercise 8: Sometimes, And Only For Them
The brief. Two requests that sound simple and collide with the one-thing-per-header rule. First: a clerk who reacts to players only, but speaks just half the time, so he does not become wallpaper. Second: a herald who fires every time, but never, ever, for creatures.
Think it through. The header can hold the zapper OR the percent, not both. Which half of each job goes in the header, and which moves into the body? What tools does the body have for chance and for kind?
The solution, first script. The zapper takes the header; the chance moves inside:
GREET_PROG -player
emote glances up from his ledger at $N.
if rand(50)
say Do not mind me. Half the time I say nothing at all.
endif
~
The solution, second script. The percent takes the header; the kind check moves inside, as an early exit:
GREET_PROG 100
if isnpc($n)
return
endif
say Every time, but never for creatures.
~
Why it works. rand(50) is the dice function: true fifty times in a hundred, so the say fires half the time, while the emote above it fires every time a player arrives, which keeps the clerk visibly alive even on his quiet rolls. The second script is the guard-then-continue shape from the flow chapter: isnpc($n) asks whether the arrival is a creature, and return ends the whole block on the spot, so the say below runs only for everyone else. Both scripts do exactly what a combined header would have meant, with the split chosen so the header carries whichever filter reads most naturally at a glance.
Where it goes wrong. Writing GREET_PROG 50 -player and assuming the engine reads your intent; it reads keywords. Putting the deterministic line AFTER the dice instead of before, which makes the script untestable by eye, since silence might be the dice or might be a bug; leading with a certain line is a habit that pays for itself every testing session. And inverting the guard: if ispc($n) around the whole body works, but three exits deep it nests badly; the early return keeps long scripts flat, and at this level your scripts are getting long.
Testing. Hand-fire the first script a few times: the glance is certain, the spoken line comes and goes with the dice. The second script always speaks for you, since you are not a creature; lead a mob in to see the return bite.
Push it further. Chance AND kind AND level: zapper -player in the header, then if rand(25) around the flourish, then a bare level comparison like level($n) >= 30 choosing the wording. Any number of filters stack once you accept the header holds one and the body holds the rest.
Exercise 9: The Braggart
The brief. A pit fighter is in love with his own left hook. Whenever HIS OWN actions produce a line mentioning it, his greeting boast, his combat spam when the blow lands, he flexes, delighted. Other people talking about left hooks do not set him off; only his own noise does.
Think it through. Which trigger listens to the text of a mob's own output? What is the matching rule: whole words, substrings, case? And the trap: what must the REACTION line never contain?
The solution. Attach to the fighter:
GREET_PROG 100
say My left hook is the stuff of legend, $N.
~
IMASK_PROG left hook
emote flexes, delighted with the sound of that.
~
Why it works. IMASK_PROG is the self-listener: it fires when a line produced by the scripted object's OWN action contains the header text. Read the I as I-myself. The greeting makes him say a sentence containing left hook; the echo of his own speech passes his senses; the mask matches, case-insensitively, by plain substring, with color codes stripped before matching; and the flex follows the boast all by itself. In live combat the same mask catches his own combat lines when they happen to mention the hook. Other people's speech does NOT fire an IMASK, which is the entire point of the trigger; the wide net is the next exercise.
Where it goes wrong. The trap you must check every single time you write a text mask: if the reaction line CONTAINS the pattern, the mob's own reaction re-triggers the mask, which reacts again, and around it goes until the engine's safety limits cut the chain. Look at the solution: the pattern is left hook, and the flex line says the sound of that, deliberately. Reread your reaction and make sure the pattern does not occur in it, ever. Second trap, quieter: an IMASK_PROG with an EMPTY header matches every line the mob's own actions produce, which turns him into a metronome of self-regard; always give the mask words. Third: expecting IMASK to hear other people. It will not; it is deaf to everything but the mob's own doings.
Testing. Hand-fire GREET_PROG and watch the chain: the boast, then the flex, two beats from one trigger, which proves the mask heard the mob's own line. Then poke him into a fight and listen for the flex when his combat spam mentions the hook.
Push it further. Give him a matching sulk: a second IMASK on the text of his own misses, with a muttered excuse. Mobs that react to their own performance read as vain, anxious, or proud with almost no scripting, and IMASK is the whole trick.
Exercise 10: The Harbor Watcher
The brief. A retired sea captain sits in the tavern. Any time the word dragon crosses his senses, anyone saying it, shouting it, an arrival announcement containing it, anything, he checks his charts with a worried frown. He should never set HIMSELF off, no matter how often he worries.
Think it through. Which trigger sees every line a creature perceives? Is its pattern a plain word or something stronger, and is it case-sensitive? And the same trap as Exercise 9, now with the net wide open: how do you keep him from feeding himself?
The solution. Attach to the captain:
GREET_PROG 100
say They speak of a dragon under the harbor, $N.
~
REGMASK_PROG dragon
emote checks the harbor charts with a worried frown.
~
Why it works. REGMASK_PROG is the wide net: it fires when ANY line the scripted creature sees matches the header pattern. That is every line: speech, emotes, combat spam, arrivals, all of it, with color codes stripped first. The pattern is a real regular expression, and it is case-sensitive; if you do not know regular expressions, ordinary lowercase words work perfectly, they simply match themselves, and that is what this solution does. The greeting exists for the demonstration: the captain himself mentions the dragon, the line crosses his own senses, and the REGMASK fires, so one hand-fired trigger shows the whole chain. In live play, any patron saying dragon in his room sets off the same frown, because their speech is a line he sees.
Where it goes wrong. Feeding himself. The reaction line must not contain the pattern; checks the harbor charts does not say dragon, by design, and if it did, the frown would trigger the frown until the engine's safety limits cut the chain, with a room full of witnesses. Case bites here too: the pattern dragon will not match Dragon at the start of a sentence; for a name likely to be capitalized, either mask the lowercase tail, ragon, which as a substring-style pattern catches both, or accept the miss. And know the difference from SPEECH_PROG: speech triggers hear SPEECH, give you the speaker in $n, and are the right tool for conversations; REGMASK hears EVERYTHING and knows only the line, carried in $g, with no separate actor. Use speech triggers for dialog and REGMASK for perception.
Testing. Hand-fire GREET_PROG and watch boast then frown. Then say the word dragon yourself in his room and watch the frown fire with no greeting at all, which is the wide net working. Then, for education, edit the emote to contain the word dragon, fire it once, watch the stutter, and fix it; a loop you have caused on purpose is a loop you will never ship by accident.
Push it further. React to the matched line itself: $g holds it, so the captain can mutter about exactly what he heard. Combine with a counter on $i and the fifth mention of dragons sends him out the door. This trigger is also how furniture and mobs react to emotes today: an emote is just a line, and the net catches it.
Part Three: Writing A Library
Three exercises on FUNCTION_PROG, the named routine. Everything you have written so far runs when the world pokes it. A FUNCTION_PROG never fires on its own: it is a block with a NAME, which other blocks call on demand, and which can hand a value back. This is the point where a bag of scripts becomes a library, and where you stop writing the same five lines in four places.
Exercise 11: The Bell And The Blessing
The brief. A temple greeter performs two rituals for every arrival: a bell is rung in their honor, and a blessing is spoken over them. The head priest is fussy and rewords both rituals monthly, so each must be written EXACTLY ONCE, in one place, no matter how many triggers eventually use them.
Think it through. How do you write a block that never fires on its own? How does another block invoke it, and how does the visitor's name get IN? And for the blessing, which must be SPOKEN by the greeter, how does text come back OUT?
The solution. Attach to the greeter:
GREET_PROG 100
mpcallfunc fanfare $N
say $%callfunc(blessing $N)%
~
FUNCTION_PROG fanfare
mpecho A small brass bell rings twice for $G.
~
FUNCTION_PROG blessing
return May the long road rise to meet you, $G.
~
Why it works. A FUNCTION_PROG's header argument is its NAME, not a chance or keywords, and the engine never fires it for any world event; it exists to be called. There are two calling styles, and the greeting uses both on purpose. mpcallfunc fanfare $N is the command style: run the named routine now, passing everything after the name as the argument. Inside the routine, that argument text arrives as the message, readable as $G, so the bell rings for the visitor by name. The second style is the function form, callfunc(blessing $N), wrapped in $%...% so its RESULT is spliced into the say line. Where does the result come from? The routine's return line: return followed by text hands that text, dollar codes substituted, back to whoever called, and the greeter speaks it. Command style for routines that ACT, function style for routines that ANSWER; that division carries you a long way.
Where it goes wrong. Firing a routine with the hand test: mudprog greeter test FUNCTION_PROG does nothing, and correctly, because the header slot holds a name, and the test machinery has no reason to match it; routines are only ever reached through callers, so test the CALLER. Misspelling the name in the call: names match case-insensitively, but fanfare and fanfair are different words, and a call to a routine that does not exist simply answers empty text, no error, which shows up as a say line with a hole in it. Defining two routines with the SAME name: only the first is ever found. And the ever-present tilde: forget the ~ between the two routines and the second becomes trailing lines of the first, visible immediately because the Triggers line shows one FUNCTION_PROG where you wrote two, and a stray return line ends the first routine early.
Testing. Hand-fire GREET_PROG: bell, then blessing, both personalized. Then edit ONLY the blessing routine's return line and fire again; the greeting block did not change, but the spoken words did, which is the whole argument for libraries in one edit.
Push it further. Add a SPEECH_PROG on the word bless that calls the same blessing routine; two triggers, one ritual, zero duplication. That is Exercise 13's subject, done properly.
Exercise 12: The Bridge Toll
The brief. A bridge troll charges by strength: the toll is two coppers per level of the traveler. He announces each arrival's personal toll, and for anyone whose toll reaches a hundred coppers he adds a greedy compliment. The toll FORMULA must live in exactly one place, because the troll's rates change with his moods.
Think it through. The routine must COMPUTE, not just word-swap: where does arithmetic happen? How does a number come back, and where does the caller put it so several lines can use it? Do the caller's numbered slots survive inside the routine?
The solution. Attach to the troll:
GREET_PROG 100
mpargset 2 $%callfunc(toll $N)%
say The bridge toll for $N stands at $2 coppers.
if $2 >= 100
say For you that is nearly a fortune. Good.
endif
~
FUNCTION_PROG toll
mpargset 3 $%level($n)%
return $%math($3 * 2)%
~
Why it works. Read the routine first. level($n) asks the level of the person who set the whole chain off; even inside a called routine, $n still means the original source, because the call passes the actor along. mpargset 3 parks that number in slot three, and the return line hands back $%math($3 * 2)%, the level doubled. Now the caller: mpargset 2 catches the returned number in slot two, the say line reads the slot into the announcement, and the if compares the slot against a hundred with a bare comparison, no function wrapper needed. One computed value, used twice, stored once.
The subtle lesson is the one you cannot see: the routine used slot THREE and the caller used slot TWO, but they did not have to differ. Each call runs in its own fresh workspace: the routine's numbered slots start empty, and nothing it does to them touches the caller's. Only two things cross the boundary: the argument text going in, readable as $G, and the return value coming out. Routines cannot clobber their callers, which is exactly what makes them safe to call from everywhere.
Where it goes wrong. Trying to nest one $%...% inside another, as in a single line that both calls and adds; the substitution reads to the first closing percent sign and the nesting breaks. The cure is exactly what both blocks do: stage into a slot with mpargset, then use the slot, one substitution per line. Doing arithmetic without math(): the engine substitutes text, it does not evaluate stray plus signs, so a say line containing $3 * 2 says the symbols, not the product. And expecting decimals: math() is whole-number arithmetic, left to right, good for tolls and counters, not for interest rates.
Testing. Hand-fire GREET_PROG: your own level, doubled, announced; past level fifty the compliment follows. Have a lower-level friend step up, or set a dummy's level, and watch the toll change per visitor while the script stands still.
Push it further. Compare the routine's answer DIRECTLY, no staging: a condition may call and compare in one breath, as in if callfunc(toll $N) >= 100, because a function call in an if may carry its comparison after the parentheses. Then swing the rates: edit only the return line to triple, and every announcement, comparison, and compliment follows the new price. That is the library dividend, paid again.
Exercise 13: The Whispering Stacks
The brief. A haunted library has a voice, and the voice must be ONE voice. Whether a visitor arrives, speaks, or fumbles a command the game does not know, the stacks respond in the same eerie register. The builder before you wrote three separate scripts and they drifted apart in tone; your version keeps every whisper in a single routine.
Think it through. Three different triggers must funnel into one block: how does the routine know WHICH occasion called it? What control-flow tool picks one response from several by a word?
The solution. Attach to the librarian ghost, or any scripted mob in the room:
GREET_PROG 100
mpcallfunc whisper arrives
~
SPEECH_PROG all
mpcallfunc whisper speaks
~
CMDFAIL_PROG all
mpcallfunc whisper fumbles
~
FUNCTION_PROG whisper
switch $G
case arrives
mpecho The stacks lean in, weighing the newcomer.
break
case speaks
mpecho The stacks murmur the words back, faintly wrong.
break
case fumbles
mpecho The stacks rustle with something like dry laughter.
break
endswitch
~
Why it works. Each trigger block has shrunk to a single line: call the whisper routine, passing one word naming the occasion. The routine reads that word back as $G and switches on it, and the switch, from the flow chapter, picks exactly one case by matching the word. All the personality lives in one block; the trigger blocks are just doorbells. This is the dispatcher pattern, and it is how big scripted set-pieces stay maintainable: triggers name occasions, one routine performs them.
Where it goes wrong. Forgetting that SPEECH_PROG all really means ALL: every sentence spoken in the room draws a murmur, which for a haunted library is the point, and for a shop would be torture; narrow the header to keywords anywhere the voice should be rarer. Misspelling an occasion word in a caller: the switch matches no case, and with no default the routine does nothing, silently; add a default case that whispers something generic and misspellings become audible instead of invisible. And a scoping reminder: CMDFAIL_PROG fires for failed commands typed in the mob's ROOM, with the failed line riding in $g, and the player still gets the normal error; your laughter is in addition, not instead, because CMDFAIL is an observer, not a veto.
Testing. Hand-fire GREET_PROG for the lean-in. Say anything for the murmur. Type gibberish for the laughter. Three doors, one voice, and when you want the register changed, you edit one block and test all three doors in thirty seconds.
Push it further. Pass richer arguments: mpcallfunc whisper arrives $N hands the routine a two-word message it can pull apart, naming the newcomer inside the whisper. Add occasions freely, a leaves case called from an EXIT_PROG, a midnight case called from a TIME_PROG, and the library's whole day funnels through the one voice.
Part Four: Quests And The Clock
Two exercises where scripts meet the quest system. The quest commands let a scripted mob hand out, advance, and settle real quests from the journal system, and QUEST_TIME_PROG narrates the countdown on quests built with a time limit. One honest note up front: quest ids are defined in a questmaster's task table by the quest building tools, not by scripts; scripts OPERATE quests that exist. On your practice dummy, a made-up id does nothing at all, politely, and the exercises below say so where it matters.
Exercise 14: The Harbor Questmaster
The brief. The harbor master's clerk recruits runners. Arrivals get the pitch; anyone who says the word job or errand is handed the harbor_run task on the spot. Separately, the dock gatekeeper along the route treats past runners with respect and everyone else with suspicion, so completing the quest must leave a mark a second mob can read.
Think it through. Which command files a quest into a player's journal, and who must the quest be defined on for it to work? How does a DIFFERENT mob, later, ask has this person completed that quest?
The solution, first script, on the clerk:
GREET_PROG 100
say The harbor master needs a runner, $N. Say the word job.
~
SPEECH_PROG job errand
say Take the harbor run, and do not dawdle on the docks.
mpstartquest $n harbor_run
~
The solution, second script, on the gatekeeper:
GREET_PROG 100
if questwinner($n harbor_run)
say The docks still speak well of your run, $N.
else
say Prove yourself on the harbor run first, friend.
endif
~
Why it works. mpstartquest $n harbor_run asks the quest system to file the task with id harbor_run, FROM THE SCRIPTED MOB AS GIVER, into the speaker's journal, exactly as if a coded questmaster had offered it: eligibility is checked, repeat rules are honored, and if the task carries a time limit the countdown of Exercise 15 starts ticking. The id must exist in the giver's task table; on your practice dummy it does not, so the line no-ops silently, which is the safe behavior you want from every quest command. The gatekeeper reads the mark with questwinner($n harbor_run), which answers whether that player has EVER completed that quest, a permanent fact the quest system records; the if picks the respectful or suspicious line accordingly, and any number of mobs across the world can read the same fact without coordinating.
The rest of the toolbox, for your notes, all following the same target-then-id shape. mpquestwin $n harbor_run awards completion, rewards and record and all, which is how a mob at the END of an errand settles it. mpendquest wraps a quest up however it can, completing if possible and otherwise dropping it from the journal, the tool for a storyline that must move on. mpstepquest nudges progress by firing a quest event, kill or visit or talk, for quests with such objectives. And for state of your OWN invention, mpqset writes a named value into the player's active quest entry and qvar() reads it back, letting a three-stage errand remember which stage without touching the quest definition.
Where it goes wrong. Testing on a mob with no task table and concluding the command is broken; nothing happened because nothing SHOULD happen, and the real test needs a real questmaster, which is a five-minute favor from any senior builder. Writing the keyword header job errand and then hand-firing SPEECH_PROG; the pretend message is the word test, which contains neither keyword, so say the word out loud instead. And confusing the two readers: questwinner() answers completion forever, qvar() reads working state on a currently ACTIVE quest; gate flavor on the first, track stages with the second.
Push it further. Make the clerk refuse repeat customers: guard the offer with questwinner($n harbor_run) and an early return, so veterans get a nod instead of a pitch. Then chain mobs: the clerk starts the quest, a warehouse mob's GIVE veto from Part One takes the parcel only from runners, and the harbor master's own script calls mpquestwin when spoken to. That chain is most of a working quest line, and every link is a pattern you now own.
Exercise 15: The Cellar Clock
The brief. The innkeeper's cellar_rats task carries a five-minute limit, set in the quest definition. While a player's clock runs, the innkeeper's voice should needle them at five minutes and one minute remaining, wherever in the world they are, and mark the deadline itself with a final line. Punctual players should never hear the doom bell.
Think it through. Which trigger pulses during a timed quest, and how often? What do its header's numbers mean, counting which way? And which echo command reaches a player who is NOT in the room?
The solution. Attach to the innkeeper:
GREET_PROG 100
say Five minutes for the cellar, $N, and the clock is honest.
~
QUEST_TIME_PROG cellar_rats 5 1
mpechoat $n The innkeeper's voice needles at you about the cellar clock.
~
QUEST_TIME_PROG cellar_rats 0
mpechoat $n Somewhere above you, the innkeeper stops waiting.
~
Why it works. When a player accepts a task that carries a time limit, the engine starts a silent countdown and, once per minute, fires QUEST_TIME_PROG on every scripted object in the world that defines it. The header is the quest id followed by the minute marks you care about, and the numbers are minutes REMAINING, counting down, so 5 1 means the five-minutes-left pulse and the one-minute-left pulse, and 0 is the deadline itself. A header of just the id fires on every pulse, which is usually too chatty. When a block runs, $n is the player on the clock and $g holds the quest id and the minutes remaining, separated by a space. The crucial delivery detail: this trigger is world-wide, the innkeeper does NOT need to be anywhere near the player, and mpechoat $n reaches the player wherever they stand, which is exactly what a voice needling at the back of the mind should do. mpecho here would instead play to the INNKEEPER'S room, the right choice only if you want him muttering to his regulars about tardy runners.
Where it goes wrong. Using mpecho and wondering why the player heard nothing; the player was three zones away, and the echo entertained an empty taproom. Expecting the hand test to fire these blocks; the pretend message is the word test, which is not a quest id and a minute, so the blocks correctly sit out, and the honest test is a short-limit practice task accepted on a spare character. Forgetting that pulses stop on their own the moment the quest completes or is dropped, which is a feature: no cleanup, no bell for the punctual. And writing the minutes UP, as elapsed time; they count DOWN, and a header of cellar_rats 4 fires at four minutes REMAINING of five, not four minutes in.
Push it further. Escalate the register: calm at five, clipped at one, cold at zero, three blocks, three moods. At zero, do more than narrate: the deadline block is a fine place for consequences, marking the player with mpsetvar so the innkeeper's next greeting remembers who ran late, or docking standing with mpfaction. The countdown itself and what failure formally MEANS stay in the quest definition; this trigger is the narration and consequence layer on top, and that division of labor is why it stays simple.
Part Five: Ears On The Whole World
Four exercises on the world-wide triggers, the family that fires on a scripted object no matter where the event happens: logins and logouts, level gains, channel chatter, and the turning of the calendar. One mechanical note covers them all: an object hears world-wide triggers once its script has been parsed, which happens when you attach or view the script and shortly after a scripted NPC loads, and only while the object is loaded at all. In practice: attach, view once, done. And one editorial note: a world-wide trigger fires for EVERY player's event, all day, every day. Restraint is the difference between a living world and a town crier nobody can silence.
Exercise 16: The Dockside Bell
The brief. The harbor keeps an old bell that superstitious sailors swear by: it swings, untouched, when anyone anywhere enters the world, and tolls low when anyone leaves it. Build the bell. Then, for the harbormaster's charity, a desk charm with two habits: a brief glow his clerks can see for every arriving soul, and a private word of guidance delivered only to brand-new souls, levels one through five, pointing them toward the training hall.
Think it through. Which triggers fire on logins and logouts, and what does their header hold? Where is the PLAYER at that moment, relative to the bell? And for the charity charm: can a world-wide trigger be filtered by who fired it?
The solution, first script, on the bell:
LOGIN_PROG
mpecho The dockside bell swings once, unasked, for $N.
~
LOGOFF_PROG
mpecho The dockside bell tolls low as $N leaves the world.
~
The solution, second script, on the charm:
LOGIN_PROG
mpecho A small charm on the harbormaster's desk glows briefly.
~
LOGIN_PROG -level 1-5
mpechoat $n A patient voice points you north, toward the training hall.
~
Why it works. LOGIN_PROG and LOGOFF_PROG fire on every loaded scripted object that defines them, wherever it stands, when any player enters or leaves the game. The header stays blank; $n is the player, who is almost never in your room, so the first script narrates to the BELL'S room with mpecho, and the harbor's regulars watch the bell keep its ledger of souls. The second script layers two blocks on one trigger. The first is a public tell-tale: a blank header, so it fires for every soul, and mpecho, so the glow plays to the charm's OWN room, a quiet sign to anyone watching the desk. The second is the charity: its header carries a zapper mask, because world-wide triggers accept them exactly like GREET does, filtering on the person who fired the event, and levels one through five means new souls only; and its body uses mpechoat $n, which reaches the player wherever THEY are, so the patient voice arrives at the far side of the login screen, not in an empty harbor office.
Where it goes wrong. The two echoes swapped: mpecho on the charity block plays helpful directions to nobody, and mpechoat on the bell whispers eerie bell noises to a player who cannot see any bell, which is less atmosphere than alarm. Forgetting the loaded rule: a scripted item in a room nobody has visited since the last reboot is not loaded and hears nothing, so put world-listeners somewhere alive, or on NPCs, which register themselves shortly after they load. And volume: a login line every login is charming from ONE bell and unbearable from nine; the world needs few world-listeners, chosen well.
Testing. Hand-fire LOGIN_PROG on the bell and it swings for you. On the charm, the glow fires for anyone, and the patient voice only if YOUR level sits in the mask's range, which for most builders it does not; that silence is the zapper working, not a bug, and the honest test is a level-one alt logging in.
Push it further. Give the bell a memory: count logins per mud day with the counter idiom and let the harbor gossip about busy tides. Or pair LOGOFF with a condolence: a friend NPC who notices a NAMED regular leaving, via a -name mask, and mutters into her cup.
Exercise 17: The Level Herald
The brief. Somewhere in the capital, a bronze gong sounds of its own accord whenever anyone in the world gains a level. And the heralds keep one private tradition: a personal word, delivered directly, to anyone reaching level twenty exactly, the level the order considers the true beginning.
Think it through. Which trigger fires on a level gain, and what extra fact rides along with it? Where does that fact live, and how do you compare it to a number?
The solution, first script:
LEVEL_PROG
mpecho A bronze gong sounds somewhere far away, honoring $N.
~
The solution, second script:
LEVEL_PROG
if $g == 20
mpechoat $n The herald's voice finds you at the twentieth step.
endif
~
Why it works. LEVEL_PROG is world-wide: any player, anywhere, gaining any level, fires it on every loaded scripted object that defines it. $n is the player, and the extra fact is $g, which holds the NEW level as a number, so the second script's bare comparison reads is the new level exactly twenty, and only then does the private line travel to the player, wherever they stand. The first script ignores $g entirely and just tolls; the second is nothing BUT the $g check. Between them they are the whole grammar of this trigger.
Where it goes wrong. Hand-testing the milestone script and declaring it dead: the pretend message is the word test, the comparison asks whether test equals twenty, it does not, and the block correctly stays quiet. The gong script hand-fires fine, because it never looks at $g; the milestone needs a real level gain, which a senior builder can grant a test character in seconds. The other classic is speaking $g without thinking: honoring $N on reaching level $g is lovely in live play and reads as level test under the hand test, which has startled more than one builder mid-demonstration; it is cosmetic, but know why it happens.
Push it further. Milestones plural: a switch on $g with cases at ten, twenty, fifty, each with its own line, one block for the order's whole liturgy. Or filter the audience instead of the level: a -class mask on the header makes a guild herald that honors only its own.
Exercise 18: The Gossip Collector
The brief. A tavern lurker cocks an ear at ALL channel chatter, any channel, and keeps a notebook he scribbles in only when the gossip channel specifically lights up. He must never, himself, speak on any channel, for reasons the exercise will make clear.
Think it through. Which trigger hears the chat channels, and what exactly arrives in $g? How do you listen to ONE channel rather than all of them? And why the vow of channel silence?
The solution. Attach to the lurker:
CHANNEL_PROG
emote tilts an ear toward chatter only he can hear.
~
CHANNEL_PROG gossip
emote scribbles a fresh line into a dog-eared notebook.
~
Why it works. CHANNEL_PROG is world-wide and fires on any traffic over the chat channels; $n is the speaker and $g holds the channel name followed by the message, as one line of text. The header's keywords are matched against that WHOLE line, which is the trick of the second block: the word gossip in the header matches the channel name at the front of $g, so the block fires for gossip-channel traffic and stays quiet otherwise. A blank header, as in the first block, hears everything. Two blocks, two radii: the ear tilts for all chatter, the notebook opens for one channel.
Where it goes wrong. The vow of silence, broken: give a mob both a CHANNEL_PROG and a body that SPEAKS on a channel with mpchannel, and his own broadcast fires his own trigger, which broadcasts, which fires, a feedback loop at world scale until the step budget cuts in. A channel listener never transmits on a channel it can hear; react in the room, as this one does, or write very carefully. Subtler: keyword headers match the whole line, so a header word that might appear in ordinary MESSAGE text, not just channel names, fires on those mentions too; the header word gossip also matches someone SAYING gossip on another channel. Distinctive channel names make clean filters; common words do not. And volume yet again: busy muds have busy channels, and an emote per line of chat is a lot of ear-tilting; in live use, thin it with a percent-style gate inside the body, rand(20) around the emote.
Testing. Hand-fire CHANNEL_PROG: the ear tilts, and the notebook stays shut, because the pretend message is the word test, which contains no gossip. Then speak a line over any real channel and watch both radii work.
Push it further. Quote the room: $g holds channel and message, so the lurker can mutter fragments of what he heard, carefully reworded, remembering Exercise 10's lesson about reactions that contain their own trigger words. Add a counter per channel and he becomes the tavern's rumor barometer.
Exercise 19: The Calendar Keeper
The brief. A boarding-house landlady lives by two clocks. On the first day of every mud month she announces that rents are due. And whenever any lodger anywhere crosses another full hour of played time, her hourglass turns itself over in sympathy. Build both habits.
Think it through. Which trigger marks the turn of the mud day, and what MUST its header contain? Which trigger marks an hour of a player's played time, and what must its header NOT contain? And one of these two events has no person attached; which, and why does it matter?
The solution. Attach to the landlady:
DAY_PROG 1
say The first day of the month. Rents are due, friends.
~
AGE_PROG
mpecho An hourglass turns itself, marking another hour of $N's road.
~
Why it works. DAY_PROG fires world-wide as each new mud day begins, on scripted objects whose header lists that day number; the header list is REQUIRED, a blank one never matches, and 1 means the first of the month. The event belongs to the calendar, not to any person, so there is no $n to speak of in a DAY block, and the landlady's line correctly mentions nobody. AGE_PROG is the opposite in both respects: its header stays BLANK, because a number there would be read as a percent chance, and it fires for a PERSON, any player crossing another full hour of total played time, with $n the player and $g the new age in hours. The hourglass line leans on $N, which a DAY block never could.
Where it goes wrong. The two headers, swapped: a day list on AGE_PROG becomes a percent and fires erratically; a blank DAY_PROG matches no day and never fires at all, silently, which is the worse bug because nothing LOOKS wrong. Reaching for $n in a DAY block: the calendar has no actor, and the code answers with a shrug of a name; write day lines about the world, not about someone. Mixing this up with TIME_PROG: TIME is the hour-of-day cousin from the triggers chapter, and it is MOB-ONLY, riding the mob's own heartbeat, so it needs the mob loaded and awake, while DAY is delivered world-wide by the calendar itself; dawn lantern-lighting is TIME, monthly rent is DAY. And the testing trap of the whole family: the hand test's pretend message is not a day number, so DAY blocks sit out the hand test entirely; AGE hand-fires happily, and the honest DAY test is patience or a friendly admin nudging the calendar.
Push it further. More day numbers in one header, 1 15, for rent day and a mid-month reminder with a shared body; or two DAY blocks with different lines. Gate an AGE line on the number: a bare comparison on $g singles out the hundredth hour for a small ceremony, the same shape as Exercise 17's twentieth step.
Part Six: The Capstone
One exercise, built the way real encounters are built: design first, then blocks, then tests. It uses the bus, a zapper, a routine with a return value, per-player memory, a summoned and re-skinned prop, paced narration, and cleanup. If you can build this from the brief alone, you have graduated from this workbook.
Exercise 20: The Sentinel's Trial
The brief. In a ruined hall stands the last sentinel, an ancient guardian who administers a trial of nerve. Arriving players are told to speak the words I am ready. When someone does, the hall goes quiet, a blade of pale light ignites, sweeps once through the candidate without leaving a mark, and the sentinel pronounces a verdict drawn from a single, editable verdict routine. The trial marks a candidate forever: the sentinel remembers who has passed, greets them differently, and refuses to judge them twice. Creatures are beneath the trial's notice. And the hall permits no actual violence: any attempt to start a fight is caught at the wrist. The light must be a real object while it exists and must be GONE afterward, leaving nothing on the floor.
Think it through, because this is the real exercise. List the moments: arrival, the spoken phrase, the ceremony, the verdict, the memory, the veto. Assign each a trigger before reading on. Then the props: what stands in for the blade, how is it dressed, and who cleans it up? Then the traps you have already met once each: the phrase header and the hand test; the memory check racing the memory write; the verdict living in one place; the veto that must narrate.
The solution. Attach to the sentinel:
GREET_PROG -player
mpecho The last sentinel stirs, stone grinding on stone.
if var($n passed_trial)
say The trial remembers you kindly, $N. Stand easy.
return
endif
say Speak the words I am ready, $N, and be judged.
~
SPEECH_PROG p i am ready
if var($n passed_trial)
say You have already been judged, $N. Rest.
return
endif
mpecho The sentinel raises one gauntlet, and the hall falls quiet.
mpmload /obj/torch
mpset $b short a blade of pale trial-light
mpecho A blade of pale trial-light ignites above the sentinel.
mpsleep 2
mpecho The blade sweeps once through $N without leaving a mark.
say $%callfunc(verdict $N)%
mpsetvar $n passed_trial yes
mppurge $b
mpecho The trial-light gutters out, its work done.
~
FUNCTION_PROG verdict
return The light finds no fear in you, $G. The trial is passed.
~
CNCLMSG_PROG ATTACK ALL
mpechoat $n The sentinel catches your wrist. The trial is not a brawl.
~
Why it works, block by block. The greeting carries a -player zapper, so creatures wandering the ruin are simply beneath notice, no if required. Its first line is the reliable opener, an always-runs line before any branching, the habit the earlier workbooks drilled: whatever else the greeting decides, the stone stirs. Then the memory check comes FIRST among the decisions: var($n passed_trial) reads the mark off the visitor themselves, and past candidates get the kindly line and an early return, so the invitation below only ever reaches the untried. Storing the mark on $n rather than on the sentinel is what makes the memory per-person and permanent; it rides in the player's own saved character, and a hundred candidates never share a flag.
The ceremony block is the heart. Its header is the phrase form, p i am ready, matching the whole phrase inside any spoken line, so both I am ready and master, I am ready to be judged open the trial. The same memory guard repeats at the top, because the greeting is not the only door to this block, and every door needs the same lock. Then theater, paced: mpmload clones a stock torch into the room and remembers it as $b, mpset $b short re-dresses the clone into a blade of pale trial-light before anyone reads its name, and the narration sells it. The mpsleep 2 holds the room for two real seconds, the pause every ritual needs; dollar codes keep their values across the pause, so $N and $b still mean the candidate and the blade afterward. The verdict line is the library lesson: the say splices $%callfunc(verdict $N)%, so the words spoken live in ONE routine that the head builder can reword without touching the ceremony. Then the bookkeeping: the mark is written, mppurge $b removes the light so nothing litters the hall, and the last echo closes the scene. Anything a script loads with mpmload is flagged to despawn on room reset anyway, so even a ceremony interrupted halfway leaves no permanent mess.
The verdict routine returns one sentence, personalized through $G, which carries whatever the caller passed, here the candidate's name. And the final block is Part One come home: an ATTACK veto with a narrated refusal, so the trial hall polices itself, catching players and aggressive creatures alike at the wrist.
Where it goes wrong. Every trap here is one you have already walked into once, which is the point of a capstone. The memory guard missing from the SPEECH block: a passed candidate says the words again and is judged twice, and the double doors are why guards repeat. The verdict inlined into the say: it works, and the next reword misses one copy, and the sentinel contradicts himself; one routine, one truth. The purge forgotten: torches dressed as light accumulate in the corner, one per candidate, an inventory of embarrassment. The veto body left empty: attacks in the hall die silently and players file bug reports. The sleep placed inside a loop, if you extend the ceremony: sleeping ends a loop, as the flow chapter warns, so pace multi-beat rituals as straight lines. And the hand test read wrong: firing GREET_PROG shows the stirring and the invitation, but the ceremony's phrase header cannot match the word test, so the trial itself is tested by SAYING the words, and the veto by swinging at the sentinel with a spare character, at which point your wrist is caught, which is the system working.
Testing, as a checklist, because shipping an encounter means walking it. View the script; the Triggers line must list GREET_PROG, SPEECH_PROG, FUNCTION_PROG, and CNCLMSG_PROG. Hand-fire the greeting; the stone stirs, and you are untried, so you get the invitation. Say I am ready; watch the full ceremony, the pause, the verdict with your name in it, and the light vanishing. Say it again; you are refused, kindly, which proves the mark. Walk out and back; the greeting now remembers you. Attack the sentinel; caught at the wrist. Five minutes, and every mechanism in the script has been seen working with your own eyes.
Push it further, and this is where the encounter becomes yours. Let the verdict JUDGE: the routine can read level($n) or a skill with skill() and return different sentences, even a refusal, and because the caller just speaks whatever comes back, the ceremony never changes. Mark failures differently from passes and let the greeting distinguish three states: untried, passed, refused. Give the trial a price with mpstartquest, sending candidates on an errand before the blade will light. Move the ATTACK veto's message into the verdict routine's style so even the refusals sound like one being. And when the whole thing sings, ask a coder to bake the script into the sentinel's file, so every copy of him, forever, is born knowing the trial.
Where You Stand Now
Count what these twenty exercises put in your hands. You can stop an action before it happens and make the refusal into content, with CNCLMSG_PROG aimed at GET, ENTER, ATTACK, CAST, or anything else on the bus, and you know the honest limits: cancel always cancels, errors count as permission, and narration is not optional. You can watch without interfering through EXECMSG_PROG, and you know which codes reach observers and why furniture can have eyes. You can aim ordinary triggers with zapper masks and combine filters the header cannot hold by moving them into the body. You can give a creature ears for raw sentences with IMASK and REGMASK, and you check every reaction line against its own pattern without thinking now. You can write a routine once with FUNCTION_PROG, call it two ways, pass arguments in through $G, and carry answers out through return. You can operate the quest system from a script and narrate its deadlines through QUEST_TIME_PROG. You can listen to the whole world with the LOGIN, LOGOFF, LEVEL, CHANNEL, DAY, and AGE families, and you know the two disciplines that tame them: the right echo for where the audience actually is, and restraint. And you have built one complete encounter from an empty mob, on a design you wrote before you typed a line.
What remains is vocabulary and craft. The reference chapter, help mudprog-reference, is the full card of every trigger, command, and function, worth a slow read now that every entry on it means something to you. The cookbook, help mudprog-cookbook, is a shelf of finished builds to steal from. And the craft comes from the loop you have run twenty times in this workbook: brief, attempt, script, test, refine. Go find a hall that needs a sentinel.
Every scripter, from the first-day beginner to the builder with a hundred NPCs behind them, has stood in a quiet room staring at a mob that refuses to do the thing the script plainly says it should do. This chapter is for that moment. It is a field guide to every way a MUDProg script goes wrong, what each failure looks like from inside the game, why the engine behaves that way, and exactly how to fix it.
You do not need to read this chapter start to finish: it is organized so you can come in from a symptom and leave with a fix. But read the first two parts once in full; they turn every later entry from a recipe into a diagnosis you could have made yourself.
One reassurance, repeated from the basics chapter because it is the foundation of fearless debugging: a broken script cannot crash the mob, the room, or the mud. The worst outcomes are a script that does nothing, does the wrong thing, or spams the room, and all three are cured with one mudprog <target> clear. Nothing you try while debugging can make anything worse than the bug you already have.
Part One: The Five Questions
Almost every script failure is a no to one of five questions, asked in order. When a script misbehaves, walk the list from the top and stop at the first no; that is where your bug lives.
Question one: did the script actually save? Type mudprog <target> and look. Old text, or none, means the edit never landed: @abort, a missing final period, or the wrong object.
Question two: did it parse the way you meant? The same view prints a Triggers line listing every trigger the engine found. A trigger missing from that list means the engine never saw its header, almost always a lost tilde; a trigger you did NOT write, such as SAY_PROG, means a body line was read as a header.
Question three: does the event actually fire on this object? Greet scripts belong on mobs and rooms, heartbeat scripts on mobs only, item scripts on items, and a few triggers are not yet wired to live play at all. Part Four is the map.
Question four: did the header let the event through? A percent that rolled against you, a keyword never said, a zapper mask that excludes, an hour list without now: the block is healthy, the gate is closed. Part Five covers every gate.
Question five: did the body do what you meant? Only now is the problem in the lines themselves: a code reading the wrong person, a condition always true, a loop that never runs, a sleep that dropped the scene. Parts Six through Ten cover the body.
The order matters. Beginners leap to question five and rewrite a healthy body twenty times while the real bug is a missing tilde back at question two. Walk the list and most bugs fall in under a minute.
Part Two: The Debugging Toolkit
You have six tools. Learn all six now, in calm weather, so they are familiar when a script is misbehaving and you are annoyed.
Tool one is the view. mudprog <target> prints the script exactly as stored, plus the Triggers line. It answers questions one and two on the spot, and it is always the first thing to type: the Triggers line is the engine telling you, in writing, what it believes your script contains.
Tool two is the test fire. mudprog <target> test <TRIGGER> fires the named trigger immediately, with you standing in as source and target and the single word test as the message rider; if no block by that name exists you are told so. Three limits, each a famous false bug report: the rider is the word test, so keyword headers will not match under test; a percent header still rolls its dice; and test will happily fire a misspelled trigger name, so a passing test proves the BODY runs, not that the name is right. Entry 4 shows that trap in full.
Tool three is the tracer line. When you cannot tell which lines of a body are running, make the script tell you: salt it with mpecho lines that print numbered markers, run it once, and see which numbers appear. It is the single most useful habit in scripting:
GREET_PROG 100
mpecho DEBUG one, the block started.
if ispc($n)
mpecho DEBUG two, the visitor is a player.
say Welcome in, $N.
else
mpecho DEBUG three, the visitor is not a player.
endif
mpecho DEBUG four, the block finished.
~
Fire it and read the numbers. One, three, four when a player walked in tells you ispc($n) answered no, so $n is not who you think. The numbers turn guesses into facts. When the bug is found, take the tracers out; nothing is sadder than a shipped mob that mutters DEBUG two at customers.
Tool four is the breadcrumb panel. Tracer lines show you the moment; variables let you inspect state after the fact. Attach a temporary LOOK_PROG that reads out the variables you care about, and the mob becomes its own status display every time you look at it:
LOOK_PROG 100
mpechoat $n Debug: visits is $<$i visits> and mood is $<$i mood>.
~
Look at the mob after each experiment and watch the values move. The angle form $<$i visits> is the text-reading form of a stored variable; an unset one reads as empty, which is itself information. Remove the panel when done, or leave it: only someone who looks sees it.
Tool five is the log trail. mplog <text> appends a stamped line to /log/mudprog and shows nothing in game, which makes it the tool for bugs that happen when you are not watching. Its cousin mpgset writes a value into the script engine itself, surviving death, resets, and reboots; no dollar code reads a global back yet, so treat mpgset breadcrumbs as messages to the staff side, readable by an admin, not as script memory. A mob wired for forensics:
GREET_PROG 100
mplog greet fired, source was $N
mpgset last_greet $N
say The ledger records your visit, $N.
~
Come back in the morning, read the log, and you know every time the block ran and for whom, even though the room was empty of builders all night.
Tool six is the scratchpad. The admin command scripttest runs raw script lines on yourself without attaching anything, semicolons standing in for line breaks: the fastest answer to what does this one line actually do. A raw body has no header and no tilde; it just runs:
mpecho The harness line runs, and the source is $N.
Typed as one scripttest line, that prints to your room with your own name substituted, because under scripttest you are host and source both. The form scripttest runfile <path> reads the body from a file, and scripttest fire <TRIGGER> on <name> fires a real trigger on a scripted mob in your room. The runfile form matters: some telnet setups quietly eat or double typed dollar signs, so anything heavy with dollar codes should be tested from a file or through the editor. Entry 26 covers the symptoms.
That is the whole kit: view, test, tracer, panel, log, scratchpad. Every entry that follows uses some mix of them, and the closing checklist at the end of the chapter strings them into a single routine.
Part Three: Structure Mistakes
These failures happen before a single body line runs: the script did not divide into the blocks you intended. They are the commonest first-month mistakes, and the Triggers line diagnoses every one.
Entry 1: The Missing Tilde Between Blocks
You wrote two blocks but only one tilde:
GREET_PROG 100
say Hello there, traveler!
SPEECH_PROG hello
say Hello to you too!
~
What you see in game: a player walks in and the mob says BOTH greetings back to back. Saying hello to the mob does nothing at all.
Why it happens: the tilde is the only thing that ends a block. Without one after the first say, everything down to the final tilde is ONE GREET_PROG whose body is three lines. The line SPEECH_PROG hello is just a body line; when the block runs it is handed to the mob as an ordinary game command and fails quietly. No speech trigger was ever registered.
The five-second diagnosis: mudprog <target> says Triggers: GREET_PROG only. Whenever a trigger you wrote is missing from that list, hunt for a missing tilde directly above where it should have been.
The fix is one character:
GREET_PROG 100
say Hello there, traveler!
~
SPEECH_PROG hello
say Hello to you too!
~
Now the Triggers line lists both, the greeting is one line again, and saying hello gets an answer.
Entry 2: The Missing Final Tilde
The mirror image of Entry 1, and sneakier. The engine forgives a script whose LAST block is missing its tilde: the trailing block still registers and fires. So you write a greeting, forget the final tilde, test it, and it works. Days later you append an idle block with mudprog <target> append, and the stored script becomes this:
GREET_PROG 100
say Welcome to my humble stall, friend.
RAND_PROG 15
emote rearranges the fruit into a neater pyramid.
~
What you see in game: the mob greets arrivals with the welcome AND immediately rearranges the fruit, every time, and never fidgets on its own at all.
Why it happens: append glues new lines onto whatever is stored. Your first block had no closing tilde, so the appended header and emote landed INSIDE it, exactly as in Entry 1: the RAND_PROG 15 line fails silently as a command, and the emote runs with every greeting.
The fix is to end every block with its tilde, always, even the last one, precisely so that later appends land outside it:
GREET_PROG 100
say Welcome to my humble stall, friend.
~
RAND_PROG 15
emote rearranges the fruit into a neater pyramid.
~
Make the habit unconditional: a tilde after every block, even the last, no exceptions, no it works anyway.
Entry 3: No Header At All
You typed the body and forgot the trigger line entirely:
say Good morning to one and all.
~
What you see in game: nothing, ever. And the diagnosis is delightfully strange: mudprog <target> prints a Triggers line reading SAY_PROG.
Why it happens: the engine treats the first line of every block as the header, no matter what it says. Your say line became a header for a trigger named SAY, with an empty body, and no game event is named SAY. The engine cannot guess you meant a body line.
The fix is to give the block its WHEN:
GREET_PROG 100
say Good morning to one and all.
~
The rule: the first line after a tilde, or of the script, is ALWAYS read as a header. When the Triggers line shows a name suspiciously like one of your commands, a header went missing above it.
Entry 4: The Imaginary Trigger Name
You misremember GREET_PROG as GREETING_PROG:
GREETING_PROG 100
say Welcome, welcome, thrice welcome!
~
What you see in game: total silence, forever, no matter how many times players walk in.
Why it happens: the engine accepts ANY word as a trigger name and files the block faithfully under it; builders may write blocks for triggers that arrive in future updates, so there is no approved list to check. But no event today is called GREETING, so nothing fires it.
The trap inside the trap: mudprog <target> test GREETING_PROG runs the block perfectly, because test fires whatever name you hand it. A passing test proves the body runs; it says nothing about the name. When a block tests fine but never fires on its own, compare its header letter by letter against the triggers chapter.
The fix:
GREET_PROG 100
say Welcome, welcome, thrice welcome!
~
Walk out, walk in, and the mob finally has manners. Most misremembered: GREET_PROG (not GREETING), SPEECH_PROG (not SPEAK_PROG, a real but different trigger that fires on the SPEAKER), FIGHT_PROG (not COMBAT), RAND_PROG (not RANDOM), DEATH_PROG (not DIE).
Entry 5: Near-Miss Spellings Of Real Triggers
A cousin of Entry 4 whose misspelling LOOKS so right. The health trigger is HITPRCNT_PROG, one word, no vowels in the second half. Write it the way English wants and the block is dead:
HIT_PRCNT_PROG 50
say You think me beaten? I am just getting started!
~
What you see in game: the mob fights to the death without its line, while mudprog <target> test HIT_PRCNT_PROG fires it happily, hiding the typo behind a passing test.
The fix:
HITPRCNT_PROG 50
say You think me beaten? I am just getting started!
~
In the same family: the _PROG suffix is optional, so GREET and GREET_PROG are the same header; case never matters; but every character before the suffix must be exact. The Triggers line shows the name the engine actually registered, ready to compare against the triggers chapter with your own eyes.
Entry 6: The Vanishing Line That Started With A Star
Lines whose first character is # or * are comments the engine skips. A gift, until you try to stage a starred direction:
GREET_PROG 100
say Watch closely, friend!
* drumroll *
say Ta-daa! The coin was in my sleeve all along.
~
What you see in game: the two say lines run and the drumroll never happens, unmarked; to the engine that line was a comment.
The fix is to say what you mean with a real command:
GREET_PROG 100
say Watch closely, friend!
emote provides his own drumroll on the bar top.
say Ta-daa! The coin was in my sleeve all along.
~
Keep # and * for actual notes at line start; they are line-long, and a * mid-line is just a star.
Entry 7: The Edit That Never Saved
Three ways a good script never makes it onto the mob at all.
First, the editor ends with a period, not a tilde. The tilde ends a BLOCK; the period alone on a line ends the SESSION and saves. Walk away without the period and the editor is still open, and your next game commands are typed INTO the buffer. @abort throws the session away on purpose, keeping the old script.
Second, the inline form needs its semicolons. In mudprog <target> set <text> every semicolon becomes a line break. Without them the whole text arrives as ONE line, which by Entry 3's rule is read as a single header: right trigger listed, empty body, tilde uselessly inside the header. If a set-built script lists its trigger but does nothing, count semicolons: one after the header, one after every body line, one before the tilde.
Third, the script went onto the wrong object: the first word after mudprog is always the target, and in a crowded room the name guard may match a different guard. View the script on the mob you think you edited; if it is missing, ask who else answers to the name.
Part Four: Right Script, Wrong Object
The script is well formed, the Triggers line perfect, but the event never visits the object carrying the script. Placement bugs share one signature: the block fires under mudprog <target> test and never fires live. Seeing that, stop editing the body and ask where the script LIVES.
Entry 8: A Greeting On An Item
You script a magic lantern to react to arrivals, and attach this to the lantern itself:
GREET_PROG 100
mpecho The lantern flares in welcome as someone enters.
~
What you see in game: mudprog lantern test GREET_PROG prints the flare beautifully. A player walking in produces nothing.
Why it happens: arrival events go to the room and the LIVING witnesses in it, never to items lying there, so the lantern cannot hear one; test bypasses that delivery and pokes the block directly, which is why testing lies to you here.
The fix is a host the event actually visits. A room greets exactly like a mob, so scenery reactions belong on the room, attached with mudprog here set or the editor on here:
GREET_PROG 100
mpecho The lantern on its hook flares in welcome as someone enters.
~
Same effect, hosted where arrivals are delivered. The rule: mobs and rooms hear arrivals, speech, and combat; items hear what is done TO them, get, drop, give, wear, put, consume, plus the bus observers. When an item must react to the wider world, move the script to the room or use EXECMSG_PROG, the one way items witness events happening to other things.
Entry 9: A Heartbeat Script On A Room
You want ambient dripping in a cave, so you attach this to the room:
RAND_PROG 100
mpecho Somewhere in the dark, water drips off cold stone.
~
What you see in game: test fires it; live play never does, however long you stand there.
Why it happens: RAND_PROG rolls on the mob heartbeat, and only living things have heartbeats. Rooms and items do not tick, so any pulse trigger attached to one, RAND, TIME, DELAY, sleeps forever. This is the number one broken room script report, and nothing is broken: the trigger is mob-only.
The fix is to give the heartbeat to something that has one: any resident mob, however unobtrusive, becomes the drummer:
RAND_PROG 8
mpecho Somewhere in the dark, water drips off cold stone.
~
Attached to a mob, the mpecho narrates with no name in front, so the drip reads as scenery even though a creature is the metronome. Note the sane percent: 100 on a live RAND_PROG fires every couple of seconds. Rooms DO hear arrival, speech, and item events; only the self-driven pulses are missing.
Entry 10: WEAR On The Mob, WEARING On The Item
The trigger pairs GET and GETTING, DROP and DROPPING, WEAR and WEARING, PUT and PUTTING, GIVE and GIVING read as interchangeable English, and are not: the plain form fires on the THING and its witnesses, the ING form on the LIVING who acted. Cross them and nothing fires. Here a builder wants a knight to comment whenever the knight dons anything:
WEAR_PROG 100
emote settles the piece into place with ritual care.
~
What you see in game: attached to the knight, nothing happens when the knight wears armor. WEAR_PROG belongs to the garment; the knight's own dressing arrives at him under a different name.
The fix is the ING form, which is the actor's side of the same moment:
WEARING_PROG 100
emote settles the piece into place with ritual care.
~
The memory hook: the ING word describes what the LIVING is doing, so the ING trigger goes on the living; the plain word is what happens to the item, so the plain trigger goes on the item, or on a room or witness mob for fan-out triggers like GET and DROP. When an item-flavored trigger stays silent, check which side of the event your host stands on.
Entry 11: ENTRY_PROG On A Mob, Waiting For Customers
ENTRY_PROG on a ROOM fires when a player enters, so it is natural to assume it does the same on a MOB. It does not: on a mob, ENTRY and ARRIVE are reserved for the mob ITSELF stepping into a room, EXIT for leaving. A shopkeeper carrying this waits forever:
ENTRY_PROG 100
say Come in, come in, everything is half off today!
~
What you see in game: silence at every arrival, though the block fires under test. Worse, the mob-side movement hooks are not yet called by live movement at all, so even a wandering mob will not fire its own ENTRY today; those blocks are written for the future.
The fix, for customers, is the trigger that has always meant a player just walked in on me:
GREET_PROG 100
say Come in, come in, everything is half off today!
~
Keep ENTRY_PROG for rooms and thresholds, GREET_PROG for hosts, and treat mob-side ENTRY, ARRIVE, and EXIT as reserved seats for a show that has not opened yet.
Entry 12: The Triggers That Only Fire Under Test
A few triggers parse cleanly, list in the Triggers line, and fire on demand under test, but the live game does not yet send them their event: today DAMAGE_PROG, BRIBE_PROG, SOCIAL_PROG, and the mob-side movement trio of Entry 11. A builder who scripts a flinch this way waits forever:
DAMAGE_PROG 100
emote staggers, clutching the fresh wound.
~
What you see in game: perfect behavior under mudprog <target> test DAMAGE_PROG, and a mob that never flinches in a real fight.
The fix is the wired combat triggers, which cover nearly every case between them: FIGHT_PROG fires every round, HITPRCNT_PROG by health threshold, so a flinch as the mob gets hurt is:
HITPRCNT_PROG 75
emote staggers, clutching a fresh wound.
~
For reacting to emotes today, REGMASK_PROG watches the raw text the mob sees; the triggers chapter shows the pattern. When a correctly spelled trigger tests fine and never fires live, check its entry in the triggers chapter for the phrase not yet called: that phrase is this entry.
Entry 13: ONCE_PROG Seems Dead
ONCE_PROG runs a single time, about a second after the mob loads. Attach a script to a mob ALREADY standing in front of you and you may see it fire once shortly after attaching, or not at all, and never again while that copy lives. Builders then report the trigger broken.
Nothing is broken; the moment it announces has passed. Watch it while developing with mudprog <target> test ONCE_PROG, or cause a FRESH copy to load, since every fresh copy runs its ONCE_PROG once, which is exactly what setup theater wants:
ONCE_PROG
emote cracks his knuckles and unlocks the shutters for the day.
~
ONCE_PROG takes no argument, has no source, and should never use $n. For once per PLAYER rather than once per mob, use a GREET_PROG with a per-player variable, Entry 31.
Entry 14: The Script That Vanished Overnight
Yesterday the innkeeper had a full script; today he stands mute, and mudprog innkeeper says no script at all. Nobody cleared it.
Why it happens: a script attached with mudprog lives on that one COPY of the mob. Destroyed, killed and respawned, or swept by a reset, the replacement is a brand new object built from the NPC's file, born knowing nothing of your script. Scripted items go the same way when sold, lost, or rotted.
The fix has two halves. While developing, simply re-attach. When a script is FINISHED it must be baked into the NPC's file so every future copy is born with it; /domains/examples/npc/mudprog_greeter.c shows the pattern, and any coder can move your tested text in a minute. And keep a copy of any long script in a file of your own as you work: the mob can vanish between sessions, so your file is the master and the mob is just the stage.
Entry 15: World-Wide Triggers After A Reboot
LOGIN_PROG, LOGOFF_PROG, LEVEL_PROG, CHANNEL_PROG, DAY_PROG, AGE_PROG, and QUEST_TIME_PROG fire on scripted objects anywhere in the world, but only on objects whose script the engine has PARSED: automatically when you attach or view it, and shortly after a scripted NPC loads. So after a reboot, a mob in an area nobody has visited is not loaded and hears nothing.
Rarely a bug, mostly a fact: if the town-crier announces nothing after a reboot, walk into his room once, or view his script, and he is back on the list. Scripts baked into NPC files re-register whenever the NPC loads, one more reason finished scripts belong in files.
Part Five: The Header Gate
The block exists, the object is right, the event arrives, and the header turns it away at the door. Gate bugs are subtle because the script is genuinely healthy; the guest list is wrong. They are also where test misleads most, because test brings its own strange guest: a message rider that is always the single word test.
Entry 16: The Keyword With Punctuation Glued On
You want a mob to answer greetings, and you write the keyword the way you would write dialogue:
SPEECH_PROG hello!
say Well met indeed! A fine day for conversation.
~
What you see in game: hello gets silence, while a player who types hello! with the mark gets an answer, which makes the bug look haunted.
Why it happens: keywords match as substrings of the spoken line. Your keyword is hello plus an exclamation mark, and hello there contains no exclamation mark, so the match fails. The header is not dialogue; it is a list of bare words separated by spaces.
The fix:
SPEECH_PROG hello
say Well met indeed! A fine day for conversation.
~
Keep header keywords plain and unpunctuated; punctuate in the body all you like. Commas bite the same way: gold, silver, gems is three keywords, two wearing commas that must also appear in the speech. Write gold silver gems.
Entry 17: The Keyword Hiding Inside Bigger Words
Keyword matching is by substring, usually a kindness: treasure happily matches treasures. Short keywords turn it into a trap:
SPEECH_PROG art
say Ah, a fellow connoisseur of the fine arts!
~
What you see in game: the mob interrupts anyone who says start, party, heart, quarter, or departure, beaming about fine arts, and the room concludes he is unwell.
Why it happens: art is a substring of all of those, and a keyword appearing ANYWHERE in the line fires the block; the plain keyword form has no notion of word boundaries.
Two fixes. The narrow one is the phrase form, whose leading p makes the rest one exact substring including its spaces, so surrounding spaces become word boundaries:
SPEECH_PROG p the art
say Ah, a fellow connoisseur of the fine arts!
~
The broad fix, usually better: choose keywords long enough to be unambiguous, painting sculpture gallery. Keywords under five letters need a hard look before they ship.
Entry 18: The Phrase Marker That Was Not A Phrase Marker
The phrase form has two strict requirements: the marker is the letter p, lower case, FIRST in the header. Miss either and you get not a phrase but a one-letter keyword, which by Entry 17 matches almost anything a human can say:
SPEECH_PROG P open sesame
say The old password! The vault remembers you.
~
What you see in game: the mob announces the vault to nearly anyone saying nearly anything. The header failed to parse as a phrase and became the three keywords P, open, and sesame; the one-letter keyword p matches any speech containing that letter, which is most speech, and open fires on its own besides.
The fix is the exact lower-case form:
SPEECH_PROG p open sesame
say The old password! The vault remembers you.
~
Now only speech containing open sesame together, in order, fires it. For OR between phrases write two blocks, one per phrase; the phrase form consumes the whole rest of the header.
Entry 19: Keywords On A Trigger That Carries No Text
Keyword headers filter the event's message text, and some events have none to filter. The classic is a toll collector meant to accept only coins:
GIVE_PROG gold
say Coin of the realm! You may pass.
~
What you see in game: hand the mob a rock and he thanks you for coin of the realm. Live gift events carry no text rider, so the gate stands open for every gift. Under test the rider is the word test, gold does not appear in it, and the block will not fire at all: over-firing live, under-firing in test, the most confusing combination in this chapter.
The fix is to filter on the ITEM, not the header. The gift rides in $o, and the functions chapter's identity checks read it:
GIVE_PROG 100
if isname($o gold)
say Coin of the realm! You may pass.
else
say I have no use for $o, $N. Coin or keep walking.
endif
~
The rule: keyword headers belong on triggers whose entries say text rides in $g, speech, channels, casting, masks. Everywhere else, gate with a condition in the body.
Entry 20: The Percent That Is Working Exactly As Written
The most reported non-bug in MUDProg:
RAND_PROG 5
emote hums an old marching tune.
~
What you see in game: you attach it, stare for thirty seconds, see nothing, and report the trigger broken.
Why it happens: 5 means five percent per heartbeat, and a heartbeat is a couple of seconds: the hum averages once a minute, and three silent minutes are entirely possible. The dice are just dice. The same misunderstanding runs the other way: testing a GREET_PROG 25 four times and seeing two greetings is exactly what 25 says.
The debugging habit: while iterating, set the percent to 100 so every test fires, and restore the real number as the LAST edit before walking away. At 100 you test the body; at the real number, the mood. Forgetting the restore ships a mob that hums every two seconds, which players report with far more energy than silence.
Entry 21: The Header That Says Never
A percent of zero, or any negative number, means never, even under test:
RAND_PROG 0
emote polishes the counter to a mirror shine.
~
What you see in game: nothing, ever, including from test, which faithfully rolls the zero and loses. It arrives by way of hurried arithmetic, a decimal like 0.5 read as 0, or a placeholder never filled in.
The fix is any number from 1 up, or the word all, or a blank argument, both meaning always:
RAND_PROG 12
emote polishes the counter to a mirror shine.
~
There are no fractional percents: 0.5 is zero and 2.7 is 2. One percent per heartbeat is already roughly twice a minute, the quietest a RAND_PROG gets; anything rarer wants a TIME_PROG or a spacing variable.
Entry 22: HITPRCNT Is A Threshold, Not A Chance
Two misreadings of the same header. First, the number is not a chance to fire; it is a HEALTH threshold, and the block fires on every combat round where the mob's health percent is at or below it. Second, because it fires every qualifying round, a dramatic one-off becomes a chant:
HITPRCNT_PROG 50
say You are better than you look! But not good enough!
~
What you see in game: silence for the whole top half of the fight, surprising builders expecting a fifty-fifty roll, then the same taunt every two seconds for the entire bottom half, surprising everyone else.
Why it happens: the engine compares health against the header each round; at or below, it passes, every round, with no built-in memory of having spoken.
The fix for the one-off is a variable guard, the standard once-per-fight latch:
HITPRCNT_PROG 50
if var($i halfway) == 1
return
endif
mpsetvar $i halfway 1
say You are better than you look! But not good enough!
~
The first qualifying round sets the latch and speaks; later rounds hit the return. A boss with phases at 75, 50, and 25 uses one block and one latch per phase. Under test, the rider is not a number and counts as zero health, so a HITPRCNT block always fires under test; do not let that convince you the threshold is ignored live.
Entry 23: The Zapper Mask That Blocks Everyone
Zapper masks restrict who may fire the trigger, and a mask with a wrong value restricts it to nobody, silently:
GREET_PROG -class fighter
say The guild recognizes a fellow blade. Enter freely.
~
What you see in game: no one ever gets the welcome, including the warriors it was written for.
Why it happens: the mask checks the arrival's class against the listed values, and this mud's class is named warrior, not fighter. No player's class is fighter, so the clause fails for everyone, and a failed clause means no fire. The engine cannot warn about unknown VALUES; it cannot know a value is not simply rare.
The diagnosis, worth learning for every zapper problem: make the mob say the exact value the mask judges. The percent form prints a function's answer straight into speech:
GREET_PROG 100
say My records list you as a $%class($n)% of level $%level($n)%.
~
Walk in and the mob reads your class and level back, spelled exactly as the engine sees them; whatever it says is what the mask must list. Then restore the mask with the true value:
GREET_PROG -class warrior paladin
say The guild recognizes a fellow blade. Enter freely.
~
The same wrong-value failure happens with -race, -deity, and -name. When a masked block goes silent for everyone, probe first, mask second. And recall the asymmetry from the triggers chapter: an unrecognized clause TYPE passes leniently while an unmatched VALUE blocks, so a misspelled clause lets everyone through and a misspelled value lets no one through.
Entry 24: The Zapper That Contradicts Itself
Every clause in a mask must pass. List two no visitor can satisfy at once and the mask is a wall:
GREET_PROG -player -npc
say Ah, a visitor of some description!
~
What you see in game: silence for everyone, because nothing is both a player and not a player. The builder meant players or mobs, but a mask is an AND of its clauses, always; there is no or between clauses.
The fix, when you want different reactions anyway, is one block per audience, since a script may hold many blocks for one trigger:
GREET_PROG -player
say Flesh and blood, and welcome for it.
~
GREET_PROG -npc
emote eyes the creature warily and says nothing.
~
When you truly want everyone, drop the mask; a percent header already accepts all comers. Within a SINGLE clause, values are an or, so -race elf dwarf passes either; only between clauses is the and fixed.
Entry 25: The Clock Trigger With No Hours Listed
TIME_PROG and DAY_PROG are the two triggers whose header is REQUIRED, and a blank one does not mean always; it means never:
TIME_PROG
emote lights a lantern against the changing light.
~
What you see in game: nothing at any hour, and nothing under test either, a strong hint the gate itself is closed.
Why it happens: the header is the list of hours to fire on, 0 through 23, and an empty list matches no hour. Every OTHER trigger treats a blank argument as always, which is exactly why this exception bites.
The fix is to list the hours you mean:
TIME_PROG 0 6 20
emote lights a lantern against the changing light.
~
That fires as the clock strikes midnight, six, and twenty. Reminders: the mob checks the clock on its own heartbeat, so TIME_PROG is mob-only (Entry 9), and a mob in an unloaded area notices no hours (Entry 15). Under test the rider counts as hour zero, so a TIME_PROG fires under test only when its list includes 0, as above; design that into your lists while developing.
Part Six: Dollar Codes And Variables
The gate opened, the body ran, and the words came out wrong: codes printed literally, names missing, memory that never sticks. The variables chapter is the reference; this part is the casualty ward.
Entry 26: The Dollar Signs That Never Arrived
You type a script at the prompt, and in game the mob says Welcome, n! or Welcome, ! or even Welcome, $N! with the code sitting there raw. Before blaming the script, know that some telnet setups and client macros quietly eat, double, or mangle dollar signs on the way to the mud: the engine received a different script than you typed.
The diagnosis: view the stored text with mudprog <target>. If it shows N! where you typed $N!, your client ate the dollar on the way in. If the stored text is right and the output still wrong, then and only then is it a script problem.
The fix is to route dollar-heavy text through a path that cannot reinterpret it: the mudprog editor is safe, and for raw testing scripttest runfile <path> is the gold standard, because a file cannot be mangled by your keyboard settings. A one-line file containing this, run through runfile, proves your dollar codes end to end:
mpecho The harness reports that the source here is $N.
If runfile substitutes your name and inline typing does not, the engine is innocent and your client is guilty. Do script entry through the editor, and keep inline typing for short dollar-free experiments.
Entry 27: The Code That Prints Itself
A dollar code the engine does not recognize is left in the text as typed, which is how you discover you invented one:
GREET_PROG 100
say A pleasure to see you again, $z.
~
What you see in game: the mob says A pleasure to see you again, $z. There is no code z; you meant $n. The raw code in the output is a kindness: silently deleting it would leave a hole you might not notice.
The fix:
GREET_PROG 100
say A pleasure to see you again, $N.
~
Related traps: codes are CASE SENSITIVE, so $g and $G, $i and $I are different codes, and while $n and $N read the same on this mud, never assume a pair does until the variables chapter says so. And a real dollar sign must be written $$, or it eats the character after it.
Entry 28: Writing var() In Text
The function form var() answers questions in if lines. It does NOT work inside spoken text, and the failure is public:
GREET_PROG 100
say Your tab stands at var($n tab) gold.
~
What you see in game: the mob says, word for word, Your tab stands at var($n tab) gold.
Why it happens: text substitution and condition evaluation are different machines. In text the engine replaces dollar forms and leaves everything else alone, so var($n tab) is just letters; in an if line the same letters are a function call. Each syntax belongs to its own place.
The fix is the angle form for a stored variable in text, or the percent form for a function's answer in text:
GREET_PROG 100
mpsetvar $n tab 12
say Your tab stands at $<$n tab> gold, $N.
~
That stores 12 on the player, then reads it back into the sentence. Text reads with dollars; conditions ask with functions.
Entry 29: mpsetvar Without Its Object
mpsetvar takes THREE parts: the object to write on, the name, the value. Drop the object and the name slides into its place:
GREET_PROG 100
mpsetvar tab 12
say The ledger now reads $<$i tab> for you.
~
What you see in game: the mob says The ledger now reads for you, a gap where the number should be, every time.
Why it happens: the engine read tab as the OBJECT, searched the room for something called tab, found nothing, and quietly did nothing, the standard answer to a bad argument. Nothing was stored, so the angle form reads back empty.
The fix is to name the object, which for the mob's own memory is $i:
GREET_PROG 100
mpsetvar $i tab 12
say The ledger now reads $<$i tab> for you.
~
The habit: read every mpsetvar aloud as ON whom, CALLED what, SET to what; if the sentence does not parse, neither will the line. The same order rules the angle form: $<$i tab> is ON $i, CALLED tab, and $<tab> alone asks to read a variable off an object named tab, which is almost never what you meant.
Entry 30: Testing An Unset Variable Against Zero
The most treacherous line in variable scripting. A variable never set reads back EMPTY, and empty is not the number zero, so the natural first-visit test fails:
GREET_PROG 100
if var($i visits) == 0
say A brand new face! Welcome!
else
say Back again, are we?
endif
~
What you see in game: the very first visitor is greeted with Back again, are we? Equality against 0 compares emptiness with the digit zero, they do not match, and the else runs. The mob is haunted by customers it never met.
The fix is to test emptiness as the engine understands it: a bare var() in an if is TRUE when the variable holds something, FALSE when empty or zero, and the exclamation mark flips it:
GREET_PROG 100
if !var($i doorcount)
say A brand new face! Welcome!
mpsetvar $i doorcount 1
else
say Back again, are we?
endif
~
Now the unset variable answers false, the flip makes it true, the new face gets the welcome, and the latch is set. Equally sound: give the variable a definite start in an ONCE_PROG, after which == 0 comparisons behave because it is never unset again. Choose one convention per mob and stay with it.
Entry 31: One Memory Shared By Every Visitor
The latch above has a subtler cousin. Store the met flag on the MOB and there is exactly one flag for the whole world. The first visitor of the day is welcomed as new; every other player forever after is a returning regular on their very first visit, because the mob's single flag was already set by someone else.
Why it happens: variables live on an object, and $i is the mob: one object, one pocket of notes, shared by all comers. Per-player memory must live on the PLAYER, where each character carries their own copy in their save, outliving reboots and a hundred respawns of the mob.
The fix is to move the note to $n and give it a name no other script will trip over:
GREET_PROG 100
if !var($n bram_met)
mpsetvar $n bram_met 1
say A new face! The first bowl of stew is on the house.
else
say Good to see you back at my counter, $N.
endif
~
Each player trips the new-face branch exactly once in their life, which is what remember me means. The prefix matters: all scripts share each object's pocket of notes, so bare met on a player collides with the next builder's bare met. Prefix with the mob's name, always.
Entry 32: Reading $b Before Loading Anything
The code $b names the last thing THIS run created with a load command. Read it before any load and it is empty:
GREET_PROG 100
mpecho Behold what the merchant produces: $b!
mpoloadroom /obj/meal
~
What you see in game: Behold what the merchant produces: ! with an empty gap, then a meal quietly appears: the narration ran before the creation.
The fix is to load first, speak second:
GREET_PROG 100
mpoloadroom /obj/meal
mpecho Out from under the counter comes $b, still steaming.
~
Now $b names the meal just created. Two boundaries: $b is per-run, so yesterday's load does not linger into today's block, and each new load overwrites it, so narrate each creation before making the next.
Entry 33: Slots That Forgot, Angles That Grabbed
Three small substitution bites, none big enough for their own entry.
The numbered slots $0 through $9 are wiped at the start of every run. Storing a value in $3 during a GREET and reading $3 during a later SPEECH reads empty air. Slots are a workbench for one run; anything that must survive between runs goes in a named variable via mpsetvar.
The angle form is greedy about its space. $<$i tab> is object, space, name; forget the space or the object and the engine does its best with what remains, which reads as empty. If an angle form comes back blank, count its parts: dollar, angle, object token, one space, name, angle.
A dollar before <, %, [, or { always starts a special form and will consume text while hunting for the closing character. Innocent prose like costs $5 <at least> will eat everything between the angles. When a chunk of your sentence vanishes near a dollar sign, write the dollar as $$ and the sentence heals.
Part Seven: Conditions That Lie
An if that is quietly always true, or always false, is worse than an error: half the script still runs and the bug wears a disguise. Every entry in this part produces one of those two eternal answers, and one tracer mpecho per branch, Part Two style, exposes them all in a single firing.
Entry 34: Writing && And || For And and Or
If you have seen code anywhere else in your life, your fingers will type the symbols. MUDProg joins conditions with the WORDS and, or, and not, and the symbol forms are not errors; they are worse, they are text:
GREET_PROG 100
if ispc($n) && level($n) > 200
say Only a level two hundred player should ever hear this.
else
say This is the line everyone should hear.
endif
~
What you see in game: every visitor, from the first-day novice to a passing chicken, is greeted as a level two hundred player. A near-impossible condition is always true.
Why it happens: the engine does not know &&, so it falls back to treating the whole line as one plain comparison: the left side becomes literal text, dollar codes substituted but functions left as words, and comparing that text against 200 is a spelling contest the letters win. None of your intended questions were ever asked.
The fix is the words:
GREET_PROG 100
if ispc($n) AND level($n) > 200
say Only a level two hundred player should ever hear this.
else
say This is the line everyone should hear.
endif
~
Upper or lower case both work; this guide capitalizes connectors for visibility. The same trap swallows || for or. When a compound condition behaves impossibly, look for symbols where words belong; the flow chapter lists every legal form.
Entry 35: A Condition That Starts With The Word not
The connector not joins two conditions, as in A and not B. It cannot STAND FIRST, and a condition that opens with it dies quietly:
GREET_PROG 100
if not ispc($n)
emote pointedly ignores the creature.
else
say Ah, a paying customer at last!
endif
~
What you see in game: every visitor, player and mob alike, is hailed as a paying customer. The wandering rats get a sales pitch.
Why it happens: with not in front, the engine reads not ispc as one strange function name, finds no such function, and a missing function answers no: always false, else always runs. Nothing warns you, because unknown functions are allowed; they might be added tomorrow.
The fix is the exclamation mark, which flips the condition it is glued to and is legal at the front:
GREET_PROG 100
if !ispc($n)
emote pointedly ignores the creature.
else
say Ah, a paying customer at last!
endif
~
The word not remains correct BETWEEN conditions: isfight($i) and not ispc($n) works. First position belongs to the bang.
Entry 36: The Function That Lost Its Parentheses
Functions are name, parentheses, arguments. Drop the parentheses and the name is just a word in a text comparison, and Entry 34's disguise returns:
GREET_PROG 100
if level $n >= 20
say The guild takes veterans only, and you qualify.
else
say Come back after you have seen more of the world.
endif
~
What you see in game: everyone qualifies, including characters created this morning. The left side is the literal text level Marla, the comparison is spelling against 20, and letters outspell digits every time.
The fix:
GREET_PROG 100
if level($n) >= 20
say The guild takes veterans only, and you qualify.
else
say Come back after you have seen more of the world.
endif
~
The engine is forgiving about WHERE the comparison sits, level($n) >= 20 and level($n >= 20) both work, but the parentheses are not optional. Self-check: in any numeric if, the left side must be a function WITH its parentheses or a dollar form, because bare words compare as spelling, and spelling against digits is effectively always true.
Entry 37: rand() Is A Coin, Not A Die
Two similarly named functions do different jobs. rand(30) asks a percent question, true thirty times in a hundred; randnum(6) rolls a die, 1 to 6. Use rand where you meant randnum and the odds silently collapse:
RAND_PROG 100
if rand(6)
emote wins the dice throw and crows in triumph.
else
emote loses the dice throw and mutters darkly.
endif
~
What you see in game: the gambler loses almost every time, day after day, because rand(6) is true six percent of the time. Built fair, plays fixed.
The fix, for an actual one-in-six die:
RAND_PROG 100
if randnum(6) == 6
emote rolls a six and sweeps the pot with a grin.
else
emote rolls the bone dice and mutters at the result.
endif
~
Rule of thumb: rand takes the CHANCE as a percent; randnum takes the SIZE of the die and must be compared to a face. And if randnum(6) bare is a third trap: nonzero counts as yes and randnum never rolls zero, so it is always true.
Entry 38: strin() Reads Needle First
The substring question strin(a b) asks whether a appears inside b: needle first, haystack second. Reversed, testing speech as strin($g gold) asks whether the ENTIRE spoken line appears inside the four letters gold, true only when a player says exactly gold and nothing else; the mob seems to demand telegraphic speech. The working order puts the word first and the speech second:
SPEECH_PROG all
if strin(gold $g)
say Gold! Yes! Tell me everything about the gold!
else
say Keep talking, friend, none of that interests me yet.
endif
~
Needle, then haystack: is gold anywhere in what they said. One wrinkle: the needle is a single word, the form's first token, so keep strin needles to one word and it will never surprise you.
Part Eight: Loops And Flow That Would Not
Control flow bugs have loud symptoms, lines repeating strangely or whole sections running when they should not, and one quiet one, the loop that never runs. All yield to the tracer habit.
Entry 39: The Missing endif Swallows The Rest
Every if needs its endif, and the engine cannot supply a missing one; it keeps reading, and everything to the end of the block becomes part of the if:
GREET_PROG 100
if ispc($n)
say One moment, checking my list.
say You are cleared to pass.
~
What you see in game: for players, both lines, all seems well. When a mob wanders in, NOTHING prints, though the second line was meant for everyone: it was swallowed into the if, without warning, because the engine closes the block at the tilde and assumes you meant it.
The fix:
GREET_PROG 100
if ispc($n)
say One moment, checking my list.
endif
say You are cleared to pass.
~
The habit that prevents it: indent inside every if and read the shape before saving; an endif belongs at the same depth as its if. The same pairing binds every structure: if with endif, switch with endswitch, for with next, while with endwhile. A missing closer never errors; it extends the structure to the tilde and quietly rearranges your script.
Entry 40: There Is No elseif
Ladders of choices tempt the elseif spelling from other languages. MUDProg does not have it, and what happens instead is sneaky:
GREET_PROG 100
if level($n) > 50
say A veteran walks among us.
elseif level($n) > 20
say A seasoned traveler, I see.
else
say Fresh boots, fresh hopes.
endif
~
What you see in game: newcomers get Fresh boots correctly, but every visitor above fifty is greeted TWICE, veteran then seasoned traveler, and the mob sounds like it is bargaining.
Why it happens: elseif is not a structure word, so it is filed as an ordinary command INSIDE the first branch, where it fails silently, and the seasoned line lands in that same branch. The mid-rung of the ladder never existed.
The fix is a real nested if inside the else:
GREET_PROG 100
if level($n) > 50
say A veteran walks among us.
else
if level($n) > 20
say A seasoned traveler, I see.
else
say Fresh boots, fresh hopes.
endif
endif
~
Each if has its own endif, and the indentation shows the rungs. For ladders keyed on one value, the switch structure from the flow chapter reads better; for ladders of different questions, nest, one level per rung.
Entry 41: The Loop Counter That Must Be A Digit
A for loop counts in a numbered slot, $1 through $9, and ONLY those. Name the counter anything else and it counts where no code can read:
GREET_PROG 100
for x = 1 to 3
emote tosses ball number $x into the air.
next
~
What you see in game: three emotes, but the mob tosses ball number north, ball number east, ball number north. The counter x was stored where no text can read, and the $x you wrote is not your counter at all: it is the ROOM EXIT code, which names a random exit each time. The collision goes to the exit code.
The fix is a digit slot on both ends:
GREET_PROG 100
for $1 = 1 to 3
emote tosses ball number $1 into the air.
next
~
Balls one, two, three, in order. The for line's shape is rigid: for $1 = 1 to 3, spaces and all. Written for $1 = 1-3 the loop shrugs and runs once with the counter unset. Counting DOWN needs no extra syntax: for $1 = 3 to 1.
Entry 42: The while That Could Not Stop
A while runs as long as its condition stays true, which means something in the body must be able to CHANGE the condition. Forget that and the loop spins in place:
GREET_PROG 100
mpsetvar $i countdown 3
while var($i countdown) > 0
mpargset 1 spinning
endwhile
say My countdown reads $<$i countdown>, and I was cut off mid-spin.
~
What you see in game: a pause you cannot feel, then the say line reporting the countdown still at 3. The body never touched the countdown, the condition never changed, and the loop cap ended the loop after its two thousandth lap, letting the rest of the block continue. The loop cap is silent; the step budget in Part Eleven is the one that logs.
The fix is a body that moves the condition, here counting down with the math tool from the functions chapter:
GREET_PROG 100
mpsetvar $i countdown 3
while var($i countdown) > 0
mpargset 1 down to $<$i countdown>
mpsetvar $i countdown $%math($<$i countdown> - 1)%
endwhile
say Counted all the way down, and the ledger now reads $<$i countdown>.
~
The counterpart is the while that never STARTS: a condition false on arrival, often Entry 30's unset variable, skips the body silently. One tracer above the loop and one inside distinguish never-started from never-stopped in a single firing.
Entry 43: return Ends One Block, Not The Trigger
A script may hold several blocks for one trigger, and all of them run when it fires. return ends only the block it stands in, so a guard in the first does not protect the second:
SPEECH_PROG all
if var($i shopclosed) == 1
return
endif
say The shop hears you.
~
SPEECH_PROG all
emote polishes the counter, listening.
~
What you see in game: close the shop by setting the variable, and the mob stops answering but KEEPS polishing at every word; the second block never heard about the guard. There is no shared exit between blocks.
The fix, when the behaviors belong together, is one block:
SPEECH_PROG all
if var($i mainclosed) == 1
return
endif
say The shop hears you.
emote polishes the counter, listening.
~
When the behaviors must stay separate, copy the guard into each block. Ask whether the pieces should ever fire independently; if no, they are one block wearing two tildes.
Part Nine: mpsleep Misunderstandings
mpsleep pauses the script for a number of whole seconds, then runs the remaining lines. The argument is whole seconds, minimum and default one, fractions rounded down, so mpsleep 0.5 sleeps a full second; there are no half beats. The three bigger misunderstandings get entries.
Entry 44: The World Does Not Sleep With You
mpsleep pauses ONE run of one script. The mob's other triggers stay live, its heartbeat keeps beating, and its idle blocks keep rolling, straight through your timed scene:
GREET_PROG 100
say Let me tell you a story of the old kingdom.
mpsleep 2
say And that, friend, was how the kingdom fell.
~
RAND_PROG 35
emote scratches his ear absently.
~
What you see in game: some visitors get story, pause, ending. Others get story, EAR SCRATCH, ending, because the idle block rolled its dice during the silent seconds. The storyteller upstages himself.
The fix is a busy latch: raise a flag during the scene, and teach the idle block to respect it:
GREET_PROG 100
mpsetvar $i busy 1
say Let me tell you a story of the old kingdom.
mpsleep 2
say And that, friend, was how the kingdom fell.
mpsetvar $i busy 0
~
RAND_PROG 35
if var($i busy) == 1
return
endif
emote scratches his ear absently.
~
The same latch serves every long scene: set it first, clear it last, check it at the top of every block that could interrupt, including the greet block itself, so a second visitor cannot restart the story mid-telling.
Entry 45: Sleeping Inside A Loop Abandons The Loop
A sleep finishes the current pass of a loop, then continues AFTER the loop; the remaining laps are quietly dropped:
GREET_PROG 100
for $1 = 1 to 3
say Fireworks volley $1!
mpsleep 1
next
say That was all three volleys!
~
What you see in game: Fireworks volley 1, a pause, then That was all three volleys. Volleys two and three never fire: the loop's countdown machinery does not survive the trip through the pause, and the script wakes past the loop's end.
The fix is to unroll timed sequences by hand; a script that wants pauses between beats should be written as beats:
GREET_PROG 100
say Fireworks volley 1!
mpsleep 1
say Fireworks volley 2!
mpsleep 1
say Fireworks volley 3!
say That was all three volleys!
~
Loops repeat work within a moment; sleeps space moments apart. Keep each tool on its own side of that line and both behave. For long timed sequences, mpalarm from the commands chapter schedules each future line independently and has no such limit.
Entry 46: Sleeping Through Your Own Death
DEATH_PROG runs as the mob dies. A sleep inside it schedules the rest of the block for a future the mob does not have:
DEATH_PROG
say You have not seen the last of me!
mpsleep 3
mpecho A dark mist rises from the corpse and streams away north.
~
What you see in game: the dying words land, and the mist never comes: by the time three seconds pass the mob is gone, and a script whose host is destroyed simply stops, by design. Under test the mist DOES appear, because testing does not kill the mob, which makes this bug especially good at hiding.
The fix is to keep a death scene immediate, all of it:
DEATH_PROG
say You have not seen the last of me!
mpecho A dark mist tears itself free of the falling body and streams away north.
~
An effect that truly must happen seconds after the death cannot be hosted on the dying mob; give it to the room, whose script survives, using the death fan-out described in the triggers chapter.
Entry 47: The Audience Left During The Pause
Between sleep and wake, the player can walk away. The script continues regardless, speech playing to an emptier room, a promised item dropped at nobody's feet. The polish for any post-sleep payoff is to confirm the guest is still present, and the roster code $l makes a neat check with strin():
GREET_PROG 100
say Wait there, I have something for you in the back.
mpsleep 2
if strin($n $l)
say Here it is. Fresh as promised!
mpoloadroom /obj/meal
endif
~
The code $l lists every living thing in the room except the host, so asking whether $n appears in it asks whether the visitor is still here; if not, the mob keeps its meal and its dignity. One line, always worth it.
Part Ten: The Veto That Did Not Veto
CNCLMSG_PROG, from the bus chapter, cancels an action before it happens and runs your block instead. It is the most powerful trigger in the engine and the one with the most ways to quietly not work. These entries walk the failure ladder from the header down.
Entry 48: You Attached The Observer, Not The Veto
Two bus triggers share one header shape, and only one stops anything. EXECMSG_PROG watches; CNCLMSG_PROG replaces:
EXECMSG_PROG GET relic
mpechoat $n The relic twists in your grip, resisting you.
~
What you see in game: the message prints beautifully, and the player walks off with the relic anyway. The observer ran DURING the take, as observers do; nothing was asked to stop.
The fix is the veto trigger, same header grammar, opposite role:
CNCLMSG_PROG GET relic
mpechoat $n The relic twists away from your fingers, refusing your grip.
~
Now the take is cancelled and your line plays instead. The memory hook: EXEC means it is executing, you are commentary; CNCL means cancel, you are the replacement. When a cursed item prints its message but protects nothing, check which name is on the header.
Entry 49: The Code Word That Is Not A Code
The first word after the trigger name is the bus code, and it must be one the bus actually speaks: GET, DROP, PUT, WEAR, REMOVE, OPEN, CLOSE, LOCK, UNLOCK, EAT, DRINK, CAST, ATTACK, ENTER, LEAVE, BUY, SELL, GIVE, plus the aliases the bus chapter lists, such as CONSUME and FIGHT. Write a word from your own head and the veto never matches:
CNCLMSG_PROG TAKE relic
mpechoat $n The relic squirms away from your fingers.
~
What you see in game: takes succeed, silently, forever. TAKE is English for it, but the bus code is GET, and an unknown code matches no event. No error says so; an unrecognized code is a veto waiting for an event that cannot come.
The fix:
CNCLMSG_PROG GET relic
mpechoat $n The relic squirms away from your fingers.
~
Two mask notes: the keyword after the code matches the item's KEY NAME, so mask on the plain noun, relic, not a phrase like p ancient bronze relic, which fails when the key name is simply relic. And bus headers are keyword gates, so the test rider cannot open them; prove a veto by attempting the real action with a real item.
Entry 50: The Veto That Swallowed The Room
The code spec ALL matches every bus code, and a veto that matches everything cancels everything:
CNCLMSG_PROG ALL
mpecho The air itself refuses all action here.
~
What you see in game: nobody in the room can pick up, drop, wear, eat, buy, attack, open, or LEAVE. Every departure is cancelled with your one eerie line. Players file this as a critical bug, because from inside it is one.
Why it happens: veto scope covers the scripted object, its room, and everyone present, and ALL really does mean every code, movement included. A demonstration header escaped into the world.
The fix is to name the action and mask the thing, as in Entry 49's corrected relic veto. If you do trap a room this way, mudprog <target> clear cures it instantly; the actions were cancelled, not damaged.
Entry 51: The Veto That Worked Silently
A veto REPLACES the action, message and all. If your block does not speak, the world just refuses, blankly:
CNCLMSG_PROG GET relic
mpsetvar $i grab_attempts 1
~
What you see in game: a player tries to take the relic. Nothing happens. No message, no error, no relic in hand. They try again, harder, then page staff about the broken item. Your veto worked perfectly and told no one.
Why it happens: the cancelled action's own success message was part of what got cancelled. Whatever the player sees is now entirely your script's job, and this script says nothing.
The fix is to always include the story of the refusal:
CNCLMSG_PROG GET relic
mpechoat $n The relic turns cold and slips through your fingers.
mpechoaround $n $N snatches at the relic and comes away empty-handed.
~
The pair of echoes gives the actor a private explanation and the room a public one, the standard costume for any veto. Rule: every CNCLMSG body contains at least one mpechoat $n line, even the temporary ones, because the temporary ones are the ones players find.
Entry 52: The Veto With A Broken Body
The bus has a safety valve: a veto script that errors while running ALLOWS the action, so a veto with a dying body looks exactly like Entry 49, takes succeeding that should not. When a veto is intermittent rather than never, suspect the body: salt it with mplog and read /log/mudprog after the next attempt. Keep veto bodies to two or three lines of refusal; anything elaborate belongs in a normal trigger the veto sets in motion.
Part Eleven: The Step Budget And The Loop Cap
Two ceilings protect the mud from runaway scripts. Every firing of a trigger gets a budget of four thousand steps, a step being one executed action, and any single loop stops itself after two thousand laps. The loop cap ends just the loop, silently, as Entry 42 showed. The step budget stops the whole run where it stands, and logs a line naming the mob and trigger to /log/script_runaway. A script that stops dead mid-run has usually blown one of the two, and the log tells you which.
What eats budget: loops, and loops inside loops especially; a for over 100 items with a 30-line body spends three thousand steps in one breath. The cure is honesty about scale, scripts are theater, not batch processing, plus one trick: the budget is PER RUN, and waking from mpsleep starts a fresh run with a fresh budget, so a long performance broken into scenes by short sleeps never troubles the ceiling.
Part Twelve: Two Exercises
Reading a field guide is one thing; using it is another. Here are two broken scripts. Read the symptoms, diagnose from the entries above, then read the worked answer.
Exercise one. A dockmaster should welcome each player once ever, and answer questions about passage. A builder attached:
GREET_PROG 100
if var($n dock_seen) == 0
mpsetvar $n dock_seen 1
say First time at my docks, eh? Mind the ropes.
else
say Back again, sailor?
endif
SPEECH_PROG passage, ticket
say The ferry costs ten gold, leaves at dawn!
~
Symptoms: every player, even brand new ones, gets Back again, sailor, and asking about passage does nothing at all.
The diagnosis: Back again for strangers is Entry 30, the unset variable compared against zero. Passage unanswered is Entry 1 in rope boots: no tilde before SPEECH_PROG, so the speech block is trapped inside the greet block, and the Triggers line proves it, listing GREET_PROG alone. A third bug waits behind the tilde: Entry 16's comma riding on the keyword passage.
The worked fix, all three repairs:
GREET_PROG 100
if !var($n dock_seen)
mpsetvar $n dock_seen 1
say First time at my docks, eh? Mind the ropes.
else
say Back again, sailor?
endif
~
SPEECH_PROG passage ticket
say The ferry costs ten gold, leaves at dawn!
~
Exercise two. A fire-eater should perform three numbered breaths with a pause between each, unless mid-show. A builder attached:
GREET_PROG 100
for $1 = 1 to 3
emote breathes a plume of flame, that was breath $1!
mpsleep 2
next
say The show is over, tip your performer!
~
Symptoms: one plume, one pause, then the closing line; breaths two and three never come. And when two players arrive close together, the shows tangle into each other.
The diagnosis: Entry 45, a sleep inside a loop abandons the loop, so the script wakes past it; and Entry 44, no busy latch, so a second GREET starts a second show mid-first. The fix unrolls the breaths and latches the show:
GREET_PROG 100
if var($i showbusy) == 1
return
endif
mpsetvar $i showbusy 1
emote breathes a plume of flame, that was breath 1!
mpsleep 2
emote breathes a plume of flame, that was breath 2!
mpsleep 2
emote breathes a plume of flame, that was breath 3!
say The show is over, tip your performer!
mpsetvar $i showbusy 0
~
If you found all five bugs before reading the answers, you are no longer debugging by luck.
Part Thirteen: The Silence Checklist
For the most common complaint of all, the script that simply does nothing, here is the chapter as one routine. Work it top to bottom and you will land on your bug.
View it: mudprog <target>. No script means the edit never saved or the mob is a fresh respawn (Entries 7 and 14). Wrong text means your client mangled it (Entry 26).
Read the Triggers line. A missing trigger is a lost tilde or header (Entries 1 to 3); a strange trigger is a body line read as a header (Entry 3); a right-looking name may still be misspelled (Entries 4 and 5).
Test it: mudprog <target> test <TRIGGER>. No such block means the name in your head and the name on the mob differ. A firing body that misbehaves wants tracer lines (Part Two); a body that fires perfectly is healthy, so keep going.
Check the object: is this a trigger this object can hear (Entries 8 to 12)? A pulse trigger on a room, an item hoping to greet, a mob-side ENTRY, a trigger not yet wired live?
Check the gate (Part Five): raise percents to 100 while testing, say the actual keyword out loud, probe zapper values with $%class($n)%, and remember the test rider cannot open a keyword gate.
Then fire the REAL event once, as a player would: walk in, say the word, hand over the item, pick the thing up. Scripts are stagecraft, and the only test that finally counts is the one with the curtain up.
If all of that passes and the mystery survives, bring a senior builder the script text, the Triggers line, what test does, and what live play does: a diagnosis nine tenths finished. That handoff is what this chapter was for, turning does not work into here is exactly what happens, which is the whole craft of debugging in one phrase.
This chapter is an archetype deep-dive: one kind of character, taken as far as scripting can take it. The archetype is the shopkeeper, and there is no better one to study. A shopkeeper needs a memory for faces, opinions about goods, rules about who may trade and when, a weakness for flattery, and a sharp eye on the stock. Every one of those is a scripting technique, and by the time you have built the twelve merchants in this chapter you will have used per-player memory, commerce triggers, zapper masks, the message bus, the mud clock, and the money commands, all in the service of people who just want to sell you a lamp.
Everything here builds on the basics chapter. If a PROG block, a dollar code, or an if line is unfamiliar, read help mudprog-basics first; this chapter re-explains as it goes, but it does assume you have attached a script once and watched it fire. Every script printed here is complete and self-contained: attach it to a practice mob exactly as printed, using the stock items /obj/meal, /obj/armor, /obj/torch, and /obj/container, and it works. When you adapt one for your area, swap those paths for your own.
What The Game Already Does For You
Before scripting a merchant, know what you get for free, because the best shop scripts decorate the machinery rather than rebuild it.
A TRUE VENDOR is a mob built by a coder on the vendor library. It has a storage room full of stock and it already answers the trade commands: players can browse its wares, ask the price of something, buy it, sell things to it, and have goods appraised. Money changes hands correctly, change is counted, and transactions are logged, all without a single line of script. If your area needs a functioning shop, ask a senior builder to stand up a real vendor; it is a few minutes of work.
What the vendor machinery does NOT have is a soul. A stock vendor greets nobody, remembers nobody, loves nothing it sells, and lets its wares go with all the sentiment of a vending machine. That is the gap scripts fill, and it is a wide one: everything in this chapter runs on top of the machinery without touching it.
The second kind of merchant is an ORDINARY MOB PLAYING SHOP. Any mob can act the merchant with nothing but script: players hand it things with the give command, the script inspects what arrived, pays with mpmoney, and hands goods back. No storage room, no browse list, just theater and a few commands. This kind cannot run the real trade commands, but for a fence in an alley, a beggar selling secrets, or a black-market table in a back room, theater is exactly right, and two of the merchants in this chapter work this way.
The distinction matters for one practical reason. The two commerce triggers, BUY_PROG and SELL_PROG, and the two trade veto codes, BUY and SELL, fire from inside the vendor machinery, so they only fire live on a true vendor. Attach them to a plain practice mob and they will sit quiet in normal play, though you can always fire them by hand with mudprog <target> test BUY_PROG while you iterate, which is exactly how you will test them in this chapter. Everything else here, greetings, speech, memory, masks, the give economy, works identically on any mob.
One honest limitation to absorb now, because two sections lean on it: scripts cannot change a vendor's prices. There is no script command that reaches into the pricing tables. Every discount, surcharge, and haggle in this chapter is therefore built from things scripts CAN do: paying money back with mpmoney, gifting goods, or plain unforgettable dialogue. This turns out to be enough, and the haggling section shows why.
The Shopkeeper's Toolkit
Everything commerce-flavored in the engine, gathered in one place. Each tool gets a full treatment later in the chapter; this list is here so you can see the whole workbench at once.
The reaction triggers, which fire AFTER something happens:
GREET_PROG <pct|mask> - A player walks in. The front door bell.
SPEECH_PROG <keywords> - A player says something nearby. Haggles,
passwords, questions about stock.
BUY_PROG <pct> - A player bought something from the vendor.
The item is $o. True vendors only.
SELL_PROG <pct> - A player sold something TO the vendor.
The item is $o. True vendors only.
GIVE_PROG all - A player handed the mob an item; $o again.
The whole economy of a scripted fence.
BRIBE_PROG <pct> - A player gave the mob coins; the amount
rides in $g. See the tip jar section for
an honest note on when this fires.
TIME_PROG <hours> - The mud clock reached a listed hour. Shop
hours live here.
LOOK_PROG <pct> - A player looked at the mob. A merchant
notices being sized up.
The veto pass, which runs INSTEAD of something about to happen. One header shape, many codes; the bus chapter is the full reference:
CNCLMSG_PROG BUY <mask> - Refuse a purchase. The mask is matched
against the words the player typed.
CNCLMSG_PROG SELL <mask> - Refuse to buy from a player. The mask is
matched against the offered item's name.
CNCLMSG_PROG GET <mask> - Refuse a pickup. This is how display
stock stays on its stand.
The commands a merchant script reaches for most:
mpmoney <who> [type] <amt> - Pay or charge coins. Negative amounts
take; always check goldamt first.
mpoload <path> - Clone an item into the mob's own hands.
mpput <item> <who> - Move an item into someone's inventory;
mpput $b $n hands over the last load.
mpjunk <item> - Destroy an item; how a fence makes
purchases disappear.
mpoloadshop <path> - Clone an item into a true vendor's
storage room. Restocking.
mpsetvar <who> <name> <val>- Write memory, on the shop or the customer.
And the questions a merchant script asks:
var(<who> <name>) - Read memory back.
goldamt(<who>) - Gold carried. Big spenders, and can-they-
afford-it checks before charging.
value(<item>) - An item's worth, for sorting treasure
from trinkets.
isname(<item> <word>) - Does the item answer to this name.
class(<who>) / race(<who>) / level(<who>) - Who am I serving.
shophas(<vendor> <name>) - Does the storeroom stock one of these.
numitemsshop(<vendor>) - How full the storeroom is.
datetime(day) / istime(<hour>) - The calendar and the clock.
Twelve merchants follow, in rising order of sophistication. Build them in order the first time; each one leans on techniques the ones before it established.
Script One: Bram's Counter, The Minimum Viable Shopkeeper
Start with the smallest script that makes a shop feel staffed: a greeting at the door, a word when something is bought, a word when something is sold. Three triggers, one line of character each.
GREET_PROG 100
say Welcome to Bram's Counter, $N. Everything here is for sale, even the counter.
emote pats the countertop fondly.
~
BUY_PROG 100
say A pleasure doing business, $N. Coin in the drawer, goods in your hands.
emote sweeps the coins into a drawer and bumps it shut with his hip.
~
SELL_PROG 100
say Hmm. I suppose I can find a home for $o somewhere.
emote turns the new acquisition over twice, then tucks it under the counter.
~
Walk through it. GREET_PROG 100 you know from the basics chapter: every player who walks in hears the welcome, with their own name spliced in by $N, and sees the emote. The character arrives in the second half of the line, the joke about the counter; a greeting with no personality in it is just a doorbell.
BUY_PROG fires the moment a purchase completes: the player has paid, the goods have moved, and now the vendor reacts. $n is the buyer. The item they bought is available as $o, though this block does not use it. Notice the direction of the name: BUY_PROG is named from the PLAYER'S side of the counter. The player buys, so it is BUY_PROG, even though the vendor is doing the selling. SELL_PROG is the same convention reversed: the player sells something to the shop, and here the item DOES appear in the dialogue, because $o makes the line specific: sell a lantern and Bram mutters about finding a home for the lantern.
Two testing notes before you move on. First, on a practice mob these two trade triggers fire only from mudprog <target> test BUY_PROG and mudprog <target> test SELL_PROG, because only a true vendor runs real trades; the greeting fires the ordinary way, by walking in. Second, under test there is no real item, so $o reads as the word something, and Bram finding a home for something is exactly the sort of graceful nonsense the engine falls back on. On a live vendor it becomes the item's real name.
Variations to try. Give each block a couple more lines with mpsleep 2 between them so Bram putters. Add a RAND_PROG 5 block where he dusts the shelves. Drop the BUY_PROG chance to 60 so his gratitude does not become wallpaper.
Greeting Customers By Name And Memory
A shop greeting has three levels. Level one is what Bram does: the same warm line for everyone. Level two is recognition: a different line for a first-time visitor than for a regular. Level three is bookkeeping: the shop counts your visits, and the greeting grows with the relationship. All three run on one mechanism, the per-player variable, and it is worth being precise about how that works before reading the script.
The command mpsetvar $n <name> <value> writes a named note ON THE PLAYER. Because the note lives on the player, it does two things a note on the shopkeeper cannot. It is automatically per-person: fifty customers carry fifty independent notes under the same name, and you never write a line of bookkeeping to keep them apart. And it is permanent: the note rides in the player's saved character, so the shop still knows them after a reboot, next week, next season. A note on the shopkeeper, by contrast, is one shared note, and it lasts only as long as that copy of the mob; the area resets, the mob is replaced, the note is gone. The working rule for every merchant in this chapter: facts about a CUSTOMER go on $n, facts about the SHOP, such as whether the shutters are up, go on $i.
The function var(<who> <name>) reads a note back, and a note that was never written reads as empty text, which conveniently counts as false in a bare if, and which you can also test for explicitly with the comparison var($n visits == ''), two single quotes with nothing between them meaning empty. That test is the safe way to notice a brand new customer before doing arithmetic on their count.
Here is level three in full: Widow Marsh, who counts.
GREET_PROG 100
emote looks up from her ledger of names.
if var($n marsh_visits == '')
mpsetvar $n marsh_visits 0
endif
mpargset 1 $%MATH($<$n marsh_visits> + 1)%
mpsetvar $n marsh_visits $1
if var($n marsh_visits) == 1
say A new face at my stall. I am Widow Marsh, and I remember everyone.
else
if var($n marsh_visits) >= 5
say Ah, $N, my favorite customer. That makes $1 visits, by my count.
else
say Back again, $N. Visit number $1, and always welcome.
endif
endif
if goldamt($n) >= 500
emote eyes $n's heavy purse with polite, predatory interest.
endif
~
The walkthrough, because this script introduces the counting idiom every later merchant reuses. The emote comes first and unconditionally, so the stall feels attended even before the branching begins. The first if is the safety catch described above: if this player has no marsh_visits note yet, write a 0, so the arithmetic that follows has a number to work with rather than empty text.
The mpargset line is the heart, and it reads inside out. $<$n marsh_visits> is the angle form from the variables chapter: it splices the stored count into the line as text. Around it, $%MATH(... + 1)% asks the engine to do arithmetic on the result. So on your third visit the inner part becomes MATH(2 + 1), the engine works it out to 3, and mpargset 1 drops that 3 into temporary slot $1. The next line files the new count back onto the player with mpsetvar, and from then on the block can use $1 anywhere in its dialogue. One habit worth copying: do the count first, then branch on it, so every branch already has the fresh number available.
The branching then reads the count three ways: exactly 1 is a stranger, 5 or more is a favorite, anything between is a regular. Notice the else with a fresh if on the next line, each closed by its own endif; the engine has no single else-if word, and stacking them this way is the idiomatic shape.
The last block is a level-two flourish with a different question entirely: goldamt($n) reads the gold in the customer's purse, and a customer worth five hundred or more gets noticed. Nothing mechanical follows, and that is deliberate. A shopkeeper who ACTS on purse-peeking, with prices or pressure, feels like a systems exploit; one who merely notices feels alive.
Variations to try. Move the count to the stall itself, var($i ...), and Marsh counts total customers served instead of knowing each one; the count then resets when the area does, which suits a daily tally. Greet by class with a switch on $%class($n)%, which the Emporium script below does with masks instead. Or record the day of the first visit alongside the count, datetime(day) into a second variable, and let her remark how long she has known them.
Reacting To Buying And Selling
Bram thanked everyone identically. A merchant with opinions reacts to WHAT moved across the counter, and the tool for that is $o, the item involved in the trade, interrogated with the item functions. The two you will use constantly are isname($o <word>), which asks whether the item answers to a name, and value($o), which reads its worth in coin. Corvin, dealer in everything, demonstrates both directions of the counter.
BUY_PROG 100
say Sold, and gladly. Mind how you carry it, $N.
if isname($o armor)
say That armor turned a blade for its last owner. Ask me nothing else.
endif
~
SELL_PROG 100
if isname($o meal)
say Food I can always move. The dock crews will eat anything.
else
say I will take it, though I already regret the shelf space.
endif
~
The shape to notice in the BUY block: the general line comes FIRST and unconditionally, then the specific line is added on top when the item warrants it. Buying anything gets the send-off; buying the armor gets the send-off plus the anecdote. Layering this way beats an if/else where the general line lives in one branch, for a dull-sounding reason that will save you real debugging time: a block whose every line is inside a condition is a block that can fire invisibly, and when you are staring at a silent mob wondering whether the trigger even went off, one guaranteed line answers the question.
The SELL block branches instead, because here the two moods genuinely exclude each other: happy to take food, resigned about everything else. When the item decides which single line to speak, if/else is right; when an item adds extra color on top of standard patter, layer.
Edge cases worth knowing at this counter. Selling several things at once fires SELL_PROG once per item that actually sells, so Corvin comments on each; keep sell lines short or a wagonload of goods becomes a speech. Under mudprog <target> test there is no real item, every isname test quietly answers no, and you will hear the general lines only, which is correct and expected; test the specific branches on a live vendor by actually trading the item. And isname matches the names an item was built with, the same ones a player could use to pick it up, so check what those are with a look before masking dialogue on them; the stock display armor at /obj/armor answers to armor, but a fancy cuirass in your area may not.
Variations to try. React to value instead of name: value($o) >= 100 in the SELL block gets Corvin excited about treasure and dismissive of trinkets, which the fence script below builds into an entire business model. Count purchases per customer with the Marsh idiom and have him start calling someone a patron after their fifth buy. Or keep one per-shop tally of everything sold, var($i sales), and let a RAND_PROG block brag about the total on slow afternoons.
Stocking The Shelves: The True Vendor's Tools
Three tools in the kit exist only for true vendors, and they make the storeroom itself something a script can see and touch. shophas($i <name>) asks whether the storeroom currently holds an item answering to a name. numitemsshop($i) counts what is back there. And mpoloadshop <path> clones a fresh item straight INTO the storeroom, where it immediately shows up in the browse list at a price the machinery works out itself. Together they give you a shop that answers stock questions honestly and restocks itself on request. Kassra's apprentice minds the counter:
GREET_PROG 100
say Mind the shelves, $N. Master Kassra counts them twice a day.
~
SPEECH_PROG stock
if shophas($i meal)
say Meals we have in the back. Browse, and master Kassra will sort you out.
else
say No meals in the back room today. Ask me to restock and we shall see.
endif
~
SPEECH_PROG restock
if numitemsshop($i) < 5
mpoloadshop /obj/meal
say There. The stockroom breathes again.
else
say The stockroom is full enough, and my back is grateful.
endif
~
The stock block turns a browse-and-hope interaction into a conversation: a customer asks about stock aloud, and the apprentice checks the actual storeroom, not a script variable that could drift out of date, so the answer is true even after other players have bought the shelves bare. The restock block puts a polite cap on generosity: below five items in the back, asking nicely conjures another meal into stock; at five or more, the apprentice declines. Without that guard, a bored player repeating the word restock inflates the storeroom forever, and every free-goods script needs a ceiling of exactly this kind.
The honest notes. On a plain practice mob, shophas and numitemsshop answer 0, there being no storeroom to check, and mpoloadshop quietly does nothing, so the apprentice appears mildly delusional; attach the finished script to a real vendor. The one-word speech headers are deliberate: SPEECH_PROG matches keywords anywhere in a spoken line, so generous keyword lists on a shopkeeper misfire constantly; a customer saying this shop has everything in stock would trip a header that listed shop or everything. One distinctive word per intent is the craft. Finally, mpoloadshop is restocking, not manufacturing: it clones the file you name, so what appears is always the same item at the machinery's usual price for it.
Variations to try. Gate restocking behind a fee, using the paid-service pattern from the black market script below. Restock on the clock instead of on request, a TIME_PROG block loading the morning delivery, which the hours section is about to make possible. Or let the apprentice admit what is missing: several stock blocks, each checking shophas for a different staple, so regulars learn to ask before they walk to the docks.
The Haggle
Haggling is the soul of shopkeeping and the first place every builder asks for the impossible, so let us start there: a script cannot change prices. What it can do is theater plus a rebate, and handled well that is BETTER than repricing, because the player gets a scene, a gamble, and coins physically handed back, instead of a number silently shrinking. The pieces: a keyword trigger to catch the attempt, a per-player latch so it works once per customer, a dice roll so the outcome is a story, and mpmoney to make success cost the house something real. Old Fetch runs the classic version:
GREET_PROG 100
say Prices are chalked on the crates, $N. If you think one is unfair, say so.
~
SPEECH_PROG haggle discount bargain cheaper
if var($n fetch_haggled)
say We have danced this dance already, $N. The chalk stays where it is.
else
mpsetvar $n fetch_haggled yes
if rand(35)
say Bah. You wear me down. Here is a little back, and tell no one.
mpmoney $n 10
else
say A bold try. That price was chalked before you were born.
endif
endif
~
The greeting bakes the invitation in, which matters more than it looks: a hidden interaction that players must guess at is an interaction that does not exist. Fetch TELLS you arguing is allowed.
The haggle block opens with the latch, and the order of operations is the entire trick: the bare var test finds the note fetch_haggled on anyone who has tried before and shuts them down; anyone else is IMMEDIATELY marked with mpsetvar BEFORE the dice are rolled. Latch first, then roll. Write it the other way, marking only winners or only losers, and the unmarked case can simply repeat the keyword until the dice cooperate, which turns your charming merchant into a slot machine. The roll itself is rand(35), true thirty-five times in a hundred, and the winner is paid ten gold on the spot, the rebate standing in for the discount the engine cannot give. The loser gets a good line, which is not nothing; half the fun of haggling is losing with style.
What-ifs, because this little script is a bundle of tuning choices. If ten gold is meaningless at your players' level, raise it, or pay in goods, mpoload and mpput $b $n, the hand-over idiom the black market script demonstrates. If once per customer EVER feels harsh, make it once per day: store $%datetime(day)% in a second note beside the latch when it is set, and on each attempt compare the stored day against today, clearing the latch when they differ. If you want charisma to matter, gate the good roll behind stat($n cha) >= 15 with an AND. And keep the keyword list tight; the four words above are all unambiguous intent, and adding a word like price would trip the block on every innocent question about prices.
One more honest note: the latch note lives on the player forever, so a customer who haggled once is remembered across reboots. That is usually the charm of the thing, but it means your OWN repeated tests also latch; while iterating, clear your note by resetting the value, for example scripttest mpsetvar $i fetch_haggled run by you sets your own note back to empty, since on your own scripttest line $i is you.
Opening Hours
Shops that never close are furniture. Shops that open at eight and shut at midnight are places in a living town, and the machinery for that is TIME_PROG, whose header is not a percent chance but a list of hours from the mud clock, 0 through 23, separated by spaces. The block fires once as the clock reaches a listed hour. Under the hood the scripted mob checks the clock on its own heartbeat, which gives TIME_PROG three honest properties to plan around: it is a mob trigger, not a room or item one; the mob must be loaded, which is to say somebody has visited its area since the last reboot, for the hour to be noticed; and if the area sat unloaded across an hour boundary, that boundary is simply missed, not queued up. Shop hours built on it should therefore always carry a fallback that works even if an opening or closing was missed, and the standard fallback is a shop-state variable that the greeting consults. Senna's stall shows the full pattern:
TIME_PROG 8
mpsetvar $i senna_closed no
emote unhooks the shutters and lays out the morning stock.
say Fresh stock and fair prices, friends. The stall is open.
~
TIME_PROG 0
mpsetvar $i senna_closed yes
emote pulls the shutters to and pockets the day's takings.
say That is the day done. Come back when the sun is up.
~
GREET_PROG 100
if var($i senna_closed) == yes
say The stall is shut, $N. Even a merchant sleeps. Come back at eight.
else
say Come in, $N, come in. The stall is open and the stock is fresh.
endif
~
Two clock blocks and one greeting. At eight the stall opens: the state note senna_closed flips to no, and the room gets the little scene. At hour zero, midnight, it closes the same way. Note where the state note lives: on $i, the stall keeper, because open or shut is a fact about the SHOP, shared by all customers alike, exactly the division of memory the Marsh section laid down. The greeting then never trusts the clock directly; it trusts the note. A customer who arrives at three in the morning is told to come back at eight even if the stall's area slept through several closings, because the note holds the last state that actually happened.
Now the honest paragraph, because hours are where scripting meets its limits. Senna's closed stall still TRADES if she is a true vendor: the browse and buy machinery does not consult your variable, and a CNCLMSG_PROG BUY veto cannot help you here, because a veto block cancels whenever its header matches, header only, no ifs about it; there is no way to veto purchases only-when-closed. You have three honest options. Accept hours as presentation, which for most shops is genuinely enough; players respect a shop that talks like it is closed. Give the shop a door and have the closing block physically lock it, mpclose and mplock on the door, with mpunlock and mpopen in the morning block, which makes the building itself enforce the hours. Or have the keeper leave: an mpgoto to a back-room at closing and back at opening, since a vendor who is elsewhere cannot be traded with at all. Each step up is more real and more work; pick the rung the shop deserves.
Testing hours without waiting for midnight: the test harness has a quirk worth knowing. mudprog <target> test TIME_PROG carries the word test where the hour should be, which the engine reads as hour 0, so the test fires exactly those TIME_PROG blocks whose list contains 0. Here that means the closing block, and it is why the closing hour in this script is written as 0: fire the test, watch the shutters come down, walk out and in, and the greeting's closed branch runs, proving the whole chain. For a block keyed to another hour, trust the same chain and check its state note, or simply relist the hour temporarily. And remember the note SURVIVES your testing: after that test the stall believes it is closed, so flip it back with a second test at a listed opening hour, or clear the script and start over.
Variations to try. A last-call warning, TIME_PROG 23, one hour before closing. Different stock by daylight, the morning block restocking with mpoloadshop while a night block does not. Or a night market that runs opposite hours entirely, which is nothing but swapping the notes, and pairs beautifully with the isnight() function for after-dark flavor lines.
Special Offers By Class And Race: Zapper Masks
Every trigger so far has taken a percent chance in its header. There is a second header form, inherited from CoffeeMUD and called a zapper mask, that instead restricts WHO can set the trigger off, judged against $n, the person who caused the event. For a shopkeeper this is the difference between one greeting for everyone and a shop that recognizes trades, guilds, and kin on sight, before a single if line runs.
A mask starts with a dash-word and the engine reads the whole header as clauses. Each clause is a dash-word naming what to check, followed by the values that qualify, each usually written with a plus sign. All clauses must pass; within one clause, matching ANY listed value passes. The clause words the engine knows:
-player or -pc - the source is a player. No values.
-npc or -mob - the source is a mob. No values.
-good / -evil - alignment checks. No values.
-class +rogue +mage - class is any listed one.
-race +dwarf +elf - race is any listed one.
-sex +female - gender matches.
-name +marla - the source answers to a listed name.
-deity +sivaine - worships a listed deity.
-level +30 - level 30 or higher.
-level +10-20 - level inside the range, inclusive.
Values are case-insensitive, several values in one clause are an or, and several clauses in one header are an and, so -class +rogue -level +30 reads: a rogue, AND level thirty or above. A clause word the engine does not recognize passes leniently rather than blocking the trigger, which keeps pasted CoffeeMUD masks from silencing a mob. One trap: the engine decides header-kind by the first character, so a mask must begin with a dash-WORD; a header like -5 is not a mask, and a plain number remains a percent chance, a confusion the mistakes section returns to.
Ilbrin's Emporium puts masks straight to shopkeeping work, four GREET blocks deep:
GREET_PROG 100
say Welcome to Ilbrin's Emporium, where every trade is a small adventure.
~
GREET_PROG -class +mage +necromancer
say For you, robed one, the back shelf. Components, inks, and quiet things.
~
GREET_PROG -race +dwarf
say Stonekin! The forge-stock is under the counter, at kin prices.
~
GREET_PROG -player -level 30
say A veteran, no less. Veterans see the good stock, not the window pieces.
~
The mechanism to understand: when one player walks in, EVERY greet block whose header passes fires, in the order written. Everyone hears the house welcome. A mage hears the welcome and then the back-shelf pitch. A level-40 dwarf warrior hears the welcome, the kin prices, and the veteran line, three blocks in a row. This is why each masked line is written as an ADDITION to the welcome, not a replacement for it; masked blocks stack, and fighting that fact leads to misery. If you truly want one exclusive greeting per kind of customer, do not reach for masks at all; use one block and a switch on $%class($n)%, which picks a single case by design. Masks shine precisely when stacking is what you want: each pitch is independent, and a customer who qualifies twice deserves both.
What the offers should DO is the same honest story as haggling: masks choose the audience, and the perk is a rebate, a gift, or dialogue, not a price change. The kin-prices line above is pure talk; to give it teeth, add mpmoney $n 5 under the dwarf mask and Ilbrin genuinely kicks back coin to stonekin at the door, at which point remember the haggle lesson and latch it per-player, or generosity becomes an income stream for anyone pacing in and out.
Edge cases and what-ifs. The class values must be the real class names on this mud, lower case as players see them; -class +Mage works, the matching being case-insensitive, but -class +wizard silently matches nobody, and a mask that matches nobody produces the most frustrating symptom in scripting, the block that never fires. The -name clause makes personal service: -name +marla on a greet is a shop that has one specific friend. And because masks judge $n on ANY trigger kind, they work on SPEECH_PROG too, giving you the rogue-only conversation the next script is built around.
The Fence: A Shop That Chooses Its Customers
Everything so far served everyone. Greasy Tam serves seventeen people in the world, and looks like a whittling stand to the rest. He is an ordinary mob, not a true vendor, because his entire business is the give economy: qualified sellers hand him goods, he hands back coin, the goods cease to exist. Three techniques meet here: zapper masks for the front-of-house sorting, a many-part condition for the back-office check, and the mpmoney and mpjunk pair for the transaction itself.
GREET_PROG 100
emote looks $N over once, finds nothing interesting, and goes back to whittling.
~
GREET_PROG -class +rogue +thief +assassin -level 25
mpechoat $n Tam's eyes flick to your hands, then to your belt, and he nods once.
mpechoat $n Wares in hand find a price here, and no questions are kept on file.
mpechoaround $n Tam sizes up $N for a long moment, then loses interest.
~
GIVE_PROG all
if class($n) == rogue OR class($n) == thief OR class($n) == assassin AND level($n) >= 25
if value($o) >= 100
say Fine work, this. Fifty in gold, and we never spoke.
mpmoney $n 50
else
say Trinket trade. Ten gold, take it or take it back.
mpmoney $n 10
endif
mpjunk $o
else
say Wrong stall, friend. This is a whittling stand.
mpput $o $n
endif
~
Front of house first. The unmasked greet is the cover story, and it is everyone's experience of Tam: a bored whittler. The masked greet fires only for the trade, rogues and their darker cousins of level twenty-five and up, and it uses the private-message pair from the basics chapter to run two scenes at once: the qualified customer privately reads the invitation via the two mpechoat lines, while everyone else in the room reads the mpechoaround line, in which Tam conspicuously fails to care. A secret told in a crowded room, with the crowd watching, is the whole fantasy of a fence, and it costs three lines.
Back office second, and here is the subtlety this script exists to teach. The GIVE_PROG cannot rely on the mask alone, because GIVE_PROG fires for ANYONE who hands Tam an item; a paladin can give him a sword whether or not any greeting ever fired for them. Gates on the door do not guard the counter. So the give block re-checks qualifications in its condition, and that condition is worth reading slowly. It chains four questions on one if line: three class checks joined by OR, and then the level check joined on with AND.
The engine folds a chain like this left to right, one connector at a time: the three class checks pool into is-any-of-these, and THEN the AND trims the pool by level. Folding left to right is simpler than the precedence rules of real programming languages, and mostly does what you meant, but on long chains, know the rule: each new connector applies to everything already folded, not just the nearest term. When in doubt, split the question into nested ifs, one per line, which can never surprise you.
The transaction itself is four honest commands. value($o) sorts treasure from trinkets at the hundred-gold line, mpmoney pays the tier, and mpjunk $o destroys the merchandise, which is what makes a fence a fence; the goods leave the world, and no player finds Tam's stash, because there is none. Note that the payouts are WRITTEN numbers, fifty and ten. The money commands read a plain number from the script, not a computed slot, so a fence pays in tiers you write out, not in fractions of appraised value; tiered flat rates are, conveniently, also how real fences talk. The else branch is the last lesson: an unqualified giver gets the cover story and, crucially, their item BACK via mpput $o $n. Without that line Tam silently confiscates whatever strangers hand him, and your bug reports will call him a thief, which is unfair, since he is merely a fence.
Variations to try. Pay in goods, not coin, mpoload and mpput $b $n, so the fence trades contraband for contraband. Track volume per seller with the Marsh counter and unlock the fifty-gold tier only for established suppliers. Refuse worthless items even from the trade, value($o) < 5 handed straight back with an insult. Or let the -name clause build an actual client list: masks and conditions both accept names, and a fence who works from a list is a quest hook waiting to happen.
The Black Market: A Password And A Back Room
Tam sorts customers by what they are. Vessa the chandler sorts them by what they KNOW, which is the older and better trick: a password. The machinery is a phrase-matched speech trigger writing a trust note on the player, a second speech trigger that serves only the trusted, and the paid-hand-over idiom, charging real coin and delivering real goods, that any script selling anything will reuse forever.
GREET_PROG 100
say Candles, wax, and wick, $N. All honest goods, sold in daylight.
~
SPEECH_PROG p the tide keeps its secrets
if var($n vessa_trusted)
say Once was enough, friend. The back room already knows your face.
else
mpsetvar $n vessa_trusted yes
mpechoat $n Vessa holds your gaze a beat too long, then nods very slightly.
mpechoaround $n Vessa squints at $N and mutters something about candle wax.
mpechoat $n Ask about the back room, and it will open for you.
endif
~
SPEECH_PROG p back room
if var($n vessa_trusted)
if goldamt($n) >= 50
say Quickly, then. Fifty gold, and this was never here.
mpmoney $n gold -50
mpoload /obj/torch
mpput $b $n
mpechoat $n Vessa presses something wrapped in wax paper into your hands.
mpechoaround $n Vessa hands $N a small parcel wrapped in wax paper.
else
say The back room keeps one rule, friend. Fifty gold, in advance.
endif
else
say The back room is for stock, and stock is for me.
endif
~
The password block runs on the phrase form of the speech header, the letter p and then the words, which matches only the WHOLE phrase in order. This is not optional fussiness for a password: a keyword header would fire on any line containing tide or secrets, and a password that half the tavern trips over by accident is not a password. Choose a phrase nobody says by chance, and hand it out where you want the trail to start: a note in a bottle, a dying NPC's last words, another scripted mob who whispers it to rogues, which is the Tam mask and this script shaking hands. Only players trip speech triggers, so no wandering mob can mumble its way into the back room. On success the block writes the trust note on the player, permanent as all player notes are, and tells them, privately, what to do next; a secret unlocked is worthless if the player is not told the next move. The repeat branch matters too: passwords get said twice, and Once was enough is worlds better than firing the ceremony again.
The back-room block is the paid hand-over, and its shape deserves memorizing because it is THE safe way to sell anything from a script: check trust, check funds, charge, load, deliver, in exactly that order. goldamt($n) >= 50 before anything else, because the charge line, mpmoney $n gold -50, a negative amount being how scripts take money, does not itself verify funds; charge a pauper without checking and the script simply fails to collect while the goods flow anyway, making your black market a charity. Then mpoload /obj/torch clones the contraband into Vessa's hands, not onto the floor, and mpput $b $n moves it to the customer; $b always names the most recently loaded object, which is what makes the pair an idiom, load then hand over, no names needed. The stock torch stands in for whatever forbidden inventory your area actually deals in. Last, the two-audience close: the buyer privately feels the wax paper, the room sees only a parcel. Deals in this room are always two scenes at once.
What-ifs. Stack the gates: keep the password AND require the trade, the Tam condition inside the trusted branch, for a black market that needs both an introduction and the right resume. Make trust expire, storing $%datetime(day)% beside the note and comparing on entry, for a password that rotates. Sell from a MENU by checking $g further inside the trusted branch, strin(lantern $g) picking one item and strin(blade $g) another, each with its own price check and load. Or have a wrong-guess trap: a final SPEECH_PROG block keyed to near-miss words that raises Vessa's eyebrow and nothing else, so eavesdroppers who half-heard the phrase reveal themselves.
The Tip Jar: BRIBE_PROG
The engine reserves a trigger for the oldest commercial interaction of all, coins pressed into a palm. BRIBE_PROG fires when a player gives the mob money, and the amount, in copper, rides in $g, so the block reads the gift's size with number($g) and reacts on a sliding scale. First the honest note, so you build with open eyes: the live money-give path does not yet call this trigger's hook. Today a coin gift lands in the mob's purse without firing the block, and BRIBE_PROG fires only under mudprog <target> test. Write the jar now, wired correctly, and it starts collecting the day the hook lands; the shape below is the correct shape. And because a shopkeeper should never let a gift go unremarked TODAY, Toby pairs the coin block with a GIVE_PROG that accepts tips in kind, which works live right now.
BRIBE_PROG 100
if number($g) >= 100
say $N, you are a saint and a patron of the humble arts of commerce.
emote polishes the tip jar until it gleams.
mpachieve $n jar_legend
else
if number($g) >= 10
say Every coin helps, $N. The jar and I both thank you.
else
say A coin is a coin, I suppose. The jar accepts all faiths.
endif
endif
~
GIVE_PROG all
if isname($o meal)
say For me? The jar takes coin, but the keeper takes lunch. Bless you.
mpjunk $o
mpexp $n 25
else
say The jar is for coin and the counter is for trade, friend.
mpput $o $n
endif
~
The coin block is a ladder, read top down: a hundred or more earns the full ceremony plus a flagged achievement, which prints its own celebration line; ten to ninety-nine earns warmth; pocket change earns the shrug, which is its own reward, since players WILL tip one copper just to hear it. Ladders like this always test the LARGEST number first; test upward from the small end and the first rung catches everything, because fifty is also more than ten. The percent header stays at 100, the amount filtering belonging in the conditions; a number in the header position would be read as a fire chance, not a minimum, a confusion the mistakes section files under its own name. Under test, the word test reads as amount zero, so you will hear the bottom rung, proof the ladder stands.
The in-kind block is Corvin's give pattern bent to hospitality: lunch is accepted, destroyed, and rewarded with a little experience, the tip-for-the-tipper; anything else is handed straight back with the house rules. The mpput return line is doing quiet load-bearing work again, keeping the jar from becoming an oubliette for misdirected swords.
Variations to try. Remember generosity: the Marsh counter on a tips-total note per player, and Toby starts greeting his patrons by their lifetime largesse. Convert the big-tip ceremony into standing perks, the trusted note from Vessa's script, so a hundred-gold tip IS the password. Or wire the jar into faction: mpfaction on the top rung, for an establishment where money talks to the whole Syndicate.
Guarding The Stock: The Cancel Pass At The Counter
Everything so far reacted to events. The message bus lets a shop REFUSE them, and the full theory lives in the bus chapter; here is the shopkeeper's working summary. A CNCLMSG_PROG block names an action code and a text mask. When the action is about to happen anywhere in the room and the mask matches, the block runs INSTEAD of the action: your script is the replacement behavior, and the action is cancelled, unconditionally, every time the header matches. No if inside the body can allow the action through; the header is the whole decision. For a merchant the three codes that matter are GET, someone lifting goods, masked against the item's key name; BUY, masked against the words the player typed after the buy command; and SELL, masked against the name of the item being offered. Hargrave, armorer, guards his window piece:
ONCE_PROG
mpoloadroom /obj/armor
emote sets a suit of display armor on the stand beside the door.
~
GREET_PROG 100
say Look all you like, $N. Touch the display and we will have words.
~
CNCLMSG_PROG GET armor
say Hands off the display, $N. Buying is done at the counter, with coin.
emote steps between $N and the stand, arms folded.
~
ONCE_PROG runs a single time when the mob loads, furnishing the scene: a real, physical suit of armor on the floor of the shop, exactly the kind of set dressing players immediately try to take. The greet warns, because a rule players discover only by tripping it feels like a trap, while a rule announced and then enforced feels like a shop. Then the veto: any attempt to pick up anything answering to armor in this room runs Hargrave's two lines instead, and the armor does not move. The would-be customer is blocked, scolded, and pointed at the legitimate path, all in the same breath, which is the veto pass at its best: not a wall, a bounce into the intended interaction.
Now the sharp edges, which on this trigger you must respect. The veto is ROOM-WIDE: the engine consults every scripted object near the action, so this block on Hargrave cancels every matching pickup in the room, and the mask is the only narrowing there is. Mask on the most distinctive word in the guarded item's name, and never on ALL, or Hargrave will confiscate every dropped coin pouch in the shop, including ones players dropped themselves seconds earlier. Which raises the second edge honestly: the mask matches NAMES, not ownership. A customer who drops their OWN suit of armor to rummage in a bag will be bounced off it by this very block, because it too answers to armor. The stock demo accepts that wart for simplicity; in a real shop, build the display piece with a distinctive key name, a dented parade cuirass say, and mask on parade, a word no adventurer's own gear is likely to carry. Distinctive names are not polish, they are how the mask aims.
The same header shape guards the other two doors of the shop. A CNCLMSG_PROG BUY block with a mask naming your never-sold item lets a vendor refuse to part with the ledger, the clock, the first coin ever earned, while selling everything else freely; the first exercise at the chapter's end builds exactly this. A CNCLMSG_PROG SELL block masked on junk you refuse to stock, torch say, bounces the item and insults its condition in one motion. One nuance on SELL: a player selling a whole batch at once is checked once, against the first item of the batch, so a picky vendor plays cleanest when players sell the contested item by itself. And the trades those vetoes cancel would have been real ones, so the messages should always explain the refusal in character; a silent cancel reads as a bug, every time.
Variations to try. Guard the whole window: several GET vetoes, one per display piece, each with its own line of outrage. Pair the veto with memory, the body writing a warned note on $n and a second identical-mask block being impossible, remember, blocks stack, so instead escalate INSIDE the one body with an if on the note, polite the first time, shouting the second. Or invert the guard entirely for a free-sample basket: no veto at all, a GET_PROG on a basket item that thanks samplers by name and counts per player how many they have taken, shaming the greedy on the third.
The Complete Shopfront: Rose Of The Saltmarsh
One merchant, every technique. Rose runs a market stall with hours, a memory, trade patter, a haggle, and a guarded lockbox: eight blocks, each one a section of this chapter in miniature. Read it top to bottom and name the pattern in each block before reading the notes below; if you can, the chapter has done its work.
ONCE_PROG
mpoloadroom /obj/container
emote sets out a battered lockbox and builds her stall around it.
~
TIME_PROG 8
mpsetvar $i rose_closed no
say The stall is open, friends. Salt, rope, and whatever the tide left behind.
~
TIME_PROG 0
mpsetvar $i rose_closed yes
emote draws a canvas over the stall and knots it down tight.
~
GREET_PROG 100
emote glances up from re-tying a bundle of rope.
if var($i rose_closed) == yes
say The stall is shut, $N. The tide trades all night, but I do not.
else
if var($n rose_known)
say Back to my corner of the market, $N? Wise. The rest is overpriced.
else
mpsetvar $n rose_known yes
say First time at my stall, I think. I am Rose. Prices firm, mostly.
endif
endif
~
BUY_PROG 100
say Wrapped and paid. If it breaks inside a week, you dropped it.
~
SELL_PROG 100
say Into the lockbox it goes. The tide gives, and the tide takes.
~
SPEECH_PROG haggle discount
if var($n rose_haggled)
say Mostly firm, I said. Today the prices are entirely firm.
else
mpsetvar $n rose_haggled yes
if rand(25)
say Once, then, because the morning was kind. A little back for you.
mpmoney $n 5
else
say Firm, as advertised. But I do admire the attempt.
endif
endif
~
CNCLMSG_PROG GET container
say That lockbox is the shop, $N. The shop is not for sale.
~
The seams worth noticing, since each block alone is familiar by now. The greeting nests BOTH memory scopes in one block, and the order is the design: shop state outermost, customer state inside, so a regular arriving after midnight gets the closed line, not the welcome-back line; the stall's reality outranks the relationship. Her haggle rate is stingier than Fetch's, twenty-five against thirty-five, and her rebate smaller, which is to say the same script IS a different character at different numbers; tuning is characterization. The first-time greeting plants prices firm, mostly, and the haggle block pays the phrase off twice, in both branches, which is what makes eight separate blocks read as one person rather than eight reflexes. And her lockbox guard masks on container, the stock item's name, carrying the same honest wart as Hargrave's display; her stall, at least, is a place customers rarely drop their own luggage.
Rose is also the script to practice DEBUGGING on, because with eight blocks the failure modes interact. Attach her, then run the whole checklist: mudprog <target> and confirm the Triggers line lists ONCE, TIME, GREET, BUY, SELL, SPEECH, and CNCLMSG progs; walk out and in for the greeting twice, stranger then regular; say haggle twice, roll then latch; test TIME_PROG and watch the canvas come down, then walk in again for the closed branch; and try to take the lockbox. Ten minutes, every technique in the chapter verified on one mob.
Testing A Shop Script
Commerce triggers meet the test harness in a few specific ways, so here is the merchant's testing card, collected from the sections above and the basics chapter.
What fires naturally on any practice mob: GREET by walking in, SPEECH by saying the keyword or phrase ALOUD, GIVE by handing the mob an item, GET vetoes by trying to take the guarded thing. Speech keyword blocks can NOT be fired by mudprog <target> test, whose stand-in message is the single word test; walking up and talking is both easier and truer.
What needs the test command on a practice mob: BUY_PROG and SELL_PROG, which only fire live on true vendors, and BRIBE_PROG, whose live hook is not yet wired. Under test remember the stand-ins: $o reads as the word something, and amounts and hours read as zero, which selects BRIBE ladders' bottom rung and TIME lists containing 0.
What persists between your tests: every note written on YOU. Haggle latches, trust notes, visit counts, they all stick to your character exactly as they would to a player's, which is the feature doing its job. Reset a note by hand when re-testing a first-time branch, for example scripttest mpsetvar $i vessa_trusted with an empty value, run by you, clears your own trust note, since $i on your own scripttest line is you. Shop-state notes live on the mob instead, and mudprog <target> clear plus reattaching gives you a factory-fresh shop, though notes survive a clear; they are on the object, not in the script. The truly fresh start is a fresh practice mob.
And the one discipline that catches most shop bugs before players do: after every attach, read the Triggers line of mudprog <target> and count the blocks you meant to write. A missing tilde merges two blocks into one and the Triggers line is where the second one fails to appear.
Common Shopkeeper Mistakes
Seven mistakes account for nearly every broken merchant. Each with its symptom, its diagnosis, and its fix.
Mistake one: the greedy mask. A stock guard written with a broad word, CNCLMSG_PROG GET ring say, bounces customers off earrings and drinking horns, because masks match by substring and ring hides inside both names. Symptom: players report being unable to pick up their own belongings in the shop. Diagnosis: read the mask and imagine every item name that could contain it. Fix: mask on the most distinctive word the item answers to, give guarded stock distinctive key names when you build it, and never guard with ALL.
Mistake two: deciding inside a veto. A CNCLMSG_PROG BUY block containing if level($n) < 10, meant to refuse only novices, refuses EVERYONE whose typed words match the mask; the if merely chooses which refusal speech plays. Symptom: an intended sometimes-rule enforces always. Diagnosis: reread the cancel rule, the header is the whole decision. Fix: if the rule is who-based, move it out of the bus entirely, onto masked triggers or scripted alternatives like the give economy, and keep vetoes for always-rules about named things.
Mistake three: crossed trade directions. A builder writes SELL_PROG expecting it to fire when the shop sells, that is, when a player buys. Symptom: thank-you lines fire on the wrong half of every transaction. Diagnosis: one test of each trigger with the room watched closely. Fix: memorize the convention, both trigger names and both bus codes are named from the PLAYER'S side; the player buys in BUY_PROG and sells in SELL_PROG.
Mistake four: a number where a mask should be, or the reverse. The header GREET_PROG 30 hoping for a level gate is a thirty percent chance; the header BRIBE_PROG 100 meaning a hundred-copper minimum is a hundred percent chance, which happens to work until the day it is edited to 50 and the jar starts ignoring half the gifts. Symptom: a gate that leaks randomly. Diagnosis: any bare number in a header position is a chance, always. Fix: gates on WHO use dash-word zapper masks, gates on AMOUNTS use conditions in the body, and only fire-chance ever goes in the header as a number.
Mistake five: the unlatched giveaway. Any block that pays, gifts, or rebates without first writing and checking a per-player note is an infinite money faucet, and players find faucets within the hour. Symptom: one character walking in and out of the shop repeatedly. Diagnosis: look at every mpmoney, mpoload, and mpexp line and ask what stops the second helping. Fix: the latch idiom, check the note, write the note BEFORE the roll or the payout, exactly as Fetch does, and cap repeatable generosity with a count or a stock ceiling like the apprentice's.
Mistake six: trusting the door to guard the counter. Ilbrin's masked greetings choose who hears the pitch, and a builder assumes the matching GIVE_PROG or SPEECH_PROG therefore only fires for the same crowd. Every trigger fires for whoever performs its event; masks on one block protect that block only. Symptom: unqualified players reaching the qualified outcome by skipping the conversation. Diagnosis: test the transaction directly with a character who should be refused. Fix: re-check qualifications in the condition of every block that PAYS OUT, the way Tam's give block re-asks class and level; let the greetings be theater and the transactions be law.
Mistake seven: testing speech with the test command. Covered on the testing card, and it still earns its place here, because every scripter loses twenty minutes to it once: a keyword speech block tested with mudprog <target> test SPEECH_PROG stays silent, the word test containing none of the keywords. The block was never broken. Say the words out loud.
Exercises
Three shops to build yourself. Each has a worked solution below it, but the learning is in attempting first: write the script, attach it, break it, and only then compare. Every solution uses only techniques from this chapter.
Exercise one. The unsold heirloom: a vendor sells everything on the shelves except one item, the shop's old clock, which no offer can buy. Refusals should come with the story of why. You need one greeting and one veto, and the veto's mask does the aiming.
GREET_PROG 100
say Everything on these shelves is for sale, $N. The clock behind me is not.
~
CNCLMSG_PROG BUY clock
say The clock was my mother's, and her mother's before that. It keeps the shop's time, not yours.
~
The BUY code's mask matches the words the player types, so buy clock, buy the clock, and buy old clock all bounce into the story, while every other purchase proceeds untouched. The greeting plants the mystery so players try, which is the point; an unbuyable item nobody attempts to buy is just scenery. Note what the veto does NOT need: no condition, no variable, no second block. The header is the rule.
Exercise two. Market day: on the first day of each mud month, the shop greets everyone with a half-price announcement; every other day, the ordinary welcome. One block, one calendar function, and remember the layering lesson: the ordinary welcome should not vanish on market day.
GREET_PROG 100
say Welcome back to the Copper Kettle, $N.
if datetime(day) == 1
say It is the first of the month! Everything I regret stocking is half price.
endif
~
datetime(day) reads the mud calendar's day number, and the comparison gates the announcement without touching the base welcome, so day one customers hear both lines in order. The half price is, as this chapter has said three ways now, a promise the script keeps through rebates or gifts if you extend it, or pure market theater if you do not; either way the town crier effect, players mentioning it to each other, is real.
Exercise three. The loyalty ledger: a vendor counts what each customer sells to the shop, announces the running count with each trade, and on the third trade rewards the seller with a meal from the back. The Marsh counter, moved onto SELL_PROG, plus the load-and- hand-over idiom.
SELL_PROG 100
if var($n kettle_sales == '')
mpsetvar $n kettle_sales 0
endif
mpargset 1 $%MATH($<$n kettle_sales> + 1)%
mpsetvar $n kettle_sales $1
say That makes $1 you have brought me, by my ledger.
if var($n kettle_sales) == 3
say Three trades makes a regular here. Regulars eat free, once.
mpoload /obj/meal
mpput $b $n
endif
~
All the idioms in one block: the empty-check seeds the counter, the MATH line increments it through slot $1, the say announces it, and the reward gate compares with == 3, not >= 3, so the meal happens exactly once, on the third trade, and never again; the counter keeps climbing but never again equals three. That one operator choice is the entire anti-farming design, and it is worth a moment's admiration before you move on: the right comparison is often cheaper than a latch.
Where To Go Next
Your shopkeepers can now remember, refuse, gossip, gate, charge, and close for the night. The techniques transfer whole to every other archetype: the guard is a shopkeeper whose stock is a doorway, the questmaster is a shopkeeper who pays in tasks, the innkeeper is a shopkeeper with beds. For the full trigger catalog read help mudprog-triggers; for the whole veto system, help mudprog-bus; for every command and function the merchants above used, help mudprog-commands and help mudprog-functions; and for fifteen more complete characters across every archetype, help mudprog-cookbook. Then go build a shop your players argue with on purpose.
Every mud that has ever mattered has a guard in it. A bored halberdier at a city gate, a toll collector squeezing coppers out of caravans, a bouncer with opinions about your boots, an honor guard whose whole job is to stand very still next to someone important. Guards are the first NPCs players push against, because a guard is a rule with a face: here is a line, and here is the person who decides whether you cross it.
That makes guards the perfect archetype for a deep scripting chapter. A good guard uses almost everything MUDProg has: triggers that watch arrivals, vetoes that stop movement and violence before they happen, speech blocks that hold conversations, prompts that ask questions, variables that remember faces, timers that change the watch, and alarms that wake the neighborhood. If you can build the guards in this chapter, you can build nearly anything.
This chapter assumes you have read the basics chapter, so you know what a PROG block is, that every block ends with a line holding only a tilde, and that dollar codes like $n and $N are swapped for live values when a line runs. It leans heavily on the message bus, so if you have not read that chapter yet, read at least its first three sections; the veto trigger CNCLMSG_PROG is the spine of everything here. Everything else is explained as it appears, from zero, and every script is complete: attach it with the mudprog command exactly as printed and it works.
One promise before we begin, the same promise the rest of the guide makes: none of this can break the mud. A guard script with a mistake in it does less than you hoped, never more. A broken veto lets people through rather than sealing them in forever. Experiment freely.
The Five Duties Of A Guard
Strip away the armor and every guard ever written does some mix of five jobs, and each job maps onto one part of the scripting engine:
The first duty is to challenge: notice an arrival and state the rule. That is GREET_PROG, which fires about a second after a player walks into the guard's room, with the arrival in $n. A guard who says nothing until you bump into his rule feels like a wall; a guard who tells you the rule first feels like a person enforcing it.
The second duty is to judge: decide whether this particular person passes. Judgments live in if lines, asking questions with functions like level($n), class($n), goldamt($n), has($n item), and var($n note), or in zapper mask headers that filter who can set a trigger off at all.
The third duty is to bar: actually stop the action. That is the message bus veto, CNCLMSG_PROG, on the codes ENTER, LEAVE, ATTACK, and GET. A veto block runs INSTEAD of the action it matches, so the walk-in, the escape, the sword swing, or the theft simply never happens, and whatever your block prints is what everyone sees in its place.
The fourth duty is to punish: give rule-breaking a cost. The commands mpdamage, mpcondition, and mptransfer let a guard bruise knuckles, root ankles, stun heads, and march people to cells.
The fifth duty is to remember: hold a grudge, honor a payment, recognize a face. Variables written with mpsetvar and read with the var() function give a guard a memory, and where you store the note decides whether that memory survives the guard's own death.
Layered on top of all five are two flourishes that turn a lone mob into a garrison: the clock, through TIME_PROG watch rotations, and the alarm, through mpasound and its cousins, which let one fight at one gate be heard three rooms away.
The rest of this chapter builds guards that do these jobs one at a time, then in combination, ending with a complete gate sergeant who does all five at once.
Ground Rules: How A Barrier Actually Works
Four facts from the message bus chapter matter so much here that they are worth restating in guard terms before the first script.
First, a matching veto always cancels. When someone tries to walk into a room and a CNCLMSG_PROG ENTER block is in scope, the walk-in dies the moment the block matches, and the block's body runs in its place. There is no allow command inside the body. An if inside a veto cannot wave the action through; it can only choose what happens instead. Every open door you will build in this chapter is built the same way: the veto refuses everyone, and for the people the guard approves, the body performs the crossing itself with mptransfer, which moves people directly, underneath the bus, and therefore cannot re-fire the veto that called it.
Second, always narrate the refusal. The action your veto replaced would have printed its own messages; those are gone now. If your block says nothing, the player types north and the world goes silent, which reads as a bug, not a border. Every veto body should contain at least an mpechoat $n telling the refused person what stopped them. The refusal is content; write it like content.
Third, ENTER and LEAVE carry no message text. For most bus codes the header can carry a mask matched against the message text, the way CNCLMSG_PROG GET relic only vetoes takings of relics. Movement messages have no text, so a mask on ENTER or LEAVE can never match and the block simply never fires. The door you thought you sealed stands wide open. Write movement vetoes with a bare code, CNCLMSG_PROG ENTER, nothing after it, and do all your narrowing inside the body.
Fourth, think about creatures, not just players. The ATTACK code fires for aggressive monsters as well as players, which is lovely: a peace-ward calms beasts too. But ENTER fires for wandering mobs as well, and a wandering mob cannot read your refusal text or say a watchword. Every movement veto in this chapter starts with the same two-line courtesy:
if isnpc($n)
return
which quietly turns creatures back where they stood, no message, no pileup of confused wolves in front of your gate, and no wolf ever locked inside it either. The isnpc($n) function is true when the mover is a creature rather than a player, and return ends the block on the spot. You will see the pair so often below that your eye will learn to skip it.
One honest hole to know about: a player being dragged along automatically because they are following a group leader is not re-checked by the bus. The leader was checked, and the group moves with the leader. A determined party can therefore carry a passenger through your gate by having only the qualified member lead. For most gates that is flavor, not a flaw; for an absolutely sealed door, remember it exists.
The Geometry Lesson: Where A Guard Stands
This is the section that separates working guard posts from mysterious broken ones, so take it slowly. When someone tries to walk from one room to another, the engine asks two questions: first LEAVE, checked against the room being left and everything scripted standing in it, then ENTER, checked against the destination room, everything scripted standing in the destination, AND everything scripted standing in the room being left. Read that last clause again, because it is the trap: a guard carrying a bare CNCLMSG_PROG ENTER block is consulted about every entry into his own room and ALSO about every move his roommates make into any neighboring room, because for those moves he is standing in the room being left.
In plain terms: a bare ENTER veto does not seal a doorway. It seals the guard's position, in every direction at once. Nobody walks in past him, and nobody standing with him walks out either, because walking out means entering a neighboring room, and he is in scope for that too.
There is no dollar code that tells the veto body which room the mover was heading for, so the body cannot say "ah, you were only leaving, go ahead". And two more room-bound facts squeeze the design space further: a guard only HEARS speech spoken in his own room, so watchwords and toll payments must be spoken standing next to him, and items can only be GIVEn to him by someone in his room as well.
Out of those constraints come exactly three postures that work, and every guard in this chapter stands in one of them:
The sealed door. The guard stands inside a room nobody is ever admitted to. His bare ENTER veto turns everyone away, and since nobody is ever inside with him, the seals-in-every-direction behavior costs nothing. This is the crypt warden, the first script below.
The veiled dead end. The guard stands inside a dead-end room and admits the worthy by judgments he can make at a distance, things like level, class, carried items, and stored notes, none of which require hearing the visitor. Admission is performed by the veto body itself with mptransfer $n here, and because his veto also catches admitted guests walking back out, departure happens another way: the guest speaks a leaving word, which the guard can hear since they now share a room, and a speech block transfers them out. This is the shrine guardian.
The steering post. The guard stands in a small lobby, a gatehouse or tollbooth, and his ENTER veto steers every mover, inbound or outbound, gently back to his own room, so that everyone who touches his gate ends up standing in front of him where conversation works. Nobody ever walks PAST him in any direction; instead, he escorts approved travelers through personally, with speech-triggered mptransfer lines to the rooms on either side. This is the watchword keeper, the tollkeeper, and the border interrogator.
If you remember one sentence from this chapter, make it this one: a scripted guard never opens a door, he carries people through it. Keep that in mind and the geometry always works out.
Script One: The Crypt Warden, A Sealed Door
The simplest guard there is: a door that is now a person. He stands inside the sealed crypt, alone, forever, and nobody enters. Attach this to a mob standing in the room you want sealed:
LOOK_PROG 100
emote stares back, unblinking, through the slit of a rusted helm.
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n A halberd swings down across the crypt door. The warden bars your way.
mpasound From the crypt mouth a voice like a closing tomb declares that none may enter.
~
Walk through it. The LOOK block is a single emote for anyone who studies him, there mostly for you while testing, since in live play nobody can reach the room to look; a sealed room's guard performs for an empty house, and that is fine. The veto block is the actual seal. The isnpc pair turns wandering creatures back silently. For players, two lines of narration do the real work, and notice WHERE each one lands, because a sealed-room guard has an audience problem: the refused player is standing in the room OUTSIDE, so mpecho, which prints to the guard's own room, would play to nobody. mpechoat $n reaches the refused player directly, wherever they stand, and mpasound prints to every room one exit away from the guard, which is exactly where the player and any watching friends are. That pairing, a private line for the refused and an adjacent-room line for the crowd, is the standard voice of an inside guard.
What if you attach this and the door still opens? Three usual suspects. Check mudprog warden and read the Triggers line; if CNCLMSG_PROG is missing, you lost a tilde. Check the header; a mask after ENTER, even the word ALL is safe but any other word is not, means the block can never match. And check that the script is on a mob actually standing IN the protected room, not next to it.
What if you want the seal to hold against creatures with a message too? Replace the return with narration of its own. What you cannot do is skip the isnpc question and let creatures hit the player branch: they will, harmlessly, but your crypt mouth fills with wolves reading refusal text they cannot understand.
Judging At A Distance
The veiled dead end and everything after it depends on one realization: even though an inside guard cannot HEAR an outsider, he can still know a great deal about them, because condition functions reach across the threshold. At the moment an ENTER veto runs, $n is the would-be enterer, wherever they stand, and every question in the functions chapter can be asked about them:
level($n) >= 20 - are they seasoned enough?
class($n) == mage - the robed orders only.
race($n) == dwarf - kin of the deep roads.
has($n armor) - are they carrying a thing by that name?
goldamt($n) >= 10 - can they afford the toll?
var($n gate_pass == 1) - did some other script vouch for them?
That last one is the quiet powerhouse. A note written on the PLAYER with mpsetvar $n by any script anywhere, a questmaster, a priest, a colleague standing outside, travels with the player to your door, and your veto reads it with var(). Passes, tolls, vouchers, and blacklists are all just notes on players, and the second half of this chapter is largely about who writes them and when.
Script Two: The Shrine Guardian, A Veiled Dead End
A members-only room, where membership is something the guard can judge at a distance: experience. The guardian stands inside a dead-end shrine and admits only level twenty and up. Attach to a mob in the shrine room:
GREET_PROG 100
say Be welcome among the honored, $N. Speak the word farewell when you wish to leave.
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
if level($n) >= 20
mpechoat $n The guardian measures you with one long look, then draws you through the veil.
mptransfer $n here
else
mpechoat $n The guardian does not move. The veil stays shut to the unseasoned.
endif
~
SPEECH_PROG p farewell
say Go well, $N. The road remembers its own.
mptransfer $n /realms/yourrealm/yourarea/rooms/outside
~
The veto is where the door lives. Everyone who tries to walk in is cancelled, always, because that is what a matching veto does; then the body sorts them. The worthy get a private line and then mptransfer $n here: the word here resolves to the room the guardian is standing in, so the transfer performs the entry the veto just refused. Transfer rides below the bus, so the veto does not fire again on it; the player simply arrives. About a second later the GREET block fires, because a player just entered the guardian's room, and the greeting doubles as the welcome ceremony plus the exit instructions. The unworthy get the other branch and stay outside where they were.
Now the subtle part, the reason this posture is called a veil. Suppose an admitted veteran tries to WALK back out. Walking out means entering the outside room, the guardian is in scope for that, and his veto fires. The body asks its one question, level, the veteran passes it, and gets drawn through the veil again, right back inside. Strange? Write the fiction so it is not: this veil only opens inward, and the way out is the word. The admit line above, drawn through the veil, reads correctly in both directions, which is deliberate. When you write your own dead-end guard, make the admit narration direction-neutral and the design carries itself.
The way out is the speech block. The header p farewell is the phrase form of the keyword mask: it fires when a spoken line contains the word farewell. The guest is inside now, sharing the guardian's room, so the guardian hears them, answers, and transfers them out to a room you name by file path. The path printed above is a placeholder; replace it with the real path of the room outside your own shrine door. Until you do, the transfer quietly degrades to leaving the guest where they stand, which is safe but silly, so set the path before you ship. Getting a room's real file path is one whereami while standing in it.
What if two rules should apply, say level AND class? Chain the questions: if level($n) >= 20 AND class($n) == warrior, one line, both must pass. What if you would rather vary the refusal by how close they came? Nest another if in the else branch and give level fifteen a kinder line than level five. The veto always cancels either way; you are sculpting the experience, not the permission.
The Steering Post
The veiled dead end works because its judgments need no conversation. The moment your gate needs SPEECH, a watchword, a toll paid on the spot, an interrogation, the visitor must be standing in the guard's room to be heard, and the geometry flips inside out: instead of keeping visitors OUT of his room, the guard now pulls everyone INTO it, and the protected territory moves one room deeper, reached only by his escort.
The whole trick is one strange, wonderful veto body:
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n The keeper takes your arm and walks you to his post by the arch.
mptransfer $n here
~
Read what this does. ANY player movement that touches the guard's room, walking into it from the street, or standing in it and trying to walk deeper, or standing in it and trying to walk back out, is cancelled and replaced with being walked to the post. Strangers who approach end up in front of the keeper. Guests who try to stroll past him end up in front of the keeper. Nobody is ever stuck anywhere except in conversation range of the one NPC who can move them along, and moving them along is what his speech blocks are for: each approved destination is one mptransfer $n /path behind one spoken phrase. The lobby becomes a little airlock with a person in the middle, which is exactly what a gatehouse is.
Script Three: The Watchword Keeper
The classic challenge and response, built on the steering post. Attach to a mob in a small gate-lobby room, and replace the two file paths with the rooms on either side of your gate:
GREET_PROG 100
say The Emberhall opens to the watchword alone, $N. Say the word and I walk you in. Say let me out and I walk you back to the street.
~
SPEECH_PROG p cold iron keeps faith
say The word is good. Through you go.
mptransfer $n /realms/yourrealm/yourarea/rooms/emberhall
~
SPEECH_PROG watchword password
say The word is not given here. It is earned in the Emberhall's service.
~
SPEECH_PROG p let me out
say Mind the step going down.
mptransfer $n /realms/yourrealm/yourarea/rooms/street
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n The keeper takes your arm and walks you to his post by the arch.
mptransfer $n here
~
The greeting fires for everyone the veto has just steered in, so the rules are always announced; a challenge nobody explains is a puzzle, and gate puzzles are only fun when they are advertised. The first speech block is the watchword itself, in phrase form: the line must contain the whole phrase cold iron keeps faith, together and in order, and when it does the keeper escorts the speaker straight through. The second speech block is a courtesy: it keys on the words watchword or password, so a player asking about the word gets an in-character pointer toward earning it instead of silence. The third is the exit phrase. And the veto is the steering post, word for word from the section above.
Three honest cautions about watchwords. First, keyword matching is by substring, so choose exit phrases with care: a block headed p back would fire on give my gold back. Multi-word phrases like let me out are safe precisely because they are unlikely to occur by accident. Second, speech is public. Everyone standing in the lobby hears the watchword spoken, and there is nothing stopping them speaking it themselves five seconds later; that is realistic, watchwords leak, and if it bothers you, rotate the phrase by storing it in a variable on the keeper and comparing with var() in the body of a broader speech block. Third, the spoken line arrives in $g lowercased, and phrase masks match case-insensitively, so COLD IRON KEEPS FAITH bellowed in capitals works fine; you never need to worry about case in watchwords.
What about the visitor who tries the word from OUTSIDE the lobby? Nothing happens, and now you know exactly why: the keeper cannot hear through walls. The steering veto guarantees they can always reach the one room where speaking works.
The Interrogation: mpprompt
Sometimes a fixed phrase is the wrong shape and you want the guard to ask an open question and judge the typed answer: who sent you, what is your business, name the second oath. The tool is mpprompt, and it needs a little ceremony to use well, so here is how it works before the script uses it.
mpprompt <text> prints the text to the triggering player as a question and then quietly swallows that player's NEXT typed line. The line never reaches the command parser, is never spoken aloud, and nobody else in the room sees it; it lands in a variable named prompt_answer stored on the player. That is the whole capture. Three consequences follow. One: the script cannot stand there waiting for the answer; scripts do not pause for input. Instead you set an appointment with mpalarm <seconds> <command>, which runs one script line later, and the appointed line calls a named routine, a FUNCTION_PROG block, to do the judging. Two: because the capture takes the player's next line whatever it is, fire the prompt only in response to something the player deliberately did, like speaking a request, never spontaneously from a greeting, or you will eat a command they meant for the game. Three: always erase the old answer before asking, with a bare mpsetvar $n prompt_answer, a set with no value, so a stale answer from last week cannot pass this week's question.
Its sibling mpconfirm <text> asks a yes-or-no question the same way and stores a clean yes or no in confirm_answer; use it for oaths and warnings where a free answer is more than you need.
Script Four: The Border Interrogator
A steering-post guard who grants passage on a judged answer, remembers liars, and holds the grudge in his greeting. Replace the far-side path with your own:
GREET_PROG 100
if var($n bi_liar == 1)
say You lied to the Redmarch watch once, $N. Words will not open this road again.
else
say Nothing crosses the Redmarch line unquestioned, $N. Say state my business when you are ready to answer.
endif
~
SPEECH_PROG p state my business
mpsetvar $n prompt_answer
say Then answer plainly.
mpprompt Who sent you to the Redmarch line?
mpalarm 15 mpcallfunc bi_judge
~
FUNCTION_PROG bi_judge
if var($n prompt_answer == '')
say Silence. Silence walks home the way it came.
else
if strin(captain $%var($n prompt_answer)%)
mpsetvar $n bi_pass 1
say The captain's name carries here. You may cross once.
else
mpsetvar $n bi_liar 1
say No such master holds this road. I will remember that answer, $N.
endif
endif
mpsetvar $n prompt_answer
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
if var($n bi_pass == 1)
mpsetvar $n bi_pass 0
mpechoat $n The interrogator lifts the bar and waves you across the line.
mptransfer $n /realms/yourrealm/yourarea/rooms/farside
else
mpechoat $n The interrogator walks you back to the barrier post. Answer for yourself first.
mptransfer $n here
endif
~
Follow one traveler through. They approach the line; the veto steers them to the barrier post; the greeting states the procedure. They say state my business; the speech block erases any stale answer, asks the question through the prompt, and books the judging for fifteen seconds out. The traveler types their answer, which vanishes silently into prompt_answer. When the alarm lands, bi_judge runs and reads the answer back with var(). Comparing to two quote marks, the empty value, catches the traveler who typed nothing in time. Otherwise the judging uses strin(), which asks whether one piece of text occurs inside another: here, whether the word captain occurs anywhere in the captured answer, so the captain sent me and captain Ardis passes both work. That generosity is deliberate; exact matching punishes phrasing, and interrogations should judge content. A good answer writes the pass note bi_pass on the player; a bad one writes the liar note instead, which the greeting reads forever after. Either way the routine erases prompt_answer so the next interrogation starts clean.
The pass is spent on use: the veto's approved branch sets bi_pass back to 0 before escorting, so one good answer buys one crossing. Delete that line and the pass becomes permanent. And notice the approved branch escorts to the FAR side by path, while the refused branch steers back to the post; on a steering guard the veto is also the escort desk.
One design honesty note: the liar flag here is a life sentence, since no script ever clears it. Guards with long memories are wonderful exactly until they are unfair; consider a redemption path, a speech block with an apology phrase that erases the note, like the ledger keeper later in this chapter, or a fee that clears the record. Punishment you can climb out of is content; punishment you cannot is a support ticket.
Tolls And Bribes
Money gates run on two functions and one command. goldamt($n) reads how much gold the player carries; mpmoney $n -10 takes ten gold, a negative amount charges, a positive one pays out. The iron rule of every till in every game ever written: CHECK, then CHARGE, then GRANT, in that order, inside the same block. Check goldamt first so you never charge a purse that cannot pay; charge before you write the paid note so a failure cannot leave a free pass behind.
Bribery has a dedicated trigger, BRIBE_PROG, which is reserved for coins given to the scripted mob, with the amount riding in $g as a number. Be aware of its honest status on this mud today: the live coin-giving path does not yet call the hook, so a BRIBE_PROG block currently fires only under mudprog <target> test. Write the block anyway, shaped correctly, and your guard's palm is ready the day the wiring goes live; the shape costs you nothing and tests fine. Because the amount arrives as text in $g, the block reads it with number($g), and because a number in the HEADER would mean a percent chance, amount checks always happen in the body, never the header.
Script Five: The Wallgate Tollkeeper
A full steering-post toll gate: pay at the window, then be escorted through the wall, with a bribe block waiting for the future. Replace both paths with the rooms on either side of your wall:
GREET_PROG 100
say Gate law is simple, $N. Say pay the toll for passage. Say onward when you are paid up, or say turn back to return the way you came.
~
SPEECH_PROG p pay the toll
if var($n wt_paid == 1)
say Your toll is paid already. Say onward when you are ready.
else
if goldamt($n) >= 10
mpmoney $n -10
mpsetvar $n wt_paid 1
say Ten gold, counted twice. Say onward when you are ready.
else
say Ten gold opens the wall, and your purse is short of it.
endif
endif
~
SPEECH_PROG p onward
if var($n wt_paid == 1)
mpsetvar $n wt_paid 0
say Through with you, then. Mind the portcullis chains.
mptransfer $n /realms/yourrealm/yourarea/rooms/inside_the_walls
else
say Paid travelers go onward. Say pay the toll first.
endif
~
SPEECH_PROG p turn back
say No charge for turning around. Off you go.
mptransfer $n /realms/yourrealm/yourarea/rooms/outside_the_walls
~
BRIBE_PROG 100
if number($g) >= 50
mpsetvar $n wt_paid 1
say The keeper's hand closes and his ledger stays shut. Say onward whenever you like.
else
say That barely buys a glance, friend. The toll is the toll.
endif
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n The keeper takes your elbow and walks you to the tollbooth window.
mptransfer $n here
~
The payment block is the check-charge-grant rule verbatim: the outer if spares repeat payers, the goldamt check guards the till, the charge lands before the note is written. The onward block spends the note and escorts; the turn back block escorts free of charge, because a toll gate that charges you to give up is a grievance generator. And the bribe block shows the amount-in-the-body pattern: fifty gold or more greases the gate, anything less is dismissed. Notice bribery here is strictly better than the toll only in dignity; price your corruption deliberately.
The keeper never opens anything. He is an airlock valve made of speech blocks, and each destination costs exactly one line. Want a third exit, up onto the wall walk for guardsmen only? One more speech block, one more path, one class($n) check. The pattern scales sideways forever.
Script Six: The Mine Warden, Item Tolls And Carried Passes
Two more ways to pay that are not coins: what you carry, and what you hand over. The functions differ in reach, and the difference decides the design. has($n armor) checks whether the player is CARRYING a thing answering to a name, and like all condition functions it works at any distance. GIVE_PROG, the trigger that fires when a player hands the scripted mob an item, requires the giver to be in the guard's room, same as speech, so gift-tolls belong on steering posts and dead-end insiders must rely on carried checks. This warden, a steering post at a mine mouth, uses both:
GREET_PROG 100
say None walk the undergalleries bare, $N. Carry armor, or hand me a meal for a standing pass. Say descend to go down. Say surface to go up.
~
GIVE_PROG 100
if isname($o meal)
mpsetvar $n mw_pass 1
say Provisions for the deep watch. Your pass stands, $N.
else
say A kind thought, but only a meal buys my pass.
endif
~
SPEECH_PROG p descend
if has($n armor)
say Keep that armor on your back down there.
mptransfer $n /realms/yourrealm/yourarea/rooms/undergallery
else
if var($n mw_pass == 1)
say Your name is on my slate. Down you go.
mptransfer $n /realms/yourrealm/yourarea/rooms/undergallery
else
say Not bare, you do not. Armor or a meal first.
endif
endif
~
SPEECH_PROG p surface
say Watch the loose stone on your way up.
mptransfer $n /realms/yourrealm/yourarea/rooms/tunnel_mouth
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n The warden hooks your collar and walks you back to his lantern post.
mptransfer $n here
~
The give block runs whenever ANY item is pressed into the warden's hands, with the item in $o, and isname($o meal) asks whether that item answers to the name meal. Test with the stock item /obj/meal, cloned with mpoloadroom /obj/meal or a builder command, then handed over. Two honesty points about gifts. First, the item genuinely changes hands; the warden is now holding it, and the script above lets him keep it whatever it is, which is why the wrong-gift line is written as him pocketing nothing and promising nothing rather than pretending to hand it back. If you script a refusal, keep the fiction matched to the mechanics or players will hammer on the seam. Second, the trigger has no text rider, so you cannot filter gifts in the header; the isname() question in the body is the filter.
The descend block stacks the two currencies: armor on your person passes you outright, and the standing pass from a meal covers the rest. Note the pass here is NOT spent, no line sets mw_pass back to 0, so one meal buys a lifetime of descents. Spent or standing is always one line of difference, and it is worth deciding on purpose every time.
Protecting People: The ATTACK Veto
The ATTACK code fires at the moment combat is about to BEGIN, when a player types the attack command or an aggressive creature pounces. Veto it and the fight never starts; the script's text replaces the opening blow. Two facts shape guard design here. First, scope: the intended victim is the primary object of an ATTACK, so a veto ON the victim protects them everywhere they go, while a veto on a guard or a room protects only that room. Second, the message text of an ATTACK is the victim's key name, which means a room or guard CAN protect one resident by name with a masked header like CNCLMSG_PROG ATTACK curator, leaving everyone else fair game. Remember the veto stops fights STARTING; a brawl that began elsewhere and spilled in is already running.
Script Seven: The Honor Guard
An unattackable ceremonial guard whose patience has edges. The veto is on the guard himself, ATTACK with the mask ALL, so every attempt to start a fight with him is smothered, and the body runs an escalation ladder instead:
GREET_PROG 100
say Stand easy, $N. The First Blade guards this hall, and the First Blade is not to be touched.
~
LOOK_PROG 100
emote holds parade stillness, eyes fixed on the middle distance.
~
CNCLMSG_PROG ATTACK ALL
if !var($n hg_strikes)
mpsetvar $n hg_strikes 0
endif
mpsetvar $n hg_strikes $%math($<$n hg_strikes> + 1)%
if var($n hg_strikes == 1)
mpechoat $n The First Blade catches your wrist without looking at you. Do not try that again.
else
if var($n hg_strikes == 2)
mpdamage $n 10 blunt
mpechoat $n The flat of the First Blade's sword cracks across your knuckles.
else
mpdamage $n 10 blunt
mpcondition $n stunned debuff 8
mpechoat $n The pommel takes you behind the ear and the hall spins.
endif
endif
~
The counter idiom in the middle is the single most reused four lines in guard scripting, so read it once carefully and then recognize it forever. The bare !var($n hg_strikes) is true when the note has never been written, so the first two lines seed it to 0 exactly once. The third line increments it: the angle form $<$n hg_strikes> substitutes the current value into the text, math() adds one, and mpsetvar files the result back. After those four lines the note holds exactly how many times THIS player has tried it, and the ladder below branches on the count: first attempt costs a warning, second costs ten blunt damage, third and beyond adds an eight-second stun. The count lives on the player, so it survives the guard being replaced and follows the offender around; there is no statute of limitations unless you write one, say a speech block with an apology that erases the note.
Because the attacker is $n and the veto replaced the fight entirely, the guard never actually enters combat; he disciplines without brawling, which is precisely the fantasy of an honor guard. And since ATTACK also fires for monsters, a wandering ghoul that lunges at him eats the same wrist-catch, harmlessly.
Script Eight: The Market Watchman, Guarding Property
Property guards veto GET. The scope rule to know: a guard's GET veto covers every pickup in his room, which for a market stall is the point, but notice it also covers players picking up things they themselves just dropped; nothing leaves the floor of this stall, period. To protect only particular goods, mask the header with words from their key names, the way CNCLMSG_PROG GET pendant would guard only the jewelry.
GREET_PROG 100
say Browse all you like, $N. This stall watches back.
~
CNCLMSG_PROG GET ALL
if !var($n mk_grabs)
mpsetvar $n mk_grabs 0
endif
mpsetvar $n mk_grabs $%math($<$n mk_grabs> + 1)%
if var($n mk_grabs == 1)
mpechoat $n The watchman's cudgel taps your knuckles away from the goods. Fair warning, the only one.
else
mpdamage $n 8 blunt
mpechoat $n The cudgel comes down hard across your reaching hand.
mpecho The watchman barks that thieves lose fingers in this market.
endif
~
EXECMSG_PROG DROP ALL
emote nods approvingly at goods returned to the ground where they belong.
~
Same counter, same ladder, different code. The third block is new: an EXECMSG_PROG observer, the watching half of the bus, which runs AFTER a matching action goes through normally. Drops are allowed at this stall, observed and approved; only takings are vetoed. Pairing a veto on one code with an observer on its mirror code gives a guard a worldview instead of just a rule.
Test this one with the stock props: mpoloadroom /obj/torch puts a harmless item on the floor to grab at.
The Punishment Ladder
Every enforcement command a guard owns, in one place, roughest first.
mpdamage <who> <amount> <type> deals direct damage. The types are blunt, cutting, thrusting, pierce, fire, cold, lightning, and magic if you leave it off. Guards mostly want blunt, the flat of the blade, and mostly want small numbers: ten points is a spanking to anyone worth guarding a gate against, and a spanking is usually the right sentence. A guard script cannot be appealed, so sentence like a judge, not a rival.
mpcondition <who> <id> <type> <seconds> applies a real condition from the game's own catalog, the same ones combat abilities use. The ids a guard reaches for: rooted pins the target in place, no movement; stunned locks them up briefly; knockdown floors them; mute silences speech, poetic justice for password shouters. The type argument for punishments is the word debuff, and seconds is the duration. Short durations, eight to twenty seconds, read as discipline; long ones read as griefing. The simpler cousin mpaffect <who> <id> <seconds> does the same with fewer knobs. And one warning that has embarrassed every builder once: conditions apply to staff too. When you test your own turnkey and he roots you, you are rooted; that is the script working.
mptransfer <who> <room> is the heaviest sentence: march them somewhere. You have been using it all chapter as an escort; pointed at a cell path it becomes an arrest. Two design rules keep transfer punishments fun. Never teleport someone as the FIRST consequence; walk the ladder, warn, then hurt, then move, so the player authored their own arrest. And never transfer to a room they cannot leave by some means, even a slow one; that is what the next script is about.
Below all of these sits the mildest and most underrated punishment: the guard remembering. A cold greeting next visit costs a player nothing and stings more than damage. The ledger keeper at the end of this chapter is built entirely out of that sting.
Script Nine: The Turnkey, A Leave Veto
The LEAVE code is checked against the room being left and everything in it, which makes it the tool for keeping people IN: cells, quarantines, locked taprooms at closing time. A guard carrying a bare LEAVE veto holds everyone in his room, full stop, and unlike ENTER there is no clever transfer that lets the body wave approved leavers onward, because the body has no idea which exit they wanted. So conditional release from a cell always goes the other way around: the veto holds everyone, and a separate block, usually speech, performs the release by transfer. Which is, if you think about it, just what a real turnkey is: the door only opens from his side.
GREET_PROG 100
say Settle in, $N. The cell keeps what the crown gives it. Three honest words of regret might yet move me.
~
CNCLMSG_PROG LEAVE
if !var($n tk_tries)
mpsetvar $n tk_tries 0
endif
mpsetvar $n tk_tries $%math($<$n tk_tries> + 1)%
if var($n tk_tries == 1)
mpechoat $n The turnkey swings his ring of keys once. The door does not move for wanting.
else
if var($n tk_tries == 2)
mpcondition $n rooted debuff 15
mpechoat $n The turnkey drives you back and irons your ankle to the floor ring.
else
mpdamage $n 8 blunt
mpcondition $n rooted debuff 15
mpechoat $n The keys catch you across the jaw as the turnkey hurls you back inside.
endif
endif
~
SPEECH_PROG p i repent
if !var($n tk_sorry)
mpsetvar $n tk_sorry 0
endif
mpsetvar $n tk_sorry $%math($<$n tk_sorry> + 1)%
if var($n tk_sorry >= 3)
mpsetvar $n tk_sorry 0
mpsetvar $n tk_tries 0
say Three times, and meant by the third. Out, and stay out.
mptransfer $n /realms/yourrealm/yourarea/rooms/gaol_yard
else
say Say it again, prisoner. Regret ripens slowly.
endif
~
The veto is the escalation ladder again, walking from a warning through rooting to a clout plus rooting, every attempt counted on the prisoner. Notice there is no isnpc courtesy this time, and that is a choice, not an oversight: a cell that holds wandering creatures too is correct fiction, and the silent count on an NPC harms nothing. The release is a counted phrase: three separate spoken repentances, tallied in their own note, and on the third the turnkey zeroes both counters and transfers the prisoner out to the yard, a path you will replace with your own. Zeroing tk_tries on release matters; mercy that forgets nothing is not mercy, and the next stay should start its ladder from the bottom.
The GREET block, which fires when a prisoner is first delivered into the cell, advertises the way out in character. Locked doors with secret handles are for puzzles; jail doors should state their price.
The Watch Rotation: TIME_PROG
Guards keep hours, and TIME_PROG is the clock. Its header is REQUIRED and is not a percent: it is a list of mud-clock hours, 0 through 23, and the block fires once as the clock turns to a listed hour, with the hour riding in $g. The mob checks the clock on its own heartbeat, so this is a mob-only trigger, and the mob must actually be loaded, which happens whenever anyone has visited its area recently; a sentry in a corner of the world nobody visits sleeps through his own rotation, and no one is there to notice. DAY_PROG is the calendar cousin for once-a-day and holiday behavior.
Rotations are built as paired hours: one block takes up the night post, another stands down at dawn. Anything the night block sets, a variable, a stance, an announcement, the dawn block should clear, so the guard is always in a state some block explicitly put him in.
Script Ten: The Wall Sentry
Pure rotation theater, no barrier at all, which is its own lesson: a guard does not need to stop anyone to make a wall feel manned.
TIME_PROG 0
emote calls the midnight hour down the wall, long and low.
mpasound A sentry's midnight call carries along the wall.
~
TIME_PROG 6
emote rakes the night brazier down to ash and rolls his shoulders as the relief bell sounds.
~
TIME_PROG 20
emote strikes flint to the wall brazier and sets his back to the cold stone.
say Curfew hours, friends. Keep to lit streets and keep your blades peaceful.
~
RAND_PROG 6
emote sweeps his gaze slowly along the road below the wall.
~
Midnight, dawn, and dusk each get an hour block, and the mpasound on the midnight call is what makes the wall feel LONG: players a room away in either direction hear the call carry. The RAND block at a low six percent gives him idle life between hours, roughly one slow scan a minute. Resist the urge to raise that number; a sentry who fidgets every heartbeat is a metronome, not a soldier.
Testing note for rotations: mudprog <target> test TIME_PROG supplies the hour 0, so only blocks whose list includes 0 fire under the hand test. The midnight block above is testable on demand; for the others, either wait for the mud clock or temporarily add 0 to their lists while you polish the lines, then take it back out.
Raising The Alarm
A guard who fights in silence is a casualty; a guard who raises the neighborhood is a system. The alarm toolbox, in rings of increasing reach: mpecho reaches the guard's own room; mpasound reaches every room exactly one exit away, the natural radius of shouting; mpchannel <channel> <text> reaches a whole chat channel mud-wide, which is almost always too far for an in-world alarm, use it rarely and deliberately; and for the audience of staff rather than players, mpllm <text> whispers to every online builder and mplog <text> writes a line to the mudprog log, both invisible in the game, perfect for telemetry like somebody actually killed the gate sergeant.
The craft problem with alarms is repetition: FIGHT_PROG fires every combat round, and an alarm that re-rings every two seconds is a fire drill. The fix is a once-per-fight gate, a variable checked and set at the top of the block, cleared when the fight ends. You saw the shape as the boss-phase idiom in the triggers chapter; here it is doing honest guard work.
Script Eleven: The Alarm Sentinel
Alarm on first blood, a second signal at half health, a grudge that survives death, and cleanup on victory:
GREET_PROG 100
if var($n slew_sentinel == 1)
say You. The last watch died on your blade, $N. This one is watching for it.
else
say The watch sees you, $N. Walk peacefully.
endif
~
FIGHT_PROG 100
if var($i as_rung == 1)
return
endif
mpsetvar $i as_rung 1
yell To arms! The sentinel rings the alarm iron!
mpasound An alarm iron rings hard and fast from the sentinel's post.
~
HITPRCNT_PROG 50
if var($i as_horn == 1)
return
endif
mpsetvar $i as_horn 1
emote staggers, drags in a breath, and sounds three ragged horn blasts.
mpasound Three ragged horn blasts beg for aid from the sentinel's post.
~
KILL_PROG
mpsetvar $i as_rung
mpsetvar $i as_horn
emote sets the alarm iron swinging gently to silence and spits on the fallen.
~
DEATH_PROG
mpsetvar $n slew_sentinel 1
say The watch... does not forget...
mpasound The alarm iron gives one last broken clang and falls silent.
~
The FIGHT block carries the once-gate: first round of a fight, the note as_rung is empty, so the alarm rings and the note is set; every later round hits the gate and returns silently. The header is 100 here so the demonstration fires on demand; the gate, not the percent, is doing the spam control. HITPRCNT_PROG 50 is the second stage, firing on rounds where his health is at or below half, with its own once-gate, because it too would otherwise repeat every qualifying round. KILL_PROG, which fires when he wins, erases both gates with bare mpsetvar lines so the next fight can ring fresh.
Now the memory lesson this script exists to teach. The two alarm gates are stored on $i, the sentinel himself, and notes on a mob die with that copy of the mob. That is exactly right for once-per-fight state. But the grudge in DEATH_PROG is stored on $n, THE KILLER, and notes on a player are saved with the character forever. So when the sentinel dies, his own memory perishes, but the mark he left on his killer does not, and the REPLACEMENT sentinel, a fresh copy with the same script, reads that mark in its greeting and knows. A guard's memory of people should live on the people; his memory of moments should live on himself. Get that rule in your bones and respawn stops being amnesia.
If you want staff to know as well, one mpllm line in DEATH_PROG does it. For reinforcements in the flesh, the commands chapter's mpmload can clone a second guard into the room mid-fight; keep it rare and make the cavalry mortal.
Script Twelve: The Gatehouse Ledger
The full memory kit in one mild little bureaucrat: a visit counter, a grudge, and a redemption phrase, no barrier at all.
GREET_PROG 100
if var($n gl_grudge == 1)
say My ledger has a black stroke by your name, $N. Say i was wrong and mean it, and I will scratch it out.
return
endif
if !var($n gl_visits)
mpsetvar $n gl_visits 0
endif
mpsetvar $n gl_visits $%math($<$n gl_visits> + 1)%
switch $<$n gl_visits>
case 1
say A first time through my gate, then. I will remember the face, $N.
case 2
say Back again. Twice through my gate makes you a regular, $N.
default
say Visit $<$n gl_visits> by my ledger, $N. The gate all but knows your stride.
endswitch
~
CNCLMSG_PROG ATTACK ALL
mpsetvar $n gl_grudge 1
mpechoat $n The gatekeeper sidesteps your swing and licks his pencil. A black stroke goes by your name.
~
SPEECH_PROG p i was wrong
if var($n gl_grudge == 1)
mpsetvar $n gl_grudge
say Scratched out, then. Ledgers forgive faster than men, $N.
else
say Your name is clean in my ledger. Keep it so.
endif
~
The greeting checks the grudge FIRST and returns early if it finds one, so a marked player gets the cold shoulder and nothing else; early return is how one block gives one visitor exactly one face. Clean visitors fall through to the counter and then a switch, which reads the count once and picks a case: first visit, second visit, or the default for everyone past that, where the count itself is spoken back through the angle form. A switch is just a tidier ladder of ifs when you are branching on one value; use whichever you can read at a glance.
The grudge is set by an ATTACK veto, so he cannot be brawled with, only resented, and cleared by an apology phrase that erases the note with a bare mpsetvar. Notice how much characterization is happening with zero mechanical stakes: no damage, no doors, just memory. If you build only one thing from this chapter into every guard you ever write, make it this ledger.
Common Mistakes
Every one of these has eaten a real builder's afternoon. Symptoms first, so you can diagnose from what you see.
The door nobody guards. You wrote CNCLMSG_PROG ENTER guard or ENTER ALL or ENTER thief, and everyone strolls through. Movement messages carry no text, so any mask after ENTER or LEAVE means the block can never match. Diagnosis: the veto never fires no matter who walks. Fix: bare code, CNCLMSG_PROG ENTER, and move the narrowing into the body.
The roach motel. You put a conditional ENTER veto on a guard, admitted people with mptransfer $n here, and now your testers are trapped inside with him, every exit bouncing them back. Nothing is broken; you built a veil and forgot the way out. Fix: add the exit speech block, farewell or let me out, with a transfer to the outside path, or convert the post to a steering lobby with escorted exits in both directions.
The silent refusal. Players report the gate is broken because typing north does nothing at all. Your veto matches and cancels, and its body prints nothing, or prints only with mpecho, which plays to the guard's room while the refused player stands outside it. Fix: every refusal speaks, and speaks with mpechoat $n, plus mpasound if the neighbors should hear.
The pileup of wolves. Days after shipping, the room outside your gate is wall-to-wall wandering monsters. Your veto refuses creatures with the player branch, and they neither read the text nor stop trying. Fix: the two-line isnpc return courtesy at the top of every movement veto.
The freed prisoner who was never freed. Your turnkey's release branch runs, says the words, and the prisoner stays in the cell, because the transfer path has a typo or still says yourrealm. A bad path quietly degrades to no move at all. Fix: paths come from whereami in the actual destination room, and the release is tested with a real walk, not a shrug.
The eaten command. You put mpprompt in a GREET block, and every visitor's first command after arriving vanishes into prompt_answer. The capture takes the player's next line, whatever it is. Fix: prompt only in response to a deliberate act, a spoken request, never spontaneously.
The free pass. Your tollkeeper sometimes lets paupers through. You wrote the paid note before charging, or granted it outside the goldamt check, and one failure path leaked a pass. Fix: check, charge, grant, in that order, all inside the same if branch.
The trigger-happy tester. You fire mudprog guard test at a keyword speech block or a masked veto and conclude it is dead. The hand test supplies the word test as the message text, which matches neither your watchword nor a code mask. That is the mask working. Fix: test speech by speaking and vetoes by really performing the action, with a second character when the veto would stop you doing it yourself.
Exercises
Four posts to build yourself before reading the answers. Each one is a combination of pieces this chapter already gave you; the worked solutions follow, but the learning is in the attempt, so attach a practice mob and try first.
Exercise one: the curfew gate. A gate guard who admits everyone by day and no one by night. Hint: TIME_PROG blocks cannot open doors, but they can leave notes on the guard, and a veto can read them.
Exercise two: the quiet door. A door that admits only mages and necromancers, with a little extra flattery for them on arrival. Hint: the veto body judges with class($n), and a zapper mask header can give one GREET block a narrower audience than another.
Exercise three: the customs post. A bonded yard that no armor may enter: travelers carrying any item answering to armor are refused, everyone else passes, and guests inside leave by a courtesy phrase. Hint: this is the veiled dead end wearing a uniform, and the judgment is has().
Exercise four: the incorruptible. A bridge guard who CANNOT be bribed: the first attempt is forgiven, the second is denounced before the whole road, and his greeting never quite forgets. Hint: BRIBE_PROG, the counter idiom, and a memory note read back in GREET.
Worked Solution One: The Curfew Gate
TIME_PROG 20 0
mpsetvar $i cg_curfew 1
say Curfew on the gate! None come in past the chain until the dawn bell.
~
TIME_PROG 6
mpsetvar $i cg_curfew
say Dawn bell. The chain comes down, and the gate stands open.
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
if var($i cg_curfew == 1)
mpechoat $n A chain bars the gate. The guard behind it shakes his head. Curfew until dawn.
else
mpechoat $n The gate guard waves you in through the open chain.
mptransfer $n here
endif
~
The clock blocks do nothing but move a note on the guard between set and erased, plus announce the change; the veto reads the note and becomes a different door by night than by day. The dusk block lists two hours, 20 and 0, so the curfew is re-asserted at midnight, which makes the state self-healing if the guard was reloaded mid-evening, and, not by accident, makes the block testable on demand, since the hand test supplies hour 0. Storing the note on $i rather than on players is correct here: curfew is the GUARD's state, one fact shared by every visitor, and it should die with the copy and be rebuilt by the next clock tick.
Worked Solution Two: The Quiet Door
GREET_PROG 100
say The Quiet Door opens for the robed orders alone, $N.
~
GREET_PROG -class mage necromancer
say You carry the smell of ozone and old paper. The Door will know its own.
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
if class($n) == mage OR class($n) == necromancer
mpechoat $n The doorkeeper bows and draws you through the Quiet Door.
mptransfer $n here
else
mpechoat $n The doorkeeper is polite and immovable. The Quiet Door stays shut.
endif
~
Two GREET blocks, and both fire for a mage: the plain one for everyone, and the zapper-masked one, whose header -class mage necromancer passes only when the arrival's class matches either value, adding the flattery on top. The veto body asks the same class question its own way, with two atoms joined by OR, either passing suffices. Note that the header mask and the body condition are two different tools for the same judgment; headers filter WHO fires a block, bodies choose WHAT a fired block does, and rich guards use both. The exit is left as the veil convention from the shrine guardian; add the farewell block if your quiet room is a dead end, exactly as before.
Worked Solution Three: The Customs Post
GREET_PROG 100
say The bonded yard lies past this customs line, $N. No armor crosses it. Say farewell inside and I will see you back out.
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
if has($n armor)
mpechoat $n The customs officer taps the armor you carry. Nothing of the kind crosses his line.
else
mpechoat $n The customs officer chalks your shoulder and passes you into the bonded yard.
mptransfer $n here
endif
~
SPEECH_PROG p farewell
say Chalk fades, friend. Next visit starts fresh.
mptransfer $n /realms/yourrealm/yourarea/rooms/customs_road
~
The judgment is inverted from every gate so far: carrying the named thing DISQUALIFIES you, which is one swapped branch and otherwise the shrine guardian's skeleton beat for beat. Test with the stock item, mpoloadroom /obj/armor, pick it up, and watch yourself get refused; drop it and walk in. The has() check matches by item name, so in a real customs post you would name the actual contraband, and note it checks CARRIED items; a smuggler's puzzle about worn versus carried versus stashed-in-a-bag is yours to design with the wornon() and container functions from the functions chapter.
Worked Solution Four: The Incorruptible
GREET_PROG 100
if var($n inc_marked == 1)
say I remember your coin purse, $N. Keep it tied shut in front of me.
else
say The Queensbridge is watched, $N, and the watcher draws a wage.
endif
~
BRIBE_PROG 100
if !var($n inc_tries)
mpsetvar $n inc_tries 0
endif
mpsetvar $n inc_tries $%math($<$n inc_tries> + 1)%
mpsetvar $n inc_marked 1
if var($n inc_tries == 1)
say My gate is not for sale, friend. Once, I will forget that.
else
say Again with the coin! Twice is a bribe sworn to the magistrate.
mpecho The bridge guard holds the offered coin high and names $N a briber before the whole road.
endif
~
The bribe block is the counter idiom pointed at corruption: every attempt is tallied and marked, the first answered privately, the second denounced to the whole room with mpecho, which is the real deterrent, since shame scales better than damage. He says once I will forget, and then marks them anyway; guards lie kindly, ledgers do not. Remember the honest status of BRIBE_PROG from the tolls section: today this block fires under the hand test, and it stands ready for the live wiring; the greeting's memory branch works either way, which is why the mark is written by the bribe block but READ somewhere that fires constantly.
The Kingsgate: A Complete Post
Everything in this chapter, standing one watch. A steering-post sergeant with a watchword, escorted exits, a protected charge, an escalation ladder, a once-per-fight alarm, a relief horn, night hours, a visit ledger, and a grudge that outlives him. Replace the two paths, attach to a mob in a gate lobby, and you have shipped a garrison:
GREET_PROG 100
if var($n ks_slayer == 1)
say The last sergeant of the Kingsgate died on your steel, $N. This one greets you with both eyes open.
return
endif
if !var($n ks_visits)
mpsetvar $n ks_visits 0
endif
mpsetvar $n ks_visits $%math($<$n ks_visits> + 1)%
switch $<$n ks_visits>
case 1
say First time at the Kingsgate, $N? The word carries you in. Say let me out and I carry you back to the road.
case 2
say The Kingsgate again, $N. You know how this goes. The word, or the road.
default
say Well met once more at the Kingsgate, $N. The word, or the road.
endswitch
~
LOOK_PROG 100
emote returns your inspection with the flat patience of a man on his thousandth watch.
~
SPEECH_PROG p the crown keeps its own
say The word is good. In you go, and mind the murder holes above.
mptransfer $n /realms/yourrealm/yourarea/rooms/inner_ward
~
SPEECH_PROG p let me out
say Back to the road with you. Keep the word to yourself out there.
mptransfer $n /realms/yourrealm/yourarea/rooms/kings_road
~
CNCLMSG_PROG ENTER
if isnpc($n)
return
endif
mpechoat $n The sergeant closes one fist in your collar and sets you back before the gate arch.
mptransfer $n here
~
CNCLMSG_PROG ATTACK herald
mpechoat $n The sergeant's arm is across you before your blade clears leather. The herald is not touched at the Kingsgate.
~
FIGHT_PROG 100
if var($i ks_rung == 1)
return
endif
mpsetvar $i ks_rung 1
yell Kingsgate! To me! Blades at the arch!
mpasound The Kingsgate alarm rings out over the walls.
~
HITPRCNT_PROG 40
if var($i ks_horn == 1)
return
endif
mpsetvar $i ks_horn 1
emote sounds two short blasts on a battered signal horn.
mpasound Two short horn blasts call for the Kingsgate relief.
~
TIME_PROG 0 20
emote kindles the gate cressets and rolls the night stiffness from his neck.
~
DEATH_PROG
mpsetvar $n ks_slayer 1
say The gate... stands... whoever holds it...
mpasound The Kingsgate alarm falls suddenly, ominously silent.
~
Walk the blocks and name their chapters. The GREET is the ledger keeper: grudge first with an early return, then the counter, then the switch. The two speech blocks and the steering veto are the watchword keeper. The masked veto CNCLMSG_PROG ATTACK herald is the honor guard's protection turned outward: it shields any resident answering to herald, the mask matching the victim's name, while the sergeant HIMSELF stays attackable, which is deliberate, because a sergeant who cannot bleed never rings his own alarm. The FIGHT and HITPRCNT blocks are the alarm sentinel, once-gates and all; the TIME block is a sliver of the wall sentry; and DEATH_PROG writes the grudge on the killer that the next sergeant's greeting will read, memory living on the person as the sentinel taught. Eleven blocks, five duties, no game code.
Run the shipping checklist on him the way a senior builder would: view the script and confirm the Triggers line lists everything; walk out and in for the greeting; speak the watchword and both escort phrases with real paths in place; have a second character swing at the herald and at the sergeant; watch the alarm ring exactly once per fight; and stand next door during a fight to hear what the neighbors hear.
Testing Your Guards
Guards are the archetype where honest testing matters most, because their best blocks, the vetoes and keyword responses, are exactly the ones the hand test cannot reach. The rules of thumb, gathered: blocks headed with percents, blanks, or ALL fire under mudprog <target> test <TRIGGER>; keyword and phrase speech blocks need the words actually spoken in the room; masked and coded vetoes need the real action attempted, walking, swinging, grabbing, with a second character whenever the veto would stop you testing it yourself. TIME blocks hand-test only if their list holds 0. HITPRCNT blocks always hand-test. Remember that punishments are real when they land on you, staff included; a turnkey who roots you has passed his test, and you get to wait out your own sentence, which is character-building in every sense.
While iterating, mplog lines at branch points are your best friend, and the script_runaway log catches anything that blows its step budget. When a guard misbehaves in ways you cannot read, view the parsed trigger list first; nine times out of ten a missing tilde has merged two blocks, and the list shows it in one glance.
Where To Go Next
The archetype travels. A questmaster is a guard whose toll is a deed; a priest is a guard whose watchword is a prayer; a ferryman is a steering post with a boat drawn on it; a boss's throne room is a sealed door with a HITPRCNT escalation behind it. The cookbook chapter has sibling recipes, including a two-NPC toll bridge and a vault door with escalating refusals, and the reference chapter is the place to check any command or function you met here in passing. Take the sergeant, refit his lines, and go man a gate of your own.
This chapter is an archetype deep-dive: one kind of content, explored to the bottom. The subject is the quest, the oldest shape in all of mud building: someone wants something done, someone does it, and someone gets paid. Every technique in the earlier chapters was building toward this. A quest giver greets, remembers, listens, validates, rewards, and refuses to be cheated, and each of those is one tool you have already met, now pointed at a story with a beginning, a middle, and a payoff.
You do not need to have read anything beyond mudprog-basics to follow along. Every idea from the deeper chapters is re-explained the first time it appears, every script is complete and attachable exactly as printed, and the chapter builds from a three-block errand to a full three-NPC quest chain with a journal entry, a deadline, and a moral. When a line puzzles you, the reference shelf is the same as ever: mudprog-triggers for the WHEN, mudprog-commands for the WHAT, mudprog-functions for the questions, mudprog-variables for the memory.
And the standing promise holds: nothing in this chapter can break the game. A quest script with a mistake in it simply does less than you hoped. The worst you can build is a giver who never pays or a wolf that dies unremarked, and both are one edit from fixed. Attach, test, tweak, repeat.
The Two Halves Of A Quest
On this mud a quest is really two things wearing one cloak, and the single most useful thing this chapter can teach you is to keep them straight.
The first half is the machinery: the quest system proper, a piece of game code called the quest daemon. It owns the player's journal. When a player is on one of its quests, the quest command lists it, green progress lines appear in their chat as objectives tick over, the quest panel of the game client fills in, and when the quest is turned in, the machinery pays the rewards written into the quest's definition: experience, gold, quest points, even items. The machinery also does an enormous amount of silent watching. A quest built with a kill objective counts the player's kills by itself. A visit objective notices the player walking into the right room. A talk objective hears the player speak to the right NPC, and an acquire objective watches their inventory for the right item. None of that watching needs a single line of script.
The second half is the theater: everything the machinery does NOT do. The machinery never speaks. It accepts a quest without a word, ticks objectives with a terse progress line, and completes a quest without so much as a handshake. It cannot give a quest a voice, a temptation, a threat, a rival, or a change of heart. That is the script's job, and it is the better job. In this chapter the machinery is the ledger and your script is the storyteller leaning over it.
You can build a quest from either half alone. A script-only quest, using nothing but the variables from mudprog-variables, works perfectly and never touches the journal; the first worked example below is exactly that, and for small local color it is often all you want. A machinery-only quest works too: a coder registers it on a questmaster NPC and players accept and turn it in with the quest command, no script anywhere. The best quests on the mud are both at once, and the toolkit that joins the two halves is what this chapter is really about.
What The Machinery Does On Its Own
Before scripting on top of the quest system, spend a page learning what is under you. You will not build any of this in a script; you will lean on all of it.
A quest lives on a questmaster NPC as a task definition, stored in a property called questmaster_tasks. It is put there by a coder writing it into the NPC's file, or by a builder using the questmaster editor inside mobmodify; admins tune existing ones with questadmin. A definition carries:
- An id: a short internal name like cellar_rats. Every script command and function in this chapter refers to the quest by this id, never by its display title. - A name and description the player sees in their journal. - Criteria: who may take it. Level range, class, race, prerequisites, reputation. - Objectives: the checklist. The types are kill, visit, talk, acquire, deliver, use, and protect, each with a target and a count. These are what the machinery auto-credits: kills from real combat, visits from real walking, talks from real conversation, acquires from items arriving in the player's pack. - Rewards: experience, gold, quest points, items, faction reputation, paid automatically at completion. - Modifiers: a time limit in seconds, a region the deeds must happen in, ordered objectives, no_drop protection on quest items, and a few others. - A repeatable flag: 0 means once ever, 1 means daily, on a real-time cooldown of a day.
Players drive their side of it with the quest command: quest list at a questmaster shows what is on offer, quest accept takes a task, quest alone shows the journal, and quest complete turns finished work in. For the command to recognize an NPC as a questmaster at all, the NPC needs its is_questmaster property set, which the questmaster editor does for you.
Two facts about the machinery matter constantly in this chapter, so here they are early and plainly.
First: a script can only start a quest that actually exists. The mpstartquest command asks the machinery to put the player on one of the HOST's registered tasks. If the scripted mob has no task registered under that id, nothing happens: no error, no journal entry, nothing. This is builder-safe by design, and it means every example in this chapter runs harmlessly on a practice mob with no quests registered; the theater plays in full and the journal calls quietly do nothing. When you build the real thing, you or a coder register the task first, then the script breathes life into it.
Second: the machinery is silent, and silence is your cue. Accepting a quest through a script prints nothing to the player by itself. Neither does completing one, beyond the rewards arriving. If your giver does not SAY that the job is taken and SAY that the debt is paid, the player experiences a mute transaction. Every example below narrates both ends, and yours should too.
The Questcraft Toolkit At A Glance
Seven commands and a handful of questions do all the work in this chapter. Here is the whole kit in one place; each gets its own section and worked example below.
- mpstartquest <who> <id> - put a player on one of the host's registered quests, host as giver. Silent on success and on refusal. - mpquestwin <who> <id> - complete the quest and pay its defined rewards, if the machinery agrees it is finished. - mpendquest <who> <id> - a near-twin of mpquestwin; treat them as the same command. The honest difference is explained beside the win probe in Reyna's section. - mpstepquest <who> <kill | visit | talk> - credit one quest event by hand, with the host standing in for the deed. - mpqset <who> <id> <key> <value> - write one named note into the player's active copy of a quest. - mpquestpoints <who> <amount> - award quest points directly. - mploadquestobj <path> - clone an item straight into the triggering player's pack. The handout command.
And the questions, used inside if lines and $%...% substitutions exactly as mudprog-functions taught:
- questwinner(<who> <id>) - has this player EVER completed that quest? The gate that stops a giver repeating themself. - qvar(<id> <key>) - read a note from the triggering player's active copy of the quest; empty text if the quest is not active for them. - questpoints(<who>) - their lifetime quest point score. - questobj(<player> <item>) - is that item quest-protected for them?
The supporting cast is everything you already know: mpsetvar and var() for memory, GIVE_PROG for handed-over items, DEATH_PROG for kill confirmation, SPEECH_PROG for every conversation, QUEST_TIME_PROG for deadlines, DAY_PROG for dailies, and mpmoney, mpexp, mpjunk, mpput for the practical business of paying and tidying.
One naming habit before the first script, learned the hard way by every scripter who skipped it: prefix your variable names and quest ids with something unique to your content. The examples below use prefixes like qc1_ and qc11_ so they can never collide with each other, or with any other script on the mud, on the shared shoulders of the same players. Notes stored on a player are shared by every script in the game; a note blandly named stage or done will eventually be someone else's note too, and the two quests will scramble each other in ways that are miserable to diagnose.
Script 1: Sergeant Vell, A Complete Quest With No Machinery At All
Start with the purest form: a whole quest in three blocks and one player variable, touching the journal not at all. This is the skeleton every later example dresses up, so read it slowly; the rest of the chapter assumes its moves.
The story: Sergeant Vell has lost his breastplate on the practice field and wants it back.
GREET_PROG 100
emote squints up from an unrolled kit of buckles and straps.
if var($n qc1_armor) == done
say Ashfield still owes you a drink for that armor, friend.
else
say You there. A soldier without his breastplate is half a soldier. Ask me about my armor.
endif
~
SPEECH_PROG armor breastplate work
if var($n qc1_armor) == done
say My thanks are already yours. The plate sits where it belongs.
else
if var($n qc1_armor) == hunting
say Still out there, friend. A plain set of armor, dropped in the mud.
else
mpsetvar $n qc1_armor hunting
say I lost my breastplate on the practice field. Fetch it back and I will pay.
mpoloadroom /obj/armor
mpecho Something metallic glints in the mud nearby.
endif
endif
~
GIVE_PROG all
if isname($o armor) and var($n qc1_armor) == hunting
mpsetvar $n qc1_armor done
mpjunk $o
say Ha! The old shell itself. You have my thanks and my coin.
mpmoney $n 40
mpexp $n 150
else
say Kind of you, but that is not what I am missing.
mpput $o $n
endif
~
Walk through the whole life of this quest as one player experiences it.
The player walks in. GREET_PROG fires, the emote plays, and the if asks the one question this entire script revolves around: what does the note qc1_armor on this player say? The var() function reads a note stored on $n, the arriving player; mpsetvar writes one. A brand new player carries no such note, so the answer is empty text, the comparison with done fails, and the else runs: Vell drops his hook line, telling the player exactly what word to say next. Ending a greeting with "ask me about X" is the oldest and best trick in questcraft; players cannot follow a thread you never dangle.
The player says armor. SPEECH_PROG fires on the keyword and the same note is read again, twice, sorting every player into one of three stories. A finished player gets a warm brush-off. A player mid-errand gets a reminder. And a fresh player reaches the innermost else, where the quest actually begins: the note is set to hunting, the briefing is spoken, and mpoloadroom clones the stock armor item onto the floor of the room, announced by a narrator line from mpecho. In a real build you would rarely spawn the target at the giver's feet; you would have a coder seed it in a far room, or script the far room itself to spawn it. It sits here so the example is complete in one room.
The player picks up the armor and hands it over: give armor to vell. GIVE_PROG fires with the item riding in $o. Two engine habits to memorize, both from the deeper chapters: GIVE_PROG ignores keywords in its header, so it is always written GIVE_PROG all with the filtering done inside, and the filtering itself is isname($o armor), which asks whether the given object answers to that name. The and joins it to the state check, and the state check is not decoration: it is the lock on the whole quest. Without it, any player who found any armor could farm Vell for coin without ever taking the errand.
On success, the order of operations is a discipline worth copying exactly: FIRST flip the note to done, THEN destroy the handed-over item with mpjunk, THEN pay. Flipping the state first means that even if the same player hands over a second armor one heartbeat later, the state check already fails and the else politely returns it. The else, note, also serves every wrong item ever pressed into Vell's hands, and mpput $o $n hands it straight back, so the sergeant cannot be used as a rubbish bin.
Attach it to a practice mob, walk out and in, say armor, give him the armor. Then do it all again and watch every line change, because the note remembers. That is a quest: a promise, a state, a proof, and a payoff, each guarded.
Variations to try. Pay quest points as well as coin by adding mpquestpoints $n 2 to the reward lines. Let Vell nag mid-errand by adding a GREET branch for the hunting state. Or demand two pieces of proof instead of one, which is exercise one at the end of the chapter.
What this version lacks is the journal. Nothing shows in quest, no progress lines print, no panel fills in. For a thirty-second errand that is fine and even charming. For anything a player might sleep on and resume tomorrow, you want the ledger too, and that is the next section.
Script 2: Reyna Of The Muster, The Journal Handshake
Now the same conversational shape, wired into the real quest system. For this script to do everything it can do, the NPC it lives on needs a task registered under the id qc2_muster, built by a coder or with the questmaster editor; give it any objectives you like, a kill or two and a visit make a fine muster errand. On a bare practice mob the script still runs, and, as you will see, it even explains itself gracefully when the machinery declines.
GREET_PROG 100
emote looks up from a muster roll weighted flat with a dagger.
if questwinner($n qc2_muster)
say The roll remembers your name kindly, $N.
else
say The company is short of hands. Ask me about the muster if you want work.
endif
~
SPEECH_PROG muster work sign
if questwinner($n qc2_muster)
say You have served already. Rest easy.
else
if qvar(qc2_muster start)
say You are on the roll. See it through, then report to me.
else
mpstartquest $n qc2_muster
if qvar(qc2_muster start)
say Your name goes on the roll. Your journal lists what the company needs.
else
say The roll is closed to you for now. Come back another season.
endif
endif
endif
~
SPEECH_PROG report finished ready
if questwinner($n qc2_muster)
say Your part is done and paid, friend.
else
mpquestwin $n qc2_muster
if questwinner($n qc2_muster)
say Signed, sealed, and paid. The company thanks you, $N.
mpquestpoints $n 5
else
say Not every line of your muster work is done yet. Keep at it.
endif
endif
~
This script introduces the two most important idioms in all of journal scripting, one in the middle block and one in the last. Both exist because of the machinery's silence: the quest commands report nothing back to the script, so the script checks for itself, immediately after asking.
The first idiom is the accept probe. When the player asks for work, the script first checks questwinner($n qc2_muster), which answers 1 if this player has EVER completed the quest; veterans get a courteous refusal and can never take the errand twice. Then it checks qvar(qc2_muster start). The qvar function reads a named note out of the player's active copy of a quest, and start is a note the MACHINERY itself writes the moment a quest begins: it is the start time, some large number. If the quest is not active for this player, qvar answers empty text instead, and empty text counts as no in an if line. So qvar(<id> start) is the cleanest possible question for "is this quest active for you right now", and this script asks it twice. Before the mpstartquest, it catches players already mid-quest and reminds them. After the mpstartquest, it asks AGAIN: if start now answers, the accept truly happened and Reyna confirms it aloud; if it still answers empty, the machinery refused, silently, as it does when the player fails the criteria, the quest is on cooldown, or no such task is registered on this mob at all. One extra if, and every silent refusal becomes an in- character line instead of a mute shrug. On your scriptless practice mob, that refusal branch is the one you will hear, and the scene still plays.
The second idiom is the win probe, the same move at the far end. The player reports in; the script fires mpquestwin and then immediately asks questwinner. The machinery completes a quest only when its own ledger agrees the work is done: every required objective ticked, no deadline blown. If it agreed, questwinner flips to 1 on the spot, rewards from the quest definition have already landed, and the script celebrates and adds five quest points on top. If it did not agree, the quest is still open, and Reyna says so instead of paying early. Your script does not need to know WHICH objective is unfinished; the player can read their own journal with quest.
A word here on mpendquest, since its name promises more than it delivers. It attempts the same completion as mpquestwin, with a fallback meant to drop the quest from the log if the attempt itself goes wrong; in the engine as it stands that fallback almost never comes into play, so treat the two as one command and write mpquestwin for clarity. When a story needs to slam a quest shut UNFINISHED, the working lever is different: mark it failed with mpqset <who> <id> failed 1, which bars the turn-in door and shows the quest as failed in the journal; the deadline section below pulls that lever at the stroke of zero. And a player can always clear an unwanted or failed quest themself with quest drop.
Notice what the script never does: it never counts kills, never watches rooms, never checks inventory. The machinery does all of that on its own, printing its progress lines as the player works. The script is pure doorman and paymaster, which is exactly the division of labor to aim for.
One more honest note. Turning a quest in through a script is not the only way; at a true questmaster the player's own quest complete command works too, wordlessly. A script like Reyna's third block exists to replace that bureaucratic ending with a human one. If you write one, make sure the NPC's spoken hints steer the player to say the word, or they may never discover the handshake.
Handing Things Over: mploadquestobj
Quests hand things to players constantly: the letter to carry, the key to the crypt, the standard-issue kit. The mploadquestobj command clones an item by file path directly INTO the triggering player's inventory, skipping the floor and the give dance entirely. Its cousin mpoloadroom, which you met above, drops the clone in the room instead; use the room version when anyone may take the item, and the quest version when it belongs to one person.
GREET_PROG 100
emote checks a tally of kit against a storehouse ledger.
if var($n qc3_kit)
say Your kit is issued already, $N. Lose it and the cost is yours.
else
mpsetvar $n qc3_kit 1
mploadquestobj /obj/torch
say Standard issue for the road, $N. Do not thank me, thank the stores.
mpechoat $n The quartermaster presses standard issue into your hands.
endif
~
The shape matters more than the story. The variable gate around the handout is not optional politeness: without it, every walk through the door mints another free item, and players will walk through that door all day. Gate every handout on a player note, exactly as this one gates on qc3_kit. The freshly cloned item is also available to the rest of the block as $b, its name as $B, so a follow-up line can name it: after the load here, $B would read as the item's short description. And when a handout should be temporary, script the taking-back too; a later GIVE_PROG can accept it home and mpjunk it.
For items the player must not lose along the way, the machinery offers what a script cannot: a quest built with the no_drop modifier protects its quest items from being dropped, and the questobj() function is how a script asks whether a given item is so protected. Belt and braces.
Nudging The Ledger: mpstepquest
The machinery credits deeds it can see: real kills, real visits, real speech. Some of the best quest moments are deeds it cannot see. The player did not kill the witness; they bought him a hot meal and he talked. The mpstepquest command exists for exactly this: it credits one quest event by hand, with the HOST standing in for the deed.
The stand-in rule is the whole command, so learn it precisely. mpstepquest $n talk credits the player as having talked to THIS mob: any active quest with a talk objective aimed at the host advances. mpstepquest $n visit credits a visit to the host's CURRENT room. mpstepquest $n kill credits a kill of the host itself, as if the scripted mob had just died to the player. It cannot credit a kill of some OTHER creature or a visit to some OTHER room; the host is always the stand-in, so put the command on the mob or room the objective points at.
One fine-print warning that has cost builders an afternoon: a talk objective in a quest definition may carry a topic, a phrase the player must actually say. The stand-in talk credit carries no words with it, so it can only satisfy talk objectives built WITHOUT a topic. If your quest demands the player say something particular, let the real conversation credit it, or build the objective topicless and let the script decide when the talking counts, which is precisely what the witness below does.
GREET_PROG 100
emote wipes crumbs from his collar and pretends to study the wall.
~
SPEECH_PROG saw witness murder
say Saw nothing. Heard nothing. My memory is a hungry thing.
~
GIVE_PROG all
if isname($o meal)
mpjunk $o
say Ah. Now that warms the memory wonderfully.
say It was the tall one with the ledger. That is all I will say.
mpstepquest $n talk
else
say My memory does not run on that.
mpput $o $n
endif
~
The design here is the quest system's talk objective pointed at this witness, topicless, with the script as the gate in front of it. Asking outright gets stonewalled: the SPEECH block answers the obvious keywords with a hint that he is a hungry sort of witness. A meal pressed into his hands crosses the GIVE threshold, and only then does the script fire the stand-in talk credit; the player's journal ticks "spoke to the witness" at the moment the information is actually spoken. The deed the machinery records is the deed as the STORY means it, not merely as the log saw it, and that is the entire art of mpstepquest.
Variations to try. Gate the meal path on the quest being active with qvar, so feeding him before taking the case earns the gossip but not the credit. Let a high-charisma player charm it out instead by checking stat($n cha) in the SPEECH block and crediting there. Or have him demand two meals, counting with a player note.
Private Margins: mpqset And qvar
Every active quest in a player's journal is a little folder the machinery keeps: when it started, which objectives have ticked, who gave it. The mpqset command lets a script write its own named notes into the margins of that folder, and qvar() reads them back. Together they give a quest chain a private ledger that lives and dies with the quest itself, which is sometimes exactly the lifetime you want: pick the quest up, notes exist; finish or abandon it, notes vanish without cleanup.
The rules, all of which matter:
- mpqset only works while the target actually has that quest active. Before the accept or after the finish there is no folder, and the write silently does nothing. - qvar always reads the player who set off the trigger; its first argument is the QUEST id, not a person. If the quest is not active for them, it answers empty text. - The machinery keeps its own notes in the same folder: start, task, progress, questgiver, complete, failed. Never overwrite start, task, or progress; they are the quest's working parts. But complete and failed are honest levers, and the deadline section below pulls one. - The folder is destroyed at completion. If a note must outlive the quest, copy it to a permanent player note with mpsetvar BEFORE the mpquestwin line runs. This is the single most common qvar bug, and it appears again in the mistakes section.
Here is a bench test you can run right now with scripttest, no NPC needed; it is a raw body, so use scripttest runfile or type it with semicolons as the basics chapter showed:
mpqset $n qc4_ledger stage vouched
mpecho Ledger probe one: $%qvar(qc4_ledger stage)% is the recorded stage.
mpsetvar $n qc4_stage vouched
mpecho Ledger probe two: $%var($n qc4_stage)% is the mirrored note.
Run it and read the two probes carefully, because their disagreement is the lesson. Probe one comes back EMPTY: you do not have a quest called qc4_ledger active, so the mpqset wrote nowhere and qvar read nothing. Probe two answers vouched, because ordinary player notes work anywhere, quest or no quest. On a live quest, probe one would answer vouched too. When a stage marker seems to vanish, this pair of probes tells you in two lines whether the quest folder actually existed when you wrote to it.
That fragility is why seasoned scripters mirror: for any value the story cannot afford to lose, write it twice, mpqset for the journal copy and mpsetvar for the permanent copy, and let later scripts trust the mpsetvar. The grand chain at the end of this chapter runs entirely on that discipline.
Fetch Quests Done Right: GIVE_PROG Validation
The item-fetch is the workhorse of questing, and GIVE_PROG is its courtroom: the moment where the player presents evidence and the script rules on it. Sergeant Vell showed the skeleton; this section builds the full standard, because fetch quests are where players probe hardest for cracks. Here is Quartermaster Edda, whose strongbox went missing between the warehouse and the docks.
GREET_PROG 100
emote counts sealed crates against a bill of lading.
if var($n qc5_box) == paid
say The manifest is settled, $N. Good doing business.
else
say A strongbox of mine went missing on the dock road. Ask me about the strongbox.
endif
~
SPEECH_PROG strongbox missing dock
if var($n qc5_box) == paid
say Settled and shelved, friend. The manifest is closed.
else
if var($n qc5_box) == searching
say Still missing. A plain container, banded and heavy, my seal on the lid.
else
mpsetvar $n qc5_box searching
say Somewhere out there sits a plain container with my seal on it. Bring it back whole.
mpoloadroom /obj/container
mpecho A dockhand drags in a battered container and leaves it by the door.
endif
endif
~
GIVE_PROG all
if isname($o container) and var($n qc5_box) == searching
mpsetvar $n qc5_box paid
mpjunk $o
say My seal, unbroken. You are worth twice what I am about to pay you.
mpmoney $n 60
mpexp $n 200
mpquestpoints $n 2
else
if isname($o container)
say A fine box, but not one I asked you to find.
else
say That is not a strongbox by any stretch.
endif
mpput $o $n
endif
~
Structurally this is Vell again, so instead of walking the happy path, walk the CRACKS, because every line of that GIVE block is a patched crack. Fetch quests get probed by players the way locks get probed by thieves, and this is the checklist of probes.
Probe one: hand over a matching item WITHOUT taking the quest. Some other container, found elsewhere, pressed hopefully into Edda's hands. The isname test passes but the state test fails, so the item bounces back with the middle else's line. Without that state check, your fetch quest is a vending machine for anyone carrying the right kind of junk.
Probe two: hand the SAME item over twice. The reward branch flips the state to paid before a single coin moves, so a second container one heartbeat later meets a state of paid, fails the check, and is politely declined. State first, destruction second, payment last. Always that order.
Probe three: keep the quest item afterward. Impossible here, because mpjunk destroys it in the reward branch. If the fiction says Edda keeps the box, destroying it IS keeping it, as far as the world model cares. If you skip the junk, the player walks off with the box and, if your state check is also weak, sells it back to your own quest tomorrow.
Probe four: hand over garbage. The final else returns anything unrecognized with mpput $o $n, which moves the object straight back to the giver's inventory. Never let an NPC silently swallow wrong items; players read that, correctly, as a bug that ate their sword.
Notice also the two-layer else: a wrong container gets a different line than a non-container. That costs three lines and buys the NPC a mind; she can tell ALMOST right from nonsense, and players notice.
If the fetch is registered with the machinery too, two more tools join in. An acquire objective pointed at the item makes the journal tick the moment the item enters the player's pack, no script needed, and the machinery consumes acquired quest items automatically at completion, its own version of the mpjunk rule. One behavior to know before you rely on acquire objectives: they check the pack at accept time too, so a player already carrying the target completes that objective instantly. For a "find my lost box" story that is fine and even delightful; for a "gather ten fresh pelts" daily it means pre-farming works, and the machinery considers that a feature. Design accordingly.
Variations to try. Demand the item AND a delivery fee by checking goldamt($n) before accepting. Refuse damp goods by keying the SPEECH hint on the weather with isweather(). Or scale thanks by speed, storing the accept time in a note and comparing against it at hand-in.
Kill Confirmation Via Speech
A kill quest has a problem no fetch quest has: the proof happens miles from the giver, and the giver was not watching. The machinery's answer is the kill objective, which counts real kills wherever they happen and ticks the journal on the spot; when your quest is registered, that is the tool to reach for first, and the whole hunt runs itself.
But the script-only version teaches a pattern every questcrafter needs sooner or later, because not every confirmation is a kill count. The pattern: the DEED writes a note on the doer, and the GIVER reads the note when the doer comes back and claims it. In MUDProg the deed's pen is a script on the victim, and the moment of death is DEATH_PROG.
Two scripts, then. First, the wolf itself. Attach this to the beast that should be hunted:
DEATH_PROG 100
mpsetvar $n qc6_wolf slain
mpecho The grey wolf shudders once and is still, its long hunt ended.
~
DEATH_PROG fires exactly once, as the mob dies, and inside it $n is the killer. One line writes the deed onto the killer's permanent notes; one line gives the death some weight in the room. That is the entire confession. Note what it does NOT check: whether the killer even has the errand. The wolf does not know about errands; it just remembers, onto whoever ends it, that it was ended. The GIVER sorts out what the note is worth.
Second, Huntmaster Orla, who posted the bounty:
GREET_PROG 100
emote oils a boar spear with slow, patient strokes.
if var($n qc6_wolf) == paid
say The hills are quieter for your work, $N.
else
say A grey wolf has been taking lambs. Ask me about the hunt if you have the stomach.
endif
~
SPEECH_PROG hunt lambs stomach
if var($n qc6_wolf) == paid
say Paid and done. Leave the poor beast its rest.
else
if var($n qc6_wolf) == slain
say So you say. Then say it plain to my face: tell me the deed is done.
else
mpsetvar $n qc6_wolf hired
say Find the grey wolf and end it. Come back and tell me the deed is done, and mind that I will know a lie.
endif
endif
~
SPEECH_PROG p the deed is done
if var($n qc6_wolf) == slain
mpsetvar $n qc6_wolf paid
say I believe you. The kill is written on you plain as ink.
mpmoney $n 80
mpexp $n 250
else
if var($n qc6_wolf) == paid
say It is, and you were paid for it. Do not milk the tale.
else
say No you have not. The wolf leaves a mark on its killer, and I see none on you.
endif
endif
~
The two scripts share one variable, qc6_wolf, and between them they walk it through a tiny state machine: empty, then hired, then slain, then paid. Study the claim block's header before anything else. It is the phrase form of SPEECH_PROG, the leading p meaning "fire when the spoken line CONTAINS this whole phrase", and the phrase was chosen with care: it shares no word with the other speech block's keywords. Had Orla listened for the word wolf in both blocks, a player announcing the kill would trigger the briefing block AND the claim block in the same breath, and she would answer twice. When one NPC has several speech blocks, read all the headers side by side and make sure no single sentence a player would naturally say can match two of them.
Now the claim logic. A player who says the words with the note at slain is paid, and the note moves to paid so the tale cannot be milked. A player who says them fresh off the street meets the best line in the script: the accusation of lying. That branch is what the whole pattern buys you. The confirmation is not the player's WORD; it is the note only the dying wolf could have written. Speech is merely the ceremony where the note is read.
Honest limits, because this pattern has them. The note lands on whoever struck the killing blow, so a helpful group mate can steal the credit; the machinery's kill objective, by contrast, credits the whole group properly. If the wolf can die to another NPC or to a wandering disaster, no player gets the note and the bounty waits for the next wolf; that is usually fine fiction. And the wolf's script must be baked into the wolf's file by a coder if the wolf respawns, or each fresh wolf rises unscripted, exactly as the basics chapter warned about permanence. None of these are reasons to avoid the pattern; they are reasons to ALSO register a kill objective when the machinery fits, and save the confession script for deeds subtler than body counts: the door left unlocked, the shrine desecrated, the letter read and resealed. DEATH_PROG is only the most dramatic pen.
Variations to try. Make the wolf taunt its hunter by checking var($n qc6_wolf) == hired in a FIGHT_PROG block, so it fights the hired player differently. Let Orla refuse the claim while the player is bloodied, with hitprcnt($n < 50), until they have washed and rested. Or pay more for a clean kill by having the wolf record the killer's health percent into the note instead of the word slain.
Racing The Clock: QUEST_TIME_PROG
Deadlines change the flavor of a quest like nothing else, and they are a genuine two-half collaboration. The machinery owns the clock: a quest built with a time limit in its modifiers starts counting the moment the player accepts, and a quest turned in past its limit simply will not complete. The script owns the dread, through a trigger made for exactly this: QUEST_TIME_PROG.
While a time-limited quest runs, the engine pulses once per minute, and each pulse fires QUEST_TIME_PROG on every scripted object in the world that defines it. The header names the quest id and, optionally, which minute marks to speak at, where the numbers are minutes REMAINING and count down toward 0, the deadline itself. A header with no minutes fires on every pulse. Inside the block, $n is the questing player and $g carries the quest id and the minutes left. Two things make this trigger unusual. It is delivered world-wide, so the scripted mob does not need to share a room with the player; mpechoat $n reaches them wherever they are, which is exactly right for a voice needling at the back of a runner's mind. And the first pulse arrives when the first minute has burned, so a five-minute errand speaks at four, three, two, one, and zero, never at five; say the number yourself at accept time if you want it heard.
Here is a courier mistress whose dispatch must beat the hour. The registered task qc7_dispatch carries the time limit, say three hundred seconds, in its definition; the script is everything the player feels.
SPEECH_PROG all
say Every hour a dispatch rides, and every rider is late but mine.
if var($n qc7_run)
say Yours is on your belt already. Run it, do not chat about it.
else
mpsetvar $n qc7_run 1
mpstartquest $n qc7_dispatch
mploadquestobj /obj/container
say This pouch must reach the garrison before the hour turns. Go.
endif
~
QUEST_TIME_PROG qc7_dispatch 5
mpechoat $n The courier's warning needles at you. Five minutes, runner.
~
Her header is all because a courier treats every word as a request to run; on a busier corner you would key it to dispatch and pouch. The briefing block is the familiar handout shape: gate, start, hand over, brief. The QUEST_TIME block is the new part, and note how little it is: narration, nothing else. The countdown, the failure, the refusal to pay a late runner, all of that is the machinery's, bought with one modifier in the quest definition. Pulses stop by themselves the moment the quest completes or is dropped, so a punctual runner never hears a bell they have already beaten.
The zero-minute pulse deserves its own paragraph, because it is where scripts get to slam a door. When time runs out, the machinery does not erase the quest on the spot; it marks the deadline missed the next time the ledger is consulted, and the too-late turn-in fails. A script that wants the failure to LAND at the stroke can define a block for minute 0 and, inside it, set the machinery's own failed note by hand with mpqset $n qc7_dispatch failed 1, then narrate the slammed gate. That is one of the two sanctioned uses of mpqset on the machinery's own notes. Exercise three at the end of the chapter has you build exactly this, and its solution is printed there.
Testing a timed quest needs one honest note: QUEST_TIME_PROG cannot be fired meaningfully from mudprog test, because its header filters on a real quest id and minutes that the test cannot supply. Test the briefing block by speaking, then accept the real registered quest and let a real minute tick; there is no substitute for hearing the cadence a player will hear.
Variations to try. A block per milestone with escalating prose, five calm, one frantic. A DIFFERENT NPC defining the same quest's QUEST_TIME block, so the garrison gatekeeper mutters about the late pouch while the courier stays composed; any scripted object may listen for any quest's clock. Or a mocking rival who only defines minute 0.
One Quest, Two Endings: Branching Outcomes
A quest whose ending the player CHOOSES is remembered long after a dozen fetches are forgotten, and branching costs less script than you would think: one choice, one permanent note per player, and every later line reading that note. Here is Brother Casmin, an archivist holding a seized heretical writ, who cannot decide its fate and makes the player do it.
GREET_PROG 100
emote turns a sealed writ over and over in ink-stained fingers.
if var($n qc8_writ) == sworn
say The oath keeps you, $N. I sleep easier for it.
else
if var($n qc8_writ) == burned
say The ashes keep their silence, $N. So do I.
else
say This writ can go into the record or into the fire, and the choice is past me. Say swear the oath, or say burn the writ.
endif
endif
~
SPEECH_PROG p swear the oath
if var($n qc8_writ)
say The choice is made, and choices of this kind are made once.
else
mpsetvar $n qc8_writ sworn
mpquestwin $n qc8_verdict
say Then it enters the record, every name and every sin. History will thank you, even if no one else does.
mpexp $n 150
endif
~
SPEECH_PROG p burn the writ
if var($n qc8_writ)
say The choice is made, and choices of this kind are made once.
else
mpsetvar $n qc8_writ burned
mpquestwin $n qc8_verdict
say Then no one hangs for old words. May the silence be kinder than the law.
mpmoney $n 30
endif
~
The mechanics are three ideas stacked. First, the choice is offered as two exact phrases, spoken back by the NPC himself so the player cannot miss them, and caught by two phrase-form speech blocks that can never overlap. Second, both branches begin with the same guard, if var($n qc8_writ) tested bare, which is truthy for EITHER outcome; the first choice locks both doors, so no player collects both endings. Third, and this is the heart of it, each branch writes a DIFFERENT value into the same permanent note before doing anything else, and both then complete the same registered quest, qc8_verdict. The journal records one quest done; the note records WHICH ending, forever.
Forever is the point. The greeting block already pays the note back: sworn players get one line, burned players another, for the rest of their days, from a mob that will have forgotten the writ itself within the hour. Any OTHER script in your area can read the same note and take sides: a magistrate who bows to the sworn, a heretic who spits at them. One variable, written once, becomes the spine of a reputation.
Note where the outcome is stored: mpsetvar, not mpqset. It is tempting to write the outcome into the quest folder as a stage note, and mid- quest that is fine, but remember the folder burns at completion. Here the mpquestwin follows two lines later, so a journal-copy of the outcome would live for less than a heartbeat. When an outcome must outlive the quest, and outcomes nearly always must, the permanent note is the only pen that lasts.
Two design notes from the trenches. Give the branches DIFFERENT rewards, as here, coin against experience; identical payoffs whisper to players that the choice was cosmetic. And resist offering an undo. The one-time lock is what gives the moment its weight; a repeatable moral choice is a slot machine.
Variations to try. Give each branch its own mpachieve mark. Move the choice items into the world: burning requires the player to HAND the writ to the brazier-tending acolyte, a GIVE_PROG validation from earlier in this chapter, while swearing requires speaking at the altar room, a room script. Or add a third, secret ending keyed on a phrase no NPC ever says aloud, and let the mud's rumor mill do the rest.
Dailies Three Ways
Repeatable content keeps a district alive between big stories, and Rogue gives you three distinct clocks to hang a daily on. Choose by what should own the reset: the machinery, the calendar stamped on each player, or the world itself at midnight.
The first clock is the machinery's, and it is the easiest by far: a registered quest whose repeatable flag is 1 may be taken again a full real-time day after each completion. The cooldown is enforced at accept, silently, which is exactly the silence the accept probe from Reyna's script turns into dialogue. Here is Bailiff Hessa running a one-writ-a-day duty slate on that clock; the task qc9_writ is registered daily, and the script is nothing but the probe pattern with a calendar excuse in its mouth.
GREET_PROG 100
emote chalks a fresh line onto the duty slate.
say The slate takes one name a day. Ask me about the writ if yours is not on it.
~
SPEECH_PROG writ slate duty
if qvar(qc9_writ start)
say Your name is on the slate already. Finish the writ you carry.
else
mpstartquest $n qc9_writ
if qvar(qc9_writ start)
say Your name goes on the slate. One writ, one day, one payment.
else
say The slate is full for you today. Come back tomorrow.
endif
endif
~
Read the refusal branch again and notice what the script does NOT know: whether the accept failed because of the daily cooldown, the criteria, or an unregistered id. It does not need to know. The probe turns every flavor of no into the same honest tomorrow, and the machinery quietly guarantees the tomorrow is real. One trap from earlier bears repeating here, because dailies are where it bites: questwinner() answers 1 forever after the FIRST completion, so it can never gate a daily; use the accept probe and let the machinery do the counting.
The second clock is the calendar stamp, for script-only dailies with no journal at all. The datetime() function answers pieces of the mud's clock, and the trick is to store TODAY as a stamp on the player when they claim the daily, then refuse while their stamp still matches today. Day of the month alone would collide with next month, so stamp day AND month together. Here is a wayside shrine that takes one meal a day from each traveler:
GREET_PROG 100
emote sweeps yesterday's petals from the shrine step.
if var($n qc15_alms) == $%datetime(day)% $%datetime(month)%
say The shrine has taken your kindness once today, friend. Tomorrow it will gladly take more.
else
say The shrine takes one gift each day. Hand me any small meal and eat well in spirit yourself.
endif
~
GIVE_PROG all
if isname($o meal)
if var($n qc15_alms) == $%datetime(day)% $%datetime(month)%
say Once a day only, or kindness curdles into habit.
mpput $o $n
else
mpsetvar $n qc15_alms $%datetime(day)% $%datetime(month)%
mpjunk $o
say Taken with thanks. The day remembers you now.
mpexp $n 50
endif
else
say The shrine takes meals only, plain and warm.
mpput $o $n
endif
~
The whole mechanism is one comparison, appearing three times: does the player's stamp equal today's day-and-month? The $%...% form drops the function answers straight into both the comparison and the mpsetvar line, so the stored value and the tested value can never drift apart. Mind that this daily runs on the MUD's calendar, whose days pass faster than real ones; that is usually the charm of it, but if your fiction says one real day, the machinery's clock above is the one that counts real hours.
The third clock is the world's own midnight. DAY_PROG fires world-wide on every scripted object that defines it, once each time the mud's calendar turns over, with the new day number riding in $g. Its header is a list of day NUMBERS to match, and this is the trap the trigger carries: the header must list the days you want, and a blank header matches none and never fires. To fire every single day, list every number your calendar can reach; one through thirty-one covers any month. Use this clock for resets that belong to the WORLD rather than to any player: the board that takes down yesterday's notices, the well that refills, the gate rota that changes. Here is a warden's bounty board whose mark changes at each midnight:
SPEECH_PROG all
say The board takes a fresh mark at each midnight, and the old marks come down.
if var($i qc10_mark)
say Today the board names the $<$i qc10_mark> as the mark.
else
say Today the board stands freshly scrubbed, waiting on midnight for its first mark.
endif
~
DAY_PROG 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
if isodd($g)
mpsetvar $i qc10_mark boar
else
mpsetvar $i qc10_mark wolf
endif
mpecho The warden scrubs the bounty board clean and chalks up a fresh mark.
~
Three details, each a small lesson. The rotating mark is stored on $i, the mob itself, because it belongs to the board, not to any player; this is the rare correct use of host-side memory that the mistakes section warns about, precisely because it SHOULD be shared by everyone. The day number in $g drives the rotation through isodd(), odd days boar, even days wolf; a switch block on $g could paint a different mark for every day of the month. And inside a DAY_PROG block there is no $n, no player who caused it, midnight has no author; speak to the room with mpecho and say, never to $n. One caveat to close: a note on a mob dies with the mob, so a freshly respawned board stands scrubbed until its next midnight. For flavor that is fine. Anything OWED to players belongs on the players or in the journal, never on the furniture.
To make the board pay, bolt on the patterns you already have: a DEATH_PROG confession on the boar and the wolf writing different notes, and a claim block on the warden that pays only when the player's note matches the board's current mark. Every daily hunt board on this mud is those three scripts holding hands.
The Grand Chain: The Pledge Of Ash Lane
Everything in this chapter, assembled: a three-NPC quest chain, written out end to end. The story first, because chains live or die on whether the player always knows where to walk next.
Old Brannock, a retired soldier, owes a debt to Josper the ledger- master, and is too proud and too lame to settle it himself. His breastplate is worth the sum. The player carries the breastplate down Ash Lane to Mirela the chandler, who advances coin on it; Mirela hands over a sealed payment box; the player carries the box to Josper, who counts it and closes the account; the player walks back to Brannock and speaks the words the old man has been waiting years to hear. Four legs, three NPCs, one player, and one variable binding them all.
That variable is qc11_pledge, and its life is a straight line:
empty - never started; Brannock has the hook.
carrying - breastplate in hand, bound for Mirela.
vouched - payment box in hand, bound for Josper.
settled - debt confirmed; the words are owed to Brannock.
done - paid, thanked, finished.
Each NPC advances the state exactly once and only from the value the previous leg left behind. That single rule is what makes a chain a chain instead of three quests in a trench coat: no leg can be skipped, repeated, or done sideways, because each door only opens from one state. Alongside the variable runs an optional journal entry, qc11_ashlane, registered on Brannock, whose wiring is covered after the scripts.
Script one of three: Brannock, the giver and the payoff both.
GREET_PROG 100
emote sits straight-backed on a bench, one hand resting on a folded cloak.
if var($n qc11_pledge) == done
say Ash Lane remembers its debts and its friends, $N.
else
say I owe a debt I cannot carry down the lane myself. Ask me about my pledge.
endif
~
SPEECH_PROG pledge owe chandler
if var($n qc11_pledge) == done
say Paid in full, friend, and my name my own again.
else
if var($n qc11_pledge) == settled
say Is it true? Then let me hear it plain: tell me the debt is cleared.
else
if var($n qc11_pledge)
say My pledge is on the road with you already. Mirela first, then Josper. See it through.
else
mpsetvar $n qc11_pledge carrying
mpstartquest $n qc11_ashlane
mploadquestobj /obj/armor
say Take my old breastplate to Mirela the chandler on Ash Lane. Her payment box goes on to Josper the ledger-master.
say Every step of that walk is one I owe you.
endif
endif
endif
~
SPEECH_PROG p the debt is cleared
if var($n qc11_pledge) == settled
mpsetvar $n qc11_pledge done
mpstepquest $n talk
mpquestwin $n qc11_ashlane
mpquestpoints $n 5
say Then I am my own man again. Here, the last coin I was saving against this day, and gladly.
mpmoney $n 100
mpexp $n 400
else
if var($n qc11_pledge) == done
say It is cleared, and you were thanked. Some debts stay paid.
else
say Not yet it is not. The ledger-master has the final say, and he has not said it.
endif
endif
~
Script two of three: Mirela the chandler, the middle leg. Her whole job is one validation and one handout.
GREET_PROG 100
emote trims a rack of tallow candles with a hooked knife.
if var($n qc11_pledge) == carrying
say You walk like someone carrying another man's honor. Show me what you have.
else
say Candles, wax, and patience for sale. Not much else today.
endif
~
GIVE_PROG all
if isname($o armor) and var($n qc11_pledge) == carrying
mpsetvar $n qc11_pledge vouched
mpjunk $o
say Brannock's plate. So the old soldier means it at last.
mploadquestobj /obj/container
say Then this is the payment box, counted and sealed. Carry it to Josper the ledger-master, and do not shake it.
else
say Unless it burns wick and tallow, I have no use for it.
mpput $o $n
endif
~
Script three of three: Josper the ledger-master, the confirmation leg.
GREET_PROG 100
emote runs a dry finger down a column of red figures.
if var($n qc11_pledge) == vouched
say That box under your arm has the look of money owed. Hand it over and we will see.
else
say Accounts are settled at this desk, not opened. Mind the ink.
endif
~
GIVE_PROG all
if isname($o container) and var($n qc11_pledge) == vouched
mpsetvar $n qc11_pledge settled
mpjunk $o
say Counted, sealed, and correct to the last coin. Brannock's page closes today.
emote draws a long line of black ink through a column of red figures.
say Go and tell the old soldier the debt is cleared. Those words, from you, will be worth more than the coin.
else
say This desk takes exact payments and nothing else.
mpput $o $n
endif
~
Now walk it as the player will, and watch the variable underneath every line. The player meets Brannock at empty, hears the hook, says pledge. State becomes carrying, the journal opens, the breastplate lands in their pack, and, crucially, Brannock's briefing names BOTH remaining stops; a chain must re-aim the player at every handoff. At Mirela's shop, her greeting already knows: the carrying state earns "show me what you have", a nudge toward the give, while everyone else gets shop patter. The breastplate crosses her counter, the validation from the fetch section passes, state becomes vouched, the box arrives, and her last line aims the player at Josper. Josper's greeting likewise reads vouched and beckons; the box crosses; state becomes settled; and his closing line does the final re-aim, telling the player the exact words to carry home. Back at Brannock, the phrase block finds settled, and the ending plays: state done, journal completed, quest points, coin, experience, and a line he has been saving for years.
Now probe it the way the fetch section taught. Give Mirela armor while at empty: her else refuses it; only the carrying state opens her door. Give Josper the box while at carrying, if you somehow kept one: refused; his door opens from vouched alone. Say the closing words to Brannock at any state but settled and he calls the lie or shrugs it off. Do the whole chain twice: Brannock's speech block finds done and declines to re-arm. Every door opens from exactly one state and every leg re-aims the player; those two rules ARE chain design, and everything else is costume.
The journal wiring, for when this leaves the practice room. Register qc11_ashlane on Brannock with the questmaster editor: name it, describe it, and give it a single talk objective aimed at Brannock with no topic set. The script does the rest: mpstartquest opens the journal at the briefing, and in the payoff block the mpstepquest talk line credits that lone objective, so the mpquestwin one line later finds the ledger satisfied and pays whatever rewards the definition carries, on top of the coin the script pays in character. If you build the task with richer objectives instead, visit Mirela's shop room, visit Josper's desk, then the machinery ticks the journal leg by leg as the player walks, free of charge; just keep the script's state variable as the thing the DOORS check, because it is the part you control line by line.
A testing checklist for any chain, this one included. First pass, the happy path in order, watching the variable move; check it at each step with a quick scripttest echo of $%var($n qc11_pledge)% if you like. Second pass, every door from every wrong state: greet and give at each NPC while at empty, carrying, vouched, settled, done, and read every refusal for tone. Third pass, the repeats: double-give at both counters, double-claim at Brannock. Fourth pass, the journal: accept on a mob with the task registered, confirm the entry appears in quest, finish, confirm it clears and pays once. It is twenty minutes of walking, and it is the difference between a chain and a complaint thread.
Common Mistakes In Questcraft
Every one of these has been built, shipped, and regretted on some mud. The symptom, the cause, the fix.
The quest that never appears. You script mpstartquest, the NPC speaks the briefing, and the player's quest command shows nothing. The id is not registered on the HOST: mpstartquest can only start a task that exists in the host mob's own quest table, and an unknown id is a silent no-op by design. Check with questadmin list on the mob; register the task with the questmaster editor, or embrace the script-only form and stop calling mpstartquest at all. The accept probe from Reyna's script turns this silent failure into an audible line, which is reason enough to always write it.
The generous giver. Rewards that can be earned twice, because the reward branch forgot its gate. Every path that pays must be locked by a state only the legitimate path sets, and the lock must FLIP before the payment: state first, junk second, pay last, the Edda order. If you remember one sentence from this chapter, make it that one.
The rubbish-bin refusal. A GIVE_PROG whose else forgets mpput $o $n, so wrong items vanish into the NPC forever. Players will hand your questmaster their best sword by mistyping one word. Return everything you do not mean to keep.
The vanished margin note. A stage written with mpqset, read back later as empty. Two causes, both covered in the qvar section: the quest was not ACTIVE when you wrote, so the write went nowhere, or the quest COMPLETED between write and read, and the folder burned with your note in it. Mirror anything that matters into mpsetvar; trust the journal folder only between accept and turn-in.
The mute nudge. An mpstepquest that never ticks the journal. Remember the stand-in rule: talk credits a topicless talk objective aimed at THE HOST, visit credits the host's current room, kill credits the host as victim. If the objective points anywhere else, or the talk objective carries a topic, the nudge cannot reach it. Put the command on the object the objective names, or rebuild the objective topicless.
The daily that locks forever. A repeatable quest gated on questwinner(), which answers 1 for the rest of the player's life after the first completion, so day two is refused by your own script before the machinery is even asked. Gate dailies on the accept probe, qvar(<id> start), and let the machinery's cooldown be the calendar.
The double-tongued giver. One spoken sentence matching two SPEECH headers, and the NPC answers twice in a breath. Read all of a mob's speech headers side by side; make claim phrases share no word with briefing keywords, the way Orla's hunt keywords and her claim phrase were chosen to miss each other.
The silent day. A DAY_PROG with a blank header, which matches nothing and never fires, or a shared counter stored on a mob that respawned overnight and forgot it. List every day number in the header, one through thirty-one, and keep anything owed to players off the furniture and on the players.
Exercises
Three exercises, each building on a script from this chapter. Try each one before reading its solution; every solution is complete and attachable, and the habit of attempting first is worth more than any paragraph in this guide.
Exercise one. Quartermaster Edda's cousin runs a manifest with TWO open lines: a sealed strongbox and a hot meal for the dock crew. Rework the fetch validation so each item is accepted once, in either order, with its own thank-you, and the doubled pay lands only when the second of the two arrives. You will need one note per item, and a final check that both are in.
The solution:
GREET_PROG 100
emote tallies dock chits into two uneven stacks.
if var($n qc12_box) == paid
say Settled, shelved, and the dockhands fed. A good day's ledger, $N.
else
say I am missing a sealed strongbox, and my dockhands are missing their dinner. Ask me about the manifest.
endif
~
SPEECH_PROG manifest strongbox dinner
if var($n qc12_box) == paid
say The manifest is closed, friend.
else
mpsetvar $n qc12_open 1
say Two lines stand open on my manifest: one sealed container, one hot meal for the dock crew. Bring me both and the pay is doubled.
endif
~
GIVE_PROG all
if !var($n qc12_open)
say I take deliveries against the manifest only. Ask me about the manifest first.
mpput $o $n
else
if isname($o container) and !var($n qc12_have_box)
mpsetvar $n qc12_have_box 1
mpjunk $o
say The strongbox, seal whole. One line closes.
else
if isname($o meal) and !var($n qc12_have_meal)
mpsetvar $n qc12_have_meal 1
mpjunk $o
say Still warm. The crew will sing your name off key.
else
say That is not on my manifest.
mpput $o $n
endif
endif
if var($n qc12_have_box) and var($n qc12_have_meal)
mpsetvar $n qc12_box paid
say Both lines closed in one ledger. Doubled pay, as promised.
mpmoney $n 120
mpexp $n 300
endif
endif
~
The points to check against your own attempt. Each item's acceptance is gated on its OWN note being unset, so the same item cannot close its line twice; the item is junked as its line closes; and the payoff is a separate check AFTER the sorting, running on whichever gift arrived second, in either order. The !var() form, the exclamation mark meaning not, reads as "if there is no such note yet", and the outer gate on qc12_open stops deliveries from players who never took the job. One deliberate rough edge remains for you to file: after the payoff, the three helper notes are still set. Harmless here, but a tidy scripter would reset qc12_open, qc12_have_box, and qc12_have_meal in the payoff branch, and a daily version of this manifest would have to.
Exercise two. A gatewarden guards the road stage of a registered quest, qc13_road. He lets a player pass only when they speak a passphrase, ash and iron, learned elsewhere in the story. On the correct phrase, first time only: step aside, mark the stage in BOTH ledgers, journal and permanent, and never perform the ceremony for that player again.
The solution:
GREET_PROG 100
emote stands square in the gate arch, halberd grounded like a doorpost.
if var($n qc13_gate) == passed
say Pass freely, $N. The words are yours and the gate knows them.
else
say None pass on my watch without the words. Find the courier if you lack them.
endif
~
SPEECH_PROG p ash and iron
if var($n qc13_gate) == passed
say The gate already knows you. Do not wear the words thin.
else
mpsetvar $n qc13_gate passed
mpqset $n qc13_road stage gate_passed
emote steps aside with a creak of leather and a short nod.
say Ash and iron it is. The road beyond is your worry now.
endif
~
The double write is the whole lesson: mpqset stamps the journal copy so any later leg of qc13_road can ask qvar(qc13_road stage) while the quest lives, and mpsetvar stamps the permanent copy so the gate itself remembers after the quest is gone, or if the player somehow reaches the gate before the quest. The greeting reads only the permanent note, and so stays truthful in every case. If your attempt stored the stage ONLY with mpqset, walk back through the vanished-margin-note mistake above and see how a player who finished the road quest would be challenged at the gate forever after.
Exercise three. Give the courier mistress's dispatch run its missing bells: a three-minute mark, a one-minute mark, and a zero-minute knell that locks the quest failed on the spot, so a runner standing at the garrison gate one breath late is refused with ceremony rather than bureaucracy.
The solution, attachable to the courier or to any scripted object at all, since the clock is delivered world-wide:
SPEECH_PROG all
say The garrison road eats slow runners, so listen for my bells.
~
QUEST_TIME_PROG qc7_dispatch 3
mpechoat $n A far-off bell counts three. The pouch feels heavier.
~
QUEST_TIME_PROG qc7_dispatch 1
mpechoat $n A near bell counts one. Run.
~
QUEST_TIME_PROG qc7_dispatch 0
mpqset $n qc7_dispatch failed 1
mpechoat $n The last bell tolls flat and final. The gate is shut.
~
Check your version against three details. The minute marks live in the HEADERS, one block per mark, not in conditions inside one block; narration reaches the runner through mpechoat $n, because at bell time they are nowhere near the speaker; and the zero block writes the machinery's own failed note, the sanctioned mpqset lever, so the turn-in door is barred at the stroke rather than at the next ledger glance. If you added flavor at five minutes too, you have merely rebuilt the original script's block, and good.
Where To Go Next
You now hold the whole questcraft kit: the two halves and the handshake between them, the accept and win probes, validation at the counter, confession at the kill, margins in the journal, clocks in three sizes, choices that stick, and a chain that walks a player across three NPCs without once losing them. What remains is vocabulary and practice.
For vocabulary: mudprog-commands is the full command shelf, mudprog-functions the full question shelf, and mudprog-reference the one-page cheat sheet of everything. The mudprog-bus chapter matters the moment your quests need to FORBID things, taking the relic, opening the door, attacking the witness, because vetoes live on the message bus, not in any trigger this chapter used. And mudprog-cookbook has sibling recipes, including the baker's errand this chapter's fetch pattern grew from.
For practice: build the Ash Lane chain in your own area with your own names, register its journal entry, and run the four-pass checklist on it. Then give the wolf a daughter who asks about her father, reads qc6_wolf, and says nothing at all when the answer is paid. Quests are just memory wearing a story, and you now own both halves of the trick.
This chapter is an archetype deep-dive: one kind of build, explored to the bottom. The last deep-dive stood behind a shop counter; this one goes underground. Rooms that notice you, chests that bite, floors that give way, doors that listen for old words, walls that were never really walls, and at the end a complete five-room dungeon with a boss and a gated treasure, every line of it plain MUDProg text you can attach with the mudprog command and test the same minute.
You do not need to have read anything beyond mudprog-basics to follow along. Every idea borrowed from the other chapters is re-explained here the first time it appears, and every script is complete exactly as printed. When you want the full story on something, the reference shelf is: mudprog-triggers for the WHEN, mudprog-commands for the WHAT, mudprog-functions for the questions a script can ask, mudprog-variables for memory, and mudprog-bus for the veto machinery that trapped chests and sealed doors are built on.
One promise before we descend, the same promise every chapter makes: you cannot break anything. A script with a mistake in it does less than you hoped, never more. A runaway loop stops itself against the engine's step budget and writes a note to the script_runaway log. A broken veto never locks up the game; the engine treats any error in a cancel script as permission. The worst thing you will build today is a trap that fails to spring, and the fix for that is never more than one mudprog <target> clear away. Experiment freely.
Thinking Like A Dungeon
A shopkeeper is one mob with one script. A dungeon is different: it is many small scripts on many hosts, cooperating so smoothly that the player experiences them as one place with one will. Before touching a single trigger, it helps to know the three duties every good dungeon script serves.
First, a dungeon telegraphs. Every trap in this chapter is announced before it is sprung: scrape marks by the stair, frost on a lid, a floor that is not where the water says it is. A trap with no warning is not a challenge, it is a mugging, and players remember the difference. The telegraph costs you one flavor line and buys you the player's trust that the dungeon plays fair.
Second, a dungeon reacts. The same room should not read the same on your fifth visit as on your first. The chest that stung you once should be spent. The doorkeeper should remember that the door is already open. Reaction is nothing but memory plus a condition, and you will use the mpsetvar command and the var() question for it on nearly every page of this chapter.
Third, and this is the deep secret of the whole archetype: a dungeon's story rides on the traveler, not on the rooms. Rooms cannot easily read each other's notes, but every room can read and write notes on the player standing in it, and the player conveniently walks from room to room carrying them. When the mask hall teaches you a password, it writes on you. When the boss dies, his death writes on his killer. When the vault opens for you three rooms later, it is reading what the boss wrote. Hold onto that image, the adventurer as a letter the dungeon mails to itself, because it is the pattern under everything from the two-room toll gate to the full barrow at the end.
The Room As An Actor
Everything in the earlier chapters went onto mobs. Rooms take scripts in exactly the same way; the only new thing to learn is the target word. Stand in the room and type mudprog here edit, and the editor opens on the room itself. mudprog here views what is attached, mudprog here clear removes it, and mudprog here test ENTRY_PROG fires a trigger with you standing in as the source, all exactly as with a mob.
What sets a room apart is which events reach it. A room hears:
- GREET_PROG, ALL_GREET_PROG, and ENTRY_PROG, the moment a PLAYER walks in. All three are wired to the same arrival, and they run in that order, so a script that defines both GREET and ENTRY blocks plays its GREET lines first. By convention this guide puts mood on GREET and machinery on ENTRY, purely so that reading the script later tells you at a glance which is which. - The item-event observers: GET_PROG, DROP_PROG, OPEN_PROG, CLOSE_PROG, PUT_PROG, WEAR_PROG, REMOVE_PROG, CONSUME_PROG. When any of those happen on the room's floor, the room witnesses them, with the item in $o and the actor in $n. A floor that reacts to what is dropped on it is a room-script classic, and the barrow at the end uses it. - The message bus: CNCLMSG_PROG and EXECMSG_PROG. The room is always in scope for actions performed inside it, and for the ENTER code the destination room is the primary host, which is what makes sealed chambers possible. - CMDFAIL_PROG, when a player in the room types something the game does not recognize. - FUNCTION_PROG routines, which work on every kind of host.
One detail worth underlining, because it changes trap design: the three arrival triggers fire for players only. A wandering mob strolling into your trapped corridor does not set off ENTRY_PROG, so you never need to worry about the local rats springing your darts. The bus is broader: a CNCLMSG_PROG ENTER veto stops every living thing, mobs included, which is why hard gates need a mob-handling clause, as we will see.
The Three Silences Of A Room
A room is a strange actor: it is the stage itself, and three things a mob does without thinking are simply not available to it. Learn these three silences now and half the mysterious failures you would ever hit in room scripting disappear before they happen.
The first silence: a room has a limited voice. The mpecho command broadcasts to the space AROUND the host, the host's environment. A mob stands in a room, so its environment is the room, and the echo lands where you expect. But a room is the outermost container of all; it stands inside nothing, so mpecho from a room script has nowhere to deliver and quietly does nothing. The same applies to mpechoaround and mpasound, and to say and emote, which are commands for creatures. What a room CAN always do is speak to a named person: mpechoat $n delivers straight to its receiver and needs no environment at all. This is why every room script in this chapter addresses the arriving player directly. When you truly want the whole room to watch a piece of theater, you hang that line on a fixture, which the next section is about.
The second silence: a room has no ears. Spoken words reach the mobs in a room, never the room itself; a SPEECH_PROG block attached to a room parses fine, shows up in the trigger list, and then never fires as long as it lives. Every password door in this chapter is therefore hosted on a creature, a doorkeeper, who does the listening.
The third silence: a room has no heartbeat. RAND_PROG and TIME_PROG ride the idle pulse of living mobs, and ONCE_PROG rides a mob's load moment, so none of them ever fire on a room. Ambience that ticks on its own needs a living host, and the invisible narrator pattern below gives every room that wants one a heartbeat to borrow.
Two smaller room-host habits, while we are being honest about the plumbing. When a room script points at something, point with dollar codes, $n and $o and $i, not with names; a room host has no surroundings to search for a name in, so name lookups on room scripts come back empty-handed while the dollar codes always resolve. And two commands that lean on the host's surroundings, mpoloadroom and mplink, sit out on room hosts for the same no-environment reason; when a room needs an item conjured or an exit opened, give that job to a fixture standing in it. None of this makes rooms weak hosts. Arrival, memory, floor-watching, and vetoes, the four things dungeons want from rooms, all work beautifully; you simply learn which jobs belong to the stage and which to the props standing on it.
Thresholds: GREET And ENTRY
The threshold script is the first thing a dungeon says, and the simplest whole script in this chapter. Attach this to a room with mudprog here edit:
ENTRY_PROG 100
mpechoat $n Cold air rises from the stones, carrying the smell of deep earth.
~
Walk out and in, and that line meets you at the door. The header says every player, every time. The body speaks to $n, the person who just arrived, which as we have just seen is the natural voice of a room. One line, and the room is no longer furniture.
Now give the threshold a memory, so first arrival feels different from every arrival after. The var() question reads a stored note, the mpsetvar command writes one, and writing it on $n, the player, makes the memory personal to each visitor and permanent, because notes on players are saved with the character:
ENTRY_PROG 100
if !var($n barrow_seen)
mpsetvar $n barrow_seen 1
mpechoat $n You have the sudden, certain feeling that no living thing has stood here in a century.
else
mpechoat $n The barrow remembers you. The silence feels almost familiar now.
endif
~
The if asks whether the note called barrow_seen on this player is still empty; the exclamation mark means not, so the question reads as "have they NOT been branded yet". The first visit takes the top branch, which both brands and speaks; every later visit takes the bottom one. Two different welcomes from one block, forever, per player.
The third threshold tool is the zapper mask, a header that filters WHO fires the block instead of rolling dice. A header starting with a dash lists requirements the arriving player must meet. Here a corridor gives seasoned adventurers an extra reading of the scene that novices walk right past, by stacking two blocks for the same trigger:
ENTRY_PROG -level 20
mpechoat $n Your practiced eye picks out fresh scrape marks where something heavy was dragged below.
~
ENTRY_PROG 100
mpechoat $n The stairway descends into a dark that swallows the light of the doorway whole.
~
Both blocks are checked on every arrival. Everyone gets the second line; only arrivals of level twenty or better also get the first, because the -level clause in the first header turns them away below that. Layered entry blocks like this are the cheapest foreshadowing there is: the scrape marks are your trap telegraph, aimed exactly at the players most likely to act on it. The full zapper vocabulary, class and race and the rest, is in the triggers chapter.
Fixtures: Giving A Room A Voice
When arrival should be theater the WHOLE room watches, not a whisper to the arriver, the line needs a host whose environment is the room: a fixture. A fixture is an ordinary mob playing the part of scenery, a statue, a brazier, a mummified doorman, anything that plausibly stands in the room forever. Script the fixture, and the room speaks through it:
GREET_PROG 100
mpecho Dust sifts from the shoulders of the basalt sentinel as its head grinds toward the doorway.
mpechoat $n Its hollow eyes settle on you and do not move away.
~
Attached to a statue mob, the first line plays to everyone present, the second privately to the newcomer, and together they make an entrance feel witnessed. This split, public mpecho plus private mpechoat, is the basic chord of dungeon atmosphere; you will hear it in nearly every script from here on. Fixtures also carry the jobs rooms cannot do at all: they hear speech, they have heartbeats, and their mplink can open exits. A good dungeon room is usually a quiet room script plus one working fixture.
Ambience That Never Repeats
Nothing flattens a dungeon like the same flavor line on every visit. The cheap cure is a dice roll at the door. The randnum() question answers with a random number from one up to its argument, function substitution written as a dollar-percent wrapper drops that answer into text or into a switch, and a switch turns each answer into a different line:
ENTRY_PROG 100
mpechoat $n The gallery opens around you, black water standing between the pillars.
switch $%randnum(3)%
case 1
mpechoat $n Somewhere ahead, a single drop of water falls and echoes for a long time.
break
case 2
mpechoat $n Something small and unseen slips into the water with a soft plink.
break
default
mpechoat $n The air tastes of rust and old rain.
endswitch
~
The first line is the constant, the room's signature, printed every single time; the switch adds one of three grace notes on top. Rolling one die into a switch like this is the standard shape for variety anywhere in scripting, and it matters doubly in dungeons, where players cross the same five rooms twenty times. Three variants is the sweet spot; write eight and you will never finish the rest of the dungeon.
The Invisible Narrator
Entry ambience fires only when someone arrives. For sounds that continue while players linger, the drip that counts out seconds, the settling of old stone at midnight, you need a heartbeat, and rooms have none. The pattern is the invisible narrator: a fixture mob that hides itself on load and then exists only to breathe atmosphere into the room.
ONCE_PROG
mphide $i
~
RAND_PROG 100
mpecho A slow drip counts out the seconds somewhere in the dark.
~
TIME_PROG 0
mpecho Far above, midnight settles over the barrow like a lid.
~
ONCE_PROG fires a single time when the mob loads, and mphide turns the host invisible; from then on the room simply seems to speak. RAND_PROG rolls its header percent every couple of seconds, and TIME_PROG fires as the mud clock reaches a listed hour, zero being midnight, both of which only living hosts can do, which is the entire reason the narrator exists.
That 100 on the RAND header is for your test, so the drip fires the moment you try it. Left that way it would drip every two seconds and drive the room's visitors out of their minds. Before you walk away, turn it down to 4 or 5, which is roughly once a minute; ambient sound should surprise, never spam. Three narrator rules keep the pattern safe: the narrator never fights, never wanders, and never speaks with say, whose attached name would break the illusion. Give it one script and stillness.
How Traps Should Feel
Now the teeth. Before the first needle flies, one paragraph of doctrine, because the engine gives you two entirely different kinds of trap and choosing between them IS trap design.
An observer trap reacts to something that succeeded. The player picks up the coin, and because the coin left the floor, the dart fires: the pickup itself worked, and the trap is its price. Observer traps ride the plain underscore triggers, GET_PROG, OPEN_PROG, ENTRY_PROG, and they are the right choice whenever the action should still happen.
A veto trap replaces the action entirely. The player tries to open the chest, and the open never happens; your script runs instead. Veto traps ride CNCLMSG_PROG, the cancel pass of the message bus, and they are the right choice when the action must NOT happen, or must not happen yet: a lid that stays down, a doorway that refuses, a relic that will not leave its plinth. The one law of the veto, which we will meet properly at the chest, is that a cancel block cancels WHENEVER it runs; there is no allowing from inside the block, only ways of performing the action on the player's behalf afterward.
Observer says: you did it, and it cost you. Veto says: you may not. Every trap, gate, and puzzle in the rest of this chapter is one of those two sentences.
The Observer Trap: The Pressure Plate
The bait trap first. An item lies in plain reach; lifting it is the mistake. GET_PROG fires on a scripted item the moment someone picks it up, with the taker in $n, which makes the item itself the perfect host:
GET_PROG 100
if var($i plate_sprung) == 1
mpechoat $n The plate beneath the flagstone clicks, spent and harmless.
else
mpsetvar $i plate_sprung 1
mpechoat $n A flagstone sinks under your hand and a dart snaps out of the wall!
mpdamage $n 10 pierce
mpalarm 60 mpcallfunc rearm
endif
~
FUNCTION_PROG rearm
mpsetvar $i plate_sprung 0
~
Attach it to a floor item, the practice object at /obj/meal or the generic thing at /obj/torch both serve, and take the item. The first taking springs the plate: the note plate_sprung on $i, the item itself, is empty, so the else branch runs, brands the item as sprung, narrates, and wounds the taker with ten points of piercing damage. Every taking inside the next minute finds the note set and gets only the spent click. Then the re-arm: mpalarm 60 schedules one script line to run sixty seconds later, and that line is mpcallfunc rearm, which invokes the FUNCTION_PROG block by name. FUNCTION_PROG blocks are routines that fire only when called, on any kind of host, and calling one from an alarm is THE way to make a delayed effect of more than one line, or, as here, to give a delayed effect a readable name. The routine clears the note, and the plate is live again.
Notice which object carries the memory. The note rides on $i, the item, because a sprung plate is a fact about the WORLD, true for everyone. Had we branded $n instead, the trap would spring once per PLAYER, fresh for every newcomer, which is a different and equally useful trap; that per-player form appears in the corridor traps below. Deciding where each note lives, on the world or on the traveler, is a choice you will make in every script in this chapter, and it always has a right answer if you ask WHO the fact is about.
The Veto Trap: The Trapped Chest
The signature dungeon furniture. We want a chest that stabs the first hand to open it, then behaves like honest furniture forever. The open must not succeed while the trap is live, so this is veto work. If the bus is new to you: CNCLMSG_PROG runs BEFORE an action commits, its header names the action code and then a mask, and when both match, the action is cancelled and your block runs in its place. Attach this to a practice container, /obj/container, which opens and closes like any chest:
CNCLMSG_PROG OPEN container
mpecho The lid shifts a hair and a needle darts from the seam of the lock!
mpdamage $n 15 pierce
mpechoat $n Something bitter stings along the back of your hand.
mpunloadscript
mpforce $n open container
~
The header reads: intercept the OPEN action, when the thing being opened answers to the name container. The mask matters more than it looks: cancel blocks are in scope for the whole room, so an unmasked OPEN veto on this chest would also cancel every OTHER open that happens nearby, including some other player's backpack. Mask every veto to its own subject; the mask is matched against the item's key name.
The body is the trap firing in place of the open: the public spring, the wound, the private venom line. Then the two-line pivot that makes it one-shot. mpunloadscript deletes this entire script from the chest, trap and all; the chest is now perfectly ordinary furniture. And mpforce $n open container makes the player perform the open again, which now sails through, because nothing is left to object. From the player's side it is seamless: they opened the chest, and it cost them.
The order of those two lines is everything. Force first, and the forced open runs into your own still-armed veto, which fires the trap again and forces again, around and around until the engine's step budget cuts the knot; the engine survives, the log gains an entry, and your trap looks ridiculous. Unload first, force second, always.
And because mpunloadscript takes EVERYTHING with it, a trapped chest's script should contain nothing but its trap. Flavor lines you want to survive the spring belong on the room or on a fixture, never on the chest that is going to wipe itself.
What about a trap that does not disarm, a chest that is really a warning? Then keep the veto forever and let it escalate instead. This one counts attempts on a note, using the counting idiom from the variables chapter, math() inside a substitution feeding mpargset:
CNCLMSG_PROG OPEN container
if var($i ward_anger == '')
mpsetvar $i ward_anger 0
endif
mpargset 1 $%math($<$i ward_anger> + 1)%
mpsetvar $i ward_anger $1
if number($1) >= 3
mpecho The ward flares white and the whole lid crackles with pent-up force!
mpdamage $n 20 shock
mpechoat $n The shock hurls your hand away and leaves your fingers numb.
else
mpecho Pale light webs across the lid and shoves the hand away.
mpechoat $n A voice with no throat whispers that the king still watches his gold.
endif
~
Every attempt bumps the counter and is refused; from the third attempt on, the refusals hurt. The lid never opens, because a cancel block that runs always cancels; this chest is a wall wearing a chest's shape, which is exactly right for treasure whose key is elsewhere, as the barrow's cache will demonstrate. Note the counter lives on $i again: the ward's temper is a fact about the chest, shared by everyone who annoys it.
Hurting People Fairly
The mpdamage command takes a victim, an amount, and a type, and the types are blunt, cutting, thrusting, pierce, heat, cold, shock, and magic. The damage is real. It respects armor and resistances, and enough of it will genuinely drop a player, so calibrate like a designer, not an executioner: a corridor trap should cost a bite of health a level-appropriate visitor shrugs off with a scare, ten to twenty points in the low levels, and never anywhere near a full health bar. A trap that can kill outright teaches players to stop exploring, which is the opposite of what a dungeon is for. On the same principle, never put mpslay in a trap. Ever.
Fairness also loves a saving throw, a chance to feel quick. The rand() question answers yes the given percent of the time:
ENTRY_PROG 100
mpechoat $n A tripwire sings, ankle-high, in the shadow of the arch.
if rand(50)
mpechoat $n You twist aside and a dart shatters against the far wall.
else
mpechoat $n A dart takes you in the shoulder before you can move.
mpdamage $n 12 pierce
endif
~
Half the visitors dodge, and even the ones who do not were warned by the singing wire, the telegraph doing its duty. If you would rather reward the character than the dice, swap the rand(50) for a question about the victim, stat($n agi) >= 20 for the nimble or level($n) >= 30 for the seasoned; the functions chapter has the full toolbox.
A trap can also cost something other than health. The mpaffect command applies a real status condition for a number of seconds, and the rooted condition, which pins the victim in place, is the classic snare. This one scales its sting by victim level while it holds them:
ENTRY_PROG 100
mpechoat $n A loop of blackened wire snaps tight around your ankle!
mpaffect $n rooted 8
if level($n) >= 30
mpdamage $n 25 cutting
else
mpdamage $n 8 cutting
endif
~
Keep condition durations short, a handful of seconds; being helpless is memorable at eight seconds and miserable at sixty. And notice HOW the scaling is done, with an if choosing between two literal amounts. Your instinct might be to compute the number and write something like mpdamage $n $1 pierce, and here is an engine honesty note: the amount slot of mpdamage, and the number slots of most mp commands, want a plain literal number, not a dollar code; a code there reads as zero damage and the trap tickles. Branching to literal amounts, as above, is the reliable idiom.
Common Trap Mistakes
Three mistakes account for nearly every trap that misfires, and two of them you have already seen coming.
The first is trying to write an allow branch inside a veto. Every new scripter writes this chest once:
CNCLMSG_PROG OPEN container
if var($n vault_friend) == 1
mpechoat $n The chest recognises you and its lid drifts obligingly upward.
else
mpechoat $n The chest refuses you with a stubborn creak.
endif
~
The refusal branch works. The friendly branch prints its welcoming line and then nothing happens, because the block ran, and a cancel block that runs cancels; a lid does not open because a script said something warm about it. The fix is the pattern from the needle chest: the qualified branch must PERFORM the open on the player's behalf, and if the veto should be gone afterward, unload first and force second. The barrow's cache at the end of this chapter is exactly that fix, working.
The second is the unmasked veto. CNCLMSG_PROG OPEN with no mask, or worse CNCLMSG_PROG ALL, on any host in the room, cancels every matching action near it, and ALL cancels essentially everything, movement included. If a room mysteriously stops cooperating while you are building, check for a broad veto you forgot; masks are not decoration.
The third is the forced-open loop, force before unload, covered at the chest. It is worth its own line in this list because the symptom, a trap that fires several times in one instant and then gives up, looks so much like an engine bug that builders hunt everywhere except the two lines they wrote in the wrong order.
Doors That Listen: The Spoken Password
Now the doors. A password door is a conversation plus a piece of stagecraft, and because rooms cannot hear, the listener is always a creature: a doorkeeper, in whatever costume your dungeon dresses him. The stagecraft is the mplink command, which adds a real, walkable exit to the room the host stands in, and its partner mpunlink, which removes one. Here is the whole mechanism; the destination path here is a harmless town square for practice, and in your own dungeon it would be the room beyond the door:
GREET_PROG 100
say The way below is shut, $N. Stone keeps it shut until the old words open it.
~
SPEECH_PROG p bone and silence
if var($i door_open) == 1
say The words are already spoken, $N. The way stands open. Go down while it lasts.
else
mpsetvar $i door_open 1
say The old words, spoken true.
mpecho Counterweights begin to fall somewhere inside the wall, and a slab of stone swings wide.
mplink breach /realms/loralei/aurin/rooms/room1
mpalarm 60 mpcallfunc sealdoor
endif
~
SPEECH_PROG door open words
say Words open it, $N, and only the right ones. The masks upstairs know what the king fears.
~
FUNCTION_PROG sealdoor
mpunlink breach
mpsetvar $i door_open 0
mpecho The slab swings shut, and the counterweights climb back into the dark.
~
Four blocks, four jobs. The GREET states the rule, so no one needs to read the builder's mind. The password block uses the phrase form of the speech header, the letter p followed by the phrase, which matches only the words bone and silence together in that order; without the p, the header would be three separate keywords and the word and alone would open your door, which is the most common password bug there is. Inside, a latch on $i remembers that the door is currently open so repeat speakers get an answer instead of a second grinding spectacle, then the theater, then mplink conjures the exit called breach, then an alarm appoints the closing crew. The hint block catches players fishing with words like door and open and points them, gently, back toward the clue. And the sealdoor routine undoes everything, exit, latch, and mood, one minute later.
Timed self-closing is not just flavor; it is hygiene. An mplink exit lasts until the room reloads, so a door with no closer stands open for hours, and a returning builder finds a hole in the world and no memory of why. Every mplink in this chapter is born with its mpunlink already scheduled. Choose your passwords like set dressing, phrases no one says by accident, and plant the clue somewhere a thorough player will find it; a password whose clue exists only in your head is a wall, not a puzzle.
Doors That Search You: The Carried Token
The other classic gate asks not what you know but what you carry. The has() question answers whether someone's inventory holds a thing by name, and a gatekeeper can ask it the moment you walk in. For practice, the token is the generic thing from /obj/torch, which answers to the name thing; in your dungeon it would be the sigil, skull, or signet you minted for the purpose:
GREET_PROG 100
if has($n thing)
say The pale token in your pack speaks for you, $N.
mpecho The gatekeeper draws back the bar and hauls the iron gate open.
mplink gateway /realms/loralei/aurin/rooms/room1
mpalarm 45 mpcallfunc closegate
else
say No token, no passage, $N. The iron does not argue and neither do I.
emote rests one hand flat against the cold iron of the gate.
endif
~
FUNCTION_PROG closegate
mpunlink gateway
mpecho The iron gate falls shut with a boom that rolls away down the passage.
~
Same skeleton as the password door with the question swapped: carriers get the gate hauled open and forty-five seconds of passage, everyone else gets a civil refusal and a closed fist of iron. Variations worth knowing: hasnum($n thing 3) demands three of something, tribute rather than a token; adding mpjunk against the token in the carrying branch CONSUMES it, turning a permanent pass into a one-use key; and checking var($n ...) instead of has() gates on a deed already done rather than an object held, which needs no item at all. There is also a harder form of both door types, built as a CNCLMSG_PROG ENTER veto on the protected room so that walking in is simply impossible until you qualify; the toll gate recipe in the cookbook and the door section of the bus chapter walk through it, including the mob-handling clause a movement veto needs so wandering NPCs are not stranded outside, and the detail that the allowed branch must move the player itself, naming the destination room by its full path.
Secret Doors: mplink And mpunlink
Because mplink is the hinge half this chapter turns on, it deserves its own honest paragraphs.
What it does: adds an exit, in the named direction, from the room the HOST STANDS IN to the room file you give it. The exit is real in every sense, listed with the obvious exits and walkable by anyone, which is what makes it feel like the wall truly opened rather than like a teleport trick. What it does not do: touch the far side. The link is one way, and the destination room does not sprout a return exit; if players should be able to walk back, either link to a room that already has its own way around, or station a second scripted host in the far room to open the return door. And its lifetime: until mpunlink removes it or the room reloads at a reset or reboot, whichever comes first, which is why the scheduled closer is not optional.
The direction name is yours to invent, and it is half the flavor: players go breach, or gap, or behind the falls. Pick a word that reads as a place, not a command.
The opener does not have to be a doorkeeper, and it does not have to be speech. Any trigger on any host standing in the room can pull the lever. Here is a favorite: a portable secret door, a keystone that opens the wall wherever it is set down. DROP_PROG fires on a scripted item when it is dropped, at which point the item is lying in the room, so its mplink works:
DROP_PROG 100
mpecho The keystone settles into a worn socket in the floor as if the socket had been cut for it.
mpecho With a long grinding sigh, a section of wall pivots open onto darkness.
mplink gap /realms/loralei/aurin/rooms/room1
mpalarm 60 mpcallfunc grindshut
~
GET_PROG 100
mpechoat $n The keystone comes up cold and heavy, and behind you the wall shivers.
~
FUNCTION_PROG grindshut
mpunlink gap
mpecho The wall pivots back and closes without leaving a seam.
~
Drop the stone and the wall opens; the alarm closes it a minute later regardless. One edge to know, since you will eventually meet it: if the stone is picked back up before its alarm fires, the closing routine finds the stone in someone's pack, where, like any carried host, its unlink has no room to act on, and the exit simply waits for the room's next reload to disappear. A pedant patches that by branding the room with the direction name and having a room-side closer; a storyteller shrugs and calls it a mystery of the old builders. Both are correct.
Sharing State: The Traveler Carries The Story
Step back and look at what every multi-room build so far has actually shared. The password door's latch lived on the doorkeeper, one host, one room. But everything that crossed a room boundary, the brand from the threshold, the token check, the deed-flags coming up next, traveled on the PLAYER. That is the rule of thumb worth engraving: state about a place lives on a host in that place; state that must cross rooms rides the player who does the crossing.
Riding the player buys you two enormous properties for free. It is per-adventurer, so two parties can be at different points of your dungeon simultaneously without trampling each other's progress. And it is permanent, because notes on players are saved with the character; your dungeon remembers its conquerors across reboots without you lifting a finger.
Spend your flags deliberately. Some are tickets, meant to be used up: the seal sequence below sets its flag back to zero the moment the door opens, so the walk must be earned again next time. Some are scars, meant to last: the barrow's king_fallen brand is never cleared, because the king stays dead for that hero forever. When you write a flag, decide at the same moment who, if anyone, ever clears it.
Name your flags like a family. Every note in one dungeon should share a prefix, barrow_seen, seal_step, king_fallen, so that six months from now a glance at any script tells you which build owns which memory, and no other builder's toll_paid ever collides with yours.
One last cupboard, for completeness: the engine also keeps a mud-wide global store, written with the mpgset command, surviving reboots and shared by every script host in the world. In the current engine it is write-only from script text, a ledger the server and staff can read but no dollar code or question can yet look up, so treat it as a place to RECORD world events for the future, a tally of king-slayings, say, and build your gates on player flags and host notes, which scripts can read today. The variables chapter keeps the authoritative word on this.
Puzzle Rooms In Sequence
With flags on the traveler, rooms can play a melody together. The classic is the sequence puzzle: walk the rooms in the order the builders intended and a door opens; walk them wrong and the attempt resets. It is nothing but one number on the player, seal_step, advanced by each room that finds it correct and zeroed by any room that finds it wrong.
The first seal room simply starts the count, every time it is crossed:
ENTRY_PROG 100
mpsetvar $n seal_step 1
mpechoat $n The first seal wakes under your feet, tracing a ring of grey light that follows you on.
~
The second seal advances the count only if the first was just taken, and punishes disorder by resetting:
ENTRY_PROG 100
mpechoat $n The second seal waits in the floor, a dark ring of laid stones.
if var($n seal_step) == 1
mpsetvar $n seal_step 2
mpechoat $n The ring answers the first seal, brightening from grey to white as you cross it.
else
mpsetvar $n seal_step 0
mpechoat $n The ring stays dark. Whatever order the wardens meant, this was not it.
endif
~
And the door room's warden reads the finished count, spends it, and opens the way, mplink work again, so the host is a fixture mob:
GREET_PROG 100
say The wardens left one door and one rule, $N. Wake their seals in the order they were laid.
if var($n seal_step) == 2
mpsetvar $n seal_step 0
mpecho A seam splits the far wall from floor to ceiling, and the stone folds aside.
mplink seam /realms/loralei/aurin/rooms/room1
mpalarm 45 mpcallfunc foldshut
endif
~
FUNCTION_PROG foldshut
mpunlink seam
mpecho The stone folds back into a seamless wall.
~
Three scripts, three hosts, one number, and the dungeon has a puzzle with no puzzle pieces: the floor itself is the lock. The design points worth stealing: every room SPEAKS its state, grey ring, white ring, dark ring, so a player can deduce the rule from feedback instead of guessing blindly; the wrong path resets rather than punishes, an invitation to try again; and the warden spends the flag on success, so the sequence is a walk, not a purchase. Longer sequences just add numbered rooms, each demanding the count it expects. If two players walk the seals together, each carries their own count, and the door opens for whoever finishes the walk correctly; that is the per-player magic doing its quiet work.
Hazards That Come Back
A dungeon lives on a clock: the area reset restores rooms, restocks mobs, and sweeps up leavings on a schedule. Your scripts share the world with that clock, so design with it, not against it.
Live-attached scripts are the first thing to know about. A script you attach with the mudprog command lives on that one copy of its host. When a scripted MOB dies or the reset replaces it, the replacement is factory-new and scriptless; your doorkeeper forgets everything he was taught. Rooms are steadier hosts, reloading only at reboots and updates, and items last exactly as long as the item does. This is by design: attached scripts are the workshop, and a finished dungeon's scripts get baked into the hosts' files by a coder so every copy is born knowing its part; the example at /domains/examples/npc/mudprog_greeter.c shows the pattern, and until then, keep your script text saved somewhere so reattaching after a reset is a paste away.
For the hazards themselves, the alarm-plus-routine pattern from the pressure plate is the workhorse: a trap that re-arms is just a trap whose one-shot latch gets cleared on a timer. Here is the room-hosted version, a spike floor with a shared latch and a forty-five second cycle, one bite per cycle no matter how many boots cross it:
ENTRY_PROG 100
if var($i spikes_sprung) == 1
mpechoat $n Broken spike stubs jut from the floor slots, already spent.
else
mpsetvar $i spikes_sprung 1
mpechoat $n The floor drops half an inch and rusted spikes lance up between the flagstones!
mpdamage $n 14 thrusting
mpalarm 45 mpcallfunc resetspikes
endif
~
FUNCTION_PROG resetspikes
mpsetvar $i spikes_sprung 0
~
The latch on $i makes the sprung state a fact about the room, so a party's second walker crosses safely behind the first, which reads as the trap having been triggered for the group, a genuinely good feel. Want it crueler, one bite per PLAYER? Move the latch to $n and drop the alarm; the player flag is the memory, and it never resets.
Two reset courtesies to keep. If a script conjures a creature with mpmload, the engine already marks it to despawn on the area reset, so scripted ambushes clean themselves up; do not build anything that assumes a summoned guard outlives the cycle. And guard your summons against stacking anyway, a quick ishere() or nummobsroom() check before loading, because a trap that spawns a guardian on every spring will happily fill the room with guardians while you are at lunch.
The Barrow Of The Pale King: A Complete Dungeon
Everything above, assembled. The barrow is five rooms in a line, plus a prize. Room one is the sunken stair, the threshold. Room two is the hall of masks, where a curator teaches the password to those who ask. Room three is the flooded gallery, the trap corridor. Room four is the king's threshold, whose doorkeeper opens the way only to the old words. Room five is the king's chamber, where the Pale King waits, and where his cache opens only for whoever has slain him. Six scripts, six hosts; attach each to its own host and the barrow runs itself. The two door scripts link to a practice destination here; building it for real, you would put your own room five path on the doorkeeper's mplink and give room five its way back.
The sunken stair, on the room:
ENTRY_PROG 100
mpechoat $n The stair sinks between root-split stones, and the daylight gives up a few steps down.
if !var($n barrow_entered)
mpsetvar $n barrow_entered 1
mpechoat $n Something older than the dark takes note of you. The feeling passes, but not far.
endif
~
The threshold pattern exactly: a signature line for everyone, and a first-crossing shiver that each player feels precisely once in their life, because the brand rides the character.
The curator, a fixture mob in the hall of masks:
GREET_PROG 100
say Masks, $N. Every servant the king ever kept hangs on these walls.
say Ask them what the king fears, and they will tell you what I tell you: bone and silence.
~
SPEECH_PROG king fears masks
say What the king fears, the door remembers, $N. Carry the words down with you: bone and silence.
~
The curator is the puzzle's kindness. The password is given away freely, in the greeting and again to anyone whose speech wanders near the subject; the puzzle of the barrow is paying attention, not extracting secrets. Note the speech header is plain keywords, king or fears or masks, wide on purpose for a helper, where the doorkeeper below is narrow on purpose for a lock.
The flooded gallery, on the room:
ENTRY_PROG 100
mpechoat $n Black water stands shin-deep between the pillars, dotted with drowned white masks.
if var($n gallery_wise) == 1
mpechoat $n You keep to the pillar bases this time, and the water lets you pass.
else
mpsetvar $n gallery_wise 1
mpechoat $n The floor is not where the water says it is. You drop hard into a hidden step.
mpdamage $n 10 blunt
endif
~
A trap that teaches. The first crossing costs a bruise and writes gallery_wise on the player; every later crossing narrates their earned surefootedness instead. A hazard the player visibly learns to beat is worth three that merely repeat, and it costs one flag.
The doorkeeper at the king's threshold:
GREET_PROG 100
say The king lies past me, $N, behind stone no blade has marked. Say what he fears and pass.
~
SPEECH_PROG p bone and silence
if var($i kings_door) == 1
say The door still stands open, $N. Down, before the stone forgets you.
else
mpsetvar $i kings_door 1
say Spoken as the masks speak it.
mpecho The doorkeeper sets his palm to the wall, and a black doorway grinds open beside him.
mplink kingsway /realms/loralei/aurin/rooms/room1
mpalarm 90 mpcallfunc closekingsdoor
endif
~
FUNCTION_PROG closekingsdoor
mpunlink kingsway
mpsetvar $i kings_door 0
mpecho The black doorway grinds shut, and the wall is only a wall again.
~
The password door pattern whole: rule announced at greet, phrase-form lock, open-latch on the keeper, theater, timed closer that also resets the latch. Ninety seconds of open door is generous on purpose; a party should fit through one speaking.
The Pale King, the boss himself:
GREET_PROG 100
say You said the words, little thief. Bone you have brought me. Silence I will make myself.
~
FIGHT_PROG 25
emote fights in a whisper of grave-cloth, never once striking twice from the same angle.
~
HITPRCNT_PROG 50
if var($i king_enraged) == 1
return
endif
mpsetvar $i king_enraged 1
mpecho The Pale King casts his crown aside, and the cold in the chamber deepens until it burns.
~
DEATH_PROG
mpecho The Pale King folds down into grave-cloth and powdered bone, and the cold goes out of the room.
mpsetvar $n king_fallen 1
mpoloadroom /obj/armor
mpset $b short the Pale King's breastplate
mpecho Where he fell, a breastplate of white metal lies quiet on the stones.
~
Four combat triggers doing four jobs. The greet is menace. FIGHT at a quarter chance gives the battle a signature without chattering every round. HITPRCNT_PROG fires every round the king's health sits at or below its header, so the enrage guards itself with a one-shot latch on $i and a return, the phase-change idiom from the triggers chapter; a respawned king is a fresh copy, latch empty, ready to rage again.
And DEATH_PROG is the dungeon's hinge. Three things happen in the king's last breath: the killer, who is $n here, is branded king_fallen, the scar-flag the cache will read; a trophy is conjured onto the floor with mpoloadroom and reskinned on the spot through $b, which always means the last thing this script loaded; and the room is told the cold has ended. Keep death bodies immediate like this; the mob is gone the moment they finish, and anything scheduled for later will find nobody home.
The king's cache, on a chest in the chamber:
CNCLMSG_PROG OPEN container
mpecho Frost creeps out from under the lid of the king's cache.
if var($n king_fallen) == 1
mpecho The frost sighs away to nothing. Whatever kept this lid has lost its king.
mpunloadscript
mpforce $n open container
else
mpdamage $n 10 cold
mpechoat $n Cold bites your fingers to the bone. The lid does not move while its master endures.
endif
~
This is the broken conditional veto from the mistakes section, repaired and put to work: the unqualified branch refuses with a sting, and the qualified branch performs the open on the player's behalf, unload first, force second. The gate reads the brand that only the king's own death can write, which is the traveler-carries-the-story pattern closing its loop: room five's treasure is locked by an event, and the event's receipt is stored on the hero. One design note: because the veto unloads itself for the FIRST qualified opener, the cache is a first-victor prize per chest-copy; when the area reset replaces the chest, the lock returns with it, which is exactly the cadence a boss chest wants.
Build the barrow in play order and test each host before scripting the next: stair, curator, gallery, doorkeeper, king, cache. Then take the whole walk yourself as a player would, and once more with a second character, to feel the per-player memory doing its work. The mudprog <target> test command fires any single trigger on demand while you iterate, keyword speech is easiest to test by just saying the words, and mplog lines write breadcrumbs to the mudprog log when a flag refuses to behave.
Exercises
Three builds to try with the chapter's tools, each with a worked solution. Build yours before reading mine; the comparison is where the learning is.
First: a corridor that counts each player's crossings, announcing the number, and grows audibly bored of anyone past their fifth crossing. The tools are the counting idiom and a threshold script. One solution:
ENTRY_PROG 100
if var($n crossings == '')
mpsetvar $n crossings 0
endif
mpargset 1 $%math($<$n crossings> + 1)%
mpsetvar $n crossings $1
if number($1) >= 5
mpechoat $n The corridor knows your stride by now. Crossing number $1, and it is bored of you.
else
mpechoat $n Your steps ring down the corridor. This is crossing number $1.
endif
~
The counter seeds itself on first meeting, the math substitution bumps it through slot 1, the note is written back, and the announcement splits on the count. Because the count lives on $n, every player is counted separately and forever; move it to $i and you would have a turnstile counting total traffic instead.
Second: a doorway that tithes three gold from everyone who passes under it, and sulks, audibly, at the penniless. The tools are goldamt() and mpmoney with a negative amount. One solution, on the room:
ENTRY_PROG 100
mpechoat $n A brass slot in the door frame rattles hungrily as you pass beneath it.
if goldamt($n) >= 3
mpmoney $n -3
mpechoat $n Three coins slide out of your purse and vanish into the slot, quick as a pickpocket.
else
mpechoat $n The slot clicks twice, finds nothing worth taking, and falls sullenly silent.
endif
~
Check before you charge, always; the guard question in front of the mpmoney is what keeps the door honest. A kinder variant brands payers with a day-pass flag and lets the flag skip the next tithe; a crueler one is left to your conscience.
Third: a vault behind a circular wall section that swings open when someone speaks the phrase open the vault, warns the room when its time is nearly up, and seals itself smooth. The tools are the phrase header, mplink, and one alarm more than usual. One solution, on a vault-keeper fixture:
SPEECH_PROG p open the vault
mpecho A hairline crack races around a circle of wall, and the circle swings inward.
mplink vaultway /realms/loralei/aurin/rooms/room1
mpalarm 20 mpecho The circle of wall shivers on its pivot. The vault does not stay open long.
mpalarm 40 mpcallfunc vaultshut
~
FUNCTION_PROG vaultshut
mpunlink vaultway
mpecho The circle of wall swings back, and the crack seals itself smooth.
~
The new trick is the pair of alarms set in the same breath: one line of warning at twenty seconds, the closing routine at forty. A script can schedule as many future moments as it likes this way, each alarm carrying one line, and mpcallfunc turning any line into a whole routine. That is stage management, and with it you can time anything a dungeon needs.
Where To Go Next
You now hold the full dungeon toolkit: rooms that greet and remember, fixtures that speak for the stage, both families of trap, doors keyed to words, tokens, walks, and deeds, exits that open and close on cue, and state that rides the traveler from the first stair to the last chest. The cookbook's toll gate and sunken reliquary make good next studies, being the same patterns in different costumes; the bus chapter deepens the veto machinery when you want customs posts and no-magic sanctums; and the workbooks will drill the memory and flow idioms until they are reflex. After that, the only thing left is the thing this chapter cannot teach: draw five rooms of your own on paper, decide what the traveler should feel in each, and go make the stones do it.
This chapter is an archetype deep-dive: one kind of character, explored to the bottom. The companion is the archetype players fall in love with. A shopkeeper is visited; a guard is passed; but a hound that pads at your heel, a cat with opinions about your gifts, a mount that complains about your riding posture, a bird that carries your words across town: those are the characters players name in their journals and grieve when a reboot takes them away. Nothing in the whole scripting engine pays back a building hour like a companion does, because a companion is pure personality, and personality is exactly the thing MUDProg was built to add.
You do not need to have read anything beyond mudprog-basics to follow along. Every idea borrowed from the other chapters is re-explained here the first time it appears, every script is complete and attachable exactly as printed, and the chapter builds from a three-line dooryard dog to a full graveyard hound with memory, loyalty, appetite, a bodyguard's reflexes, and the ability to follow its mistress from room to room using nothing but its own script. When a line puzzles you, the deeper chapters are the reference shelf: mudprog-triggers for the WHEN, mudprog-commands for the WHAT, mudprog-functions for the questions a script can ask, mudprog-variables for the memory, and mudprog-bus for the vetoes and the watching.
One promise before we start, the same promise the basics chapter makes: nothing in this chapter can break the game. A script with a mistake in it simply does less than you hoped. Attach, test, tweak, repeat.
What The Game Already Does Without You
Rogue already has real companion machinery, and it is worth knowing where its edges are, because a script decorates that machinery rather than replacing it.
Players can own live pets through the pet system: bought from merchants, called to their side with pet call, sent home with pet dismiss, fed and groomed and walked with their own care commands. Classes get companions of their own: a warrior's squire, a mage's familiar, a bard's fans, a necromancer's grisly staff. Mounts have a system too, with ownership and riding and all the saddle-level plumbing. All of that is code, not script, and none of it is your problem. You cannot script a pet into existing, and you do not need to.
What the machinery lacks is the part players actually remember: a voice. The pet system moves a wolf from room to room; it does not make the wolf growl when someone raises a hand to its owner. The mount system carries a rider; it does not give the horse an opinion about the rider's posture. That gap, the entire gap, is yours, and this chapter is the tour of it.
A practical note on where companion scripts live. Attach them to NPCs you build into your own areas, with mudprog <mob> edit exactly as the basics chapter taught: a hound cloned into your cemetery, a cat lounging in your tavern, a raven perched in your tower. A creature summoned by the pet system is rebuilt fresh each time it is called, so a script attached by hand to one of those evaporates when it is dismissed; script the residents of your rooms, not the contents of someone's kennel. And as always, a builder can attach a script to themselves with mudprog self, which turns out to matter for exactly one trigger in this chapter, as you will see.
The Companion Triggers At A Glance
A dozen triggers do almost all the work in this chapter. Here is the whole cast, each with what sets it off and what the dollar codes hold at that moment. Everything that follows is these twelve, combined and recombined.
GREET_PROG - a player walked in. Fires about a second after
arrival, once per arriving player. $n is the
arrival. Every pet's front door.
RAND_PROG <pct> - rolls the percent on each of the mob's
heartbeats. The idle-life trigger: dozing,
pacing, grooming.
SPEECH_PROG - someone spoke nearby. $n the speaker, $g the
line. Header keywords choose which lines. This
is how pets take commands: stay, heel, come.
FOLLOW_PROG - someone started following the scripted mob.
$n is the new follower.
UNFOLLOW_PROG - they stopped. $n is the deserter.
RIDE_PROG - someone climbed onto the scripted mount. $n is
the rider.
RIDING_PROG - fires on the RIDER's own script when they
mount; $t is the mount. The other side of the
same saddle.
GIVE_PROG - a player pressed an item into the mob's paws.
$n the giver, $o the item. Feeding time.
CONSUME_PROG - someone nearby ate or drank, and the scripted
mob witnessed it. $n the eater, $o the food.
Begging time.
DAMAGE_PROG - the mob was struck. $n is the striker.
LOOK_PROG - a player looked at the mob. $n is the looker.
The window into a mood system.
REGMASK_PROG - the mob SAW a line of text matching a pattern.
The all-seeing eye: this one trigger powers
following, guarding, and being petted.
Two more make appearances from the message bus chapter: CNCLMSG_PROG ATTACK, the veto that lets a bodyguard hound catch a blow before it lands, and the ONCE_PROG spawn trigger, which a mount uses to configure itself. If a header above says nothing about an argument, give it a percent chance; 100 means always.
The smallest companion in the world is one reaction block. Attach this to any mob and fire it with mudprog <mob> test GREET_PROG:
GREET_PROG 100
emote looks up hopefully, in case the newcomer is the sort who carries biscuits.
~
That is not much of a dog yet. But notice the shape: the game notices the moment, the trigger hands it to you, and your one line supplies the wanting. Now let us build companions, ten of them, each a little more alive than the last.
A Word About Who Follows Whom
Before the first full script, one honest piece of orientation, because it is the first question every builder asks: how do I make my hound follow a player around?
Following on Rogue is a bond between a leader and a follower, and the bond is forged by game systems, not by a script command. When a player calls a pet to heel, when a warrior summons a squire, when a familiar answers a mage, the SYSTEM ties the creature to the player, and from then on the creature trails them from room to room automatically. There is no mp command that creates that bond, and this is deliberate: a script that could lash any creature to any player would be a griefing kit.
The two follow triggers watch that bond from the leader's side. FOLLOW_PROG fires on a scripted object when something starts following IT; UNFOLLOW_PROG fires when the follower gives up or is dismissed. Put plainly: the script goes on the one being followed. Since the live systems bond creatures to PLAYERS, the being followed is usually a player, which is why the follow triggers are the one place where mudprog self earns its keep: a creator running an event character can script their own FOLLOW_PROG and greet each stray that falls in behind them. On your own built NPCs, the pair fires whenever code makes something follow them, and it hand-tests perfectly with mudprog <mob> test FOLLOW_PROG, so Script 2 below gives the pair its proper voice.
And the hound? The hound does not need the follow system at all. A script can watch its owner leave the room and simply walk the same way, which is Script 3, the shadow-walker, and one of the best tricks in this whole guide. Patience; dogs first.
Script 1: Bramble, The Dooryard Hound
The problem: a room with a dog-shaped statue in it. The smallest fix is the same trio the basics chapter teaches for every mob: a greeting, some idle life, and one drop of memory so regulars are treated like regulars.
GREET_PROG 100
emote scrambles up from the doorstep, tail beating out a welcome.
if !var($n bramble_met)
mpsetvar $n bramble_met 1
emote circles the newcomer twice, taking a long and thorough census of smells.
else
emote greets an old friend with a shove of his blunt head against $n's knee.
endif
~
RAND_PROG 5
emote sighs, flops over, and lets the sun get at his belly.
~
RAND_PROG 5
emote chases something magnificent through his sleep, paws twitching.
~
Walk through it. GREET_PROG fires for each player who walks in, about a second after they arrive so the emote lands after the room description rather than tangled inside it. The first emote is unconditional: everyone gets the tail. Then the if asks a question. var($n bramble_met) reads a small stored note called bramble_met off $n, the arriving player; the exclamation mark means NOT, so the line reads "if this player does NOT yet carry my note". A stranger carries no note, so Bramble writes one with mpsetvar and performs the full census. Ever after, the note exists, the else branch runs, and the player is family. Because the note is written on the PLAYER, it survives reboots and it survives Bramble himself; a respawned Bramble still knows everyone the old Bramble knew. That one choice, player note rather than mob note, is the difference between a dog and a goldfish, and there is a whole interlude on it shortly.
The two RAND_PROG blocks each roll five percent on every heartbeat. When a mob has several blocks for the same trigger, each block rolls separately, so over a few minutes both flavors of idleness appear in whatever order the dice choose. Two small blocks beat one big one here: variety without repetition.
Variations to try. Add a third RAND_PROG with weather flavor, using if isweather(rain) from the functions chapter, so Bramble smells wet-dog in the rain. Or give the census branch a follow-up with mpsleep 3 and a second emote, so the inspection takes visible time; mpsleep is explained fully at the messenger bird, but all it does is pause the script where it stands.
Test it exactly as the basics chapter taught: attach with mudprog hound edit, then walk out and in, or fire mudprog hound test GREET_PROG to play the arrival yourself.
Script 2: Umbra, Who Notices Being Followed
The problem: the follow bond exists, and nothing marks the moment. When something begins following a scripted creature, FOLLOW_PROG fires with the follower in $n; when the bond breaks, UNFOLLOW_PROG fires the same way. The pair is the smallest complete drama in the game: hello and goodbye.
FOLLOW_PROG 100
emote pours itself upright and falls into step at $n's heel.
say Where you walk, I walk. Mind the puddles for both of us.
~
UNFOLLOW_PROG 100
emote stops dead in the road, ears flattening.
say So. Alone again. I had almost decided to like you.
~
Umbra is written as a talking shadow-cat, because a companion that speaks only at bonding and parting feels genuinely momentous; if your creature should not talk, swap the say lines for emotes and the shape is unchanged. The scripting is nothing you have not seen: two triggers, $n for the other party, one beat of body language before each line so the words land on a moving creature instead of a statue.
The honest note from the orientation above applies here: on today's Rogue the follow bond is created by game systems that bond creatures to players, so on a built NPC this pair mostly speaks when code ties something to it, and when you fire it by hand. Both blocks hand-test cleanly, mudprog umbra test FOLLOW_PROG and mudprog umbra test UNFOLLOW_PROG, with you cast as the follower. Write the pair anyway on any creature meant to be bonded with: scripts outlive wiring, and the day a taming skill ships, every Umbra already has its lines.
One more wrinkle worth knowing while we are near the follow system: a creature carrying the no follow property refuses all bonds, which is how you keep a scripted fixture, a statue, a ghost, a talking door, from being led away by clever players. That is set in the NPC file, not the script, but knowing it exists saves an afternoon of confusion.
Interlude: The Two Kinds Of Memory
Every companion in this chapter remembers something, and the engine gives you two different places to keep memories. Choosing the right one is half of companion craft, so take the interlude before the scripts get ambitious.
The first kind is the stored note: mpsetvar <who> <name> <value> writes it, the var() function reads it, and the long form $<who name> drops it straight into text. Notes are invisible bookkeeping; nothing shows on the character, and only scripts that know the name can read it. Bramble's bramble_met is a note. Notes written on a PLAYER are saved with the character and survive everything. Notes written on a MOB die with the mob, and mobs die a lot: reboots, respawns, area resets. The rule of thumb: facts about a player go on the player; moods of the moment may live on the mob, because a mood SHOULD reset when the world does.
The second kind is the property: mpset <who> <property> <value> writes it, and reader functions like mood() and hastag() look at it. Properties are the same shelves game code keeps its own labels on, which is exactly why the mood() function exists: mood is a real property with a real reader, made for exactly the kind of state machine the cat at the end of this chapter runs. The practical difference from notes is small but real: properties are the shared, game-visible shelf, notes are the script-private one. When a purpose built function like mood() exists, use the property and enjoy the readable script; for everything private, prefer notes with distinctive names, bramble_met and not met, because every script in the game shares the same shelf on each player.
There is a third kind of memory, and it is the sneaky one: a timed condition. mpcondition <who> <id> <type> <seconds> applies a real buff or debuff that expires BY ITSELF, and the affected() function asks whether it is still running. That makes conditions the perfect kitchen timer: no arithmetic, no cleanup, the fact simply stops being true at the right moment. The feeding script below runs its entire appetite on one condition, and the cat uses another to decide how long a sulk lasts. When you catch yourself wanting to store a timestamp in a note and compare clocks, stop; you wanted a condition.
Script 3: Patch, The Shadow-Walker
Now the trick the whole chapter is named for: a hound that follows its mistress from room to room, with no follow bond, no code, nothing but script. This is the most advanced script so far in this guide, and it is worth every line, so we will take it slowly.
The insight is simple. When Mira walks west, everyone in the room sees one line of text: Mira leaves west. A scripted mob is part of everyone. REGMASK_PROG is the trigger that fires when the mob SEES text matching a pattern, and the seen line itself arrives in $g. So the hound watches for Mira leaves, reads the direction out of the line, and walks the same way with mpwalkto. She leads; text follows; the dog follows the text.
GREET_PROG 100
emote lifts his head from his paws and files the new arrival under mostly harmless.
~
REGMASK_PROG Mira leaves
if var($i stay) == 1
return
endif
if strin(northeast $g)
mpwalkto northeast
return
endif
if strin(northwest $g)
mpwalkto northwest
return
endif
if strin(southeast $g)
mpwalkto southeast
return
endif
if strin(southwest $g)
mpwalkto southwest
return
endif
if strin(north $g)
mpwalkto north
return
endif
if strin(south $g)
mpwalkto south
return
endif
if strin(east $g)
mpwalkto east
return
endif
if strin(west $g)
mpwalkto west
return
endif
if strin(up $g)
mpwalkto up
return
endif
if strin(down $g)
mpwalkto down
endif
~
SPEECH_PROG stay
if isname($n mira)
mpsetvar $i stay 1
emote sinks onto his haunches, obedient to the letter if not the spirit.
endif
~
SPEECH_PROG heel come
if isname($n mira)
mpsetvar $i stay 0
emote springs up, all forgiveness, and returns to his mistress's side.
endif
~
Take the big block apart one idea at a time.
The header first: REGMASK_PROG Mira leaves. A REGMASK header is a pattern matched against every line of text the mob perceives, and unlike keyword headers it is CASE SENSITIVE, so write the owner's name with its capital letter exactly as it appears on screen. The pattern here is the two words Mira leaves, which appear together in exactly one kind of line: the departure message. That is the bond. Swap Mira for the name of whoever this hound belongs to and the script is theirs. Resist the temptation to shorten the pattern to just leaves; that hound faithfully follows EVERYONE who exits, which is a comedy the first time and a bug report the second.
The first thing the body does is check an off-switch: if the stored note stay on the hound himself ($i, remember, is always the scripted creature) holds 1, the script hits return, and return simply stops the script on the spot. You met return in the flow chapter as a way out of loops; here it is the hound declining to move. This note lives on the MOB quite deliberately, the interlude's rule in action: if the world resets and a fresh Patch spawns, he should be following his mistress, not honoring a stay order given to a dog that no longer exists.
Then comes the ladder of direction tests. strin(northeast $g) asks whether the text northeast appears anywhere inside $g, the seen line, and $g always arrives in lower case, so the tests are written lower case. On a hit, mpwalkto steps the hound that way, and return ends the script immediately. That return is not politeness; it is load-bearing, and so is the ORDER of the ladder. The word east hides inside northeast. If the plain east test ran first, a mistress striding northeast would send her hound trotting due east, and the two would part company forever at the first diagonal. So the four compound directions are tested first, each one bailing out with return before the short words get a chance to lie. When you extend the ladder, and you can, with in and out or the exits of your own area, always put the longer word above any word hiding inside it.
The two SPEECH_PROG blocks are the leash. A speech header is a list of keywords, and the block fires when any spoken line nearby contains one of them. isname($n mira) then checks that the SPEAKER is Mira, because a well-trained hound does not sit for hecklers; without that guard, any passerby could park your dog by shouting stay. Notice the second header carries two words, heel come, so either word frees him.
Now the honest edge cases, because this pattern has real ones and a builder who knows them is never surprised.
First, keyword headers match SUBSTRINGS. The word come hides inside welcome, so Mira greeting a friend with a cheerful welcome will also recall her dog from a stay. Harmless here, and honestly rather sweet, but remember the principle when you pick command words: choose words that do not hide inside common speech, or use the phrase form the triggers chapter teaches, a header reading p heel boy, which matches only the whole phrase.
Second, the hound follows the TEXT, not the woman. If Mira leaves by a magical exit whose departure message is custom prose with no direction word in it, the ladder finds nothing and the hound is left behind, staring at a shimmer. He also cannot open doors, and a locked gate strands him with dignity. Treat that as texture, not failure; a dog waiting at a door is a story.
Third, do not chain shadows. Two hounds both scripted to follow Mira work fine side by side. But a hound scripted to follow ANOTHER HOUND works exactly once, because the moment both move, each sees the other leaving and they orbit each other down the corridor like a small furry binary star. One mistress, many dogs: fine. Dog following dog: no.
Fourth, if this creature is ALSO bonded through the real follow system, pick one mechanism and turn the other off; a creature that is both led by code and shadowing by script will occasionally try to move twice. The script version exists precisely so your built NPCs need no bond at all.
Test it the honest way: attach, then walk out of the room and watch the doorway behind you. For the desk test, mudprog patch test GREET_PROG proves the script parses and greets; the REGMASK block only speaks during real movement, because only real movement produces the line it watches for.
Script 4: Fang, The Bodyguard
The problem: a companion that loves its owner should MIND when someone swings at her. Two mechanisms together make a guardian: the all-seeing REGMASK for the growl, and a message-bus veto for the interception.
A word of honest orientation before the script. The bus chapter lists which action codes fan out to observers after the fact, and ATTACK is not one of them; you cannot watch fights start through EXECMSG_PROG, and the chapter explains why: combat has richer triggers of its own. For a bystander who wants to REACT to violence, the tool is REGMASK, because the one thing every attack does is print a line with the word attacks in it. And for a bystander who wants to PREVENT violence, the tool is CNCLMSG_PROG ATTACK, the cancel pass, which runs your block INSTEAD of the attack. A bodyguard wants both: a growl for the mood, and a body thrown in front of the blow for the loyalty.
GREET_PROG 100
emote rises and puts himself squarely between his mistress and the newcomer.
~
REGMASK_PROG attacks
emote bares his teeth, a low thunder rolling up out of his chest.
~
CNCLMSG_PROG ATTACK mira
mpechoat $n A wall of bristling muscle slams into you, and your blow goes wide.
mpecho $I throws himself in front of Mira, taking the strike on his own shoulder.
mpkill $n
~
DAMAGE_PROG 100
emote snaps back at whatever hurt him, giving no ground at all.
~
The REGMASK block is the mood. Its pattern is the single word attacks, so any combat line the hound sees, anyone attacking anyone, raises the thunder. One caution carried over from the bus chapter, and it matters every single time you write a REGMASK: the block's own output must NOT contain the pattern. If Fang's growl mentioned attacks, he would see his own growl, growl at it, see that growl too, and rumble in a loop until the engine's step limit cut him off. Read your reaction lines out loud against your pattern before you attach; ours says thunder and teeth and never the watched word.
The CNCLMSG block is the loyalty, and it is worth reading closely because the cancel pass inverts the usual script logic. The header names the bus code ATTACK and then a mask, mira, which for attacks is matched against the VICTIM's name. So the block fires exactly when someone in the room tries to start a fight with Mira. Because this is the cancel pass, the attack now simply does not happen; your block runs in its place, which means your block owes the room an explanation. The mpechoat line tells the attacker privately what stopped them; the mpecho line shows everyone the dog doing it; and then mpkill $n turns the hound on the aggressor, so the fight the attacker wanted happens after all, just not with the target they chose. $n in a cancel block is the ACTOR of the cancelled action, the attacker, which is exactly who a bodyguard should be biting.
DAMAGE_PROG rounds him out: struck, he answers with defiance rather than script logic, and the game's own combat AI does the rest. Note what is NOT here: no health checks, no damage math, no target juggling. The combat system fights; the script only feels.
Variations to try. Give the growl a threshold of dignity with a level() check from the functions chapter, so Fang does not bother growling at fights between sparrows. Add a second CNCLMSG_PROG ATTACK block masked with the hound's OWN name, so would-be dog-kickers get caught by the same wall of muscle. And if Mira would rather her hound merely threatened, delete the mpkill line; a veto block with no counterattack is a perfect deterrent, endlessly patient, entirely bloodless.
Testing an interception honestly requires an attacker, so borrow a second character or a colleague and have them swing at the owner. The hand test mudprog fang test CNCLMSG_PROG will report that no block fired, and the bus chapter explains why: a hand test carries the word test as its message, and this block's mask is waiting for the word mira, so the mask does its job against the wrong message. That silence is correct behavior, not a broken script.
Script 5: Gristle, Who Must Be Fed
The problem: every pet ever written begs, and almost none of them can actually be fed. The GIVE_PROG trigger closes the loop: when a player presses an item into the mob's paws, the block fires with the giver in $n and the gift in $o. Add the CONSUME_PROG witness trigger, which fires when the mob SEES someone eat or drink nearby, and you get the full canine economy: begging, receiving, and gratitude with a timer on it.
GIVE_PROG 100
if isname($o meal)
emote snatches the gift out of your hands and bolts it down in two wet gulps.
mpjunk $o
mpcondition $i well_fed buff 600
emote subsides into a warm, grateful heap against your leg.
else
emote noses at the offering, sneezes, and looks up at $n with flat disappointment.
endif
~
CONSUME_PROG 100
if !affected($i well_fed)
emote appears at $n's elbow, eyes tragic, the very portrait of famine.
endif
~
RAND_PROG 6
if affected($i well_fed)
emote drowses in the warmest patch of floor, ribs rising and falling in deep content.
else
emote paces a slow circuit of the room, nose down, checking twice anywhere food has ever been.
endif
~
The GIVE block first asks WHAT it was handed. isname($o meal) asks whether the object answers to the name meal, which the standard test meal does, along with most cooked food in the game; a stricter or looser dog just checks different names. Real food gets the full performance, and then two commands you should study. mpjunk $o destroys the gift, because the give already happened and the meal is inside the hound in the inventory sense; mpjunk makes it inside the hound in the biological sense. Never skip this line, or your pet slowly fills with uneaten sandwiches like a horrifying pinata. Then mpcondition applies a buff named well_fed to the hound himself for six hundred seconds, and that one line is the whole appetite clock: no counters, no arithmetic, just a fact with an expiry date, exactly as the interlude promised.
Everything else reads the clock. The CONSUME block fires when the hound watches anyone eat or drink nearby, but affected($i well_fed) mutes the begging while the buff runs; a fed dog watching you eat is a philosopher, an unfed one is a tragedy, and the script knows the difference. The RAND block gives him two idle lives, drowsing while fed, prowling while not, so a player can tell the state of the dog at a glance without a single number shown anywhere.
What it was handed and what it refuses deserve one more beat. The else branch runs for every non-food gift: swords, boots, love letters. The sneeze is a fine refusal, but note honestly that the give itself still happened; the boot is in the dog. The room where this chapter deals with that properly is the cat script later, whose gift standards are enforced BEFORE items change hands, with a cancel block; for a good-natured hound, an occasional swallowed boot is in character, and a builder can always shake it out of him in the editor.
Also honest: the header of a GIVE_PROG is not the place for item names. Speech-style headers filter on a message, and the give event carries none, so a header like GIVE_PROG meal quietly fires for everything. Filter gifts the way this script does, with isname($o ...) in the body, always.
To feed him for real, clone a meal and hand it over: mpoload /obj/meal in a scripttest line, or simply buy food, then give meal to gristle. The desk test mudprog gristle test GIVE_PROG carries no item at all, so $o is empty, isname finds nothing, and you get the sneeze; that is the else branch doing its job against an empty-handed test, and it is how all your give scripts will behave under the hand test.
Variations to try. Track devotion alongside appetite: an extra note per feeding, mpsetvar $n gristle_fed and a running count as in the shopkeeper chapter's ledger, with a milestone emote at ten meals. Or teach him table manners with a second condition, gristle_shamed, applied by a SPEECH_PROG bad dog block, muting the begging for an hour.
Script 6: Velvet, Who Does Not Know You
The problem: a pet that treats its owner and the general public exactly alike is not a pet, it is furniture that moves. The engine's tool for "only these people" is the zapper mask, a header form you have not needed until now, so here is the whole idea from the beginning.
Most trigger headers you have written hold a percent chance. A zapper mask replaces the chance with a DESCRIPTION OF WHO QUALIFIES, written as dash-clauses: -name +mira fires only when the acting person answers to mira; -class +ranger +druid only for those classes; -level 30 only for level thirty and up, and -level 10-20 for a range; -race, -sex, -deity likewise; and the two bare clauses -player and -npc pass only real players or only mobs. Clauses combine with AND: every clause listed must pass, or the block stays silent. The person being tested is always the SOURCE of the trigger, the one who spoke or arrived or gave.
Velvet is a cat of consequence who answers to exactly one person.
GREET_PROG -player
emote opens one eye, conducts a swift audit of the new arrival, and closes it again.
~
SPEECH_PROG -name +mira
if strin(come $g)
emote flows down from her cushion and winds, purring, around Mira's ankles.
endif
~
SPEECH_PROG 100
if !isname($n mira)
if strin(come $g)
emote turns her face away with the full aristocratic weight of her contempt.
endif
endif
~
Three headers, three lessons.
The greeting header is the gentlest zapper there is: -player, no values, meaning the acting person must be a real player. On a greeting this is training wheels rather than protection, an honest confession: arrivals that greet are already players, so this clause always passes. It is here so you can watch a zapper PASS before you meet one that filters. Swap it for -level 20 and the audit is reserved for people of consequence; swap it for -race +elf and Velvet acquires opinions about ancestry. The header is the bouncer; the body never even hears about the people the bouncer turns away.
The second header is the owner's private channel. -name +mira means the block only even considers lines spoken by Mira, before the body runs at all; the body then listens for the word come inside her line with strin. Splitting the work this way, mask for WHO, strin for WHAT, is the cleanest obedience pattern in the engine: one glance at the header tells you whose voice matters.
The third block is the deliberate mirror, and it teaches the one thing zappers cannot do: there is no NOT clause, no mask meaning "everyone except Mira". When you need the complement, take the everyone header, 100, and guard the body with the negated question, if !isname($n mira). Now watch the two blocks together when Mira herself says come: both headers pass for her, block two purrs, and block three's guard finds she IS mira and stays silent. A stranger saying come reaches only block three and receives the back of a cat's head. Two blocks, opposite gates, no gaps and no double-fires; copy this pairing every time a companion needs an inside voice and an outside voice.
What happens when someone RENAMES a pet, or your owner plays an alt? The mask tests names, not souls. -name +mira follows the name: an alt named Marn is a stranger to Velvet, which is almost always what you want in a world of disguises, and worth one honest sentence in your area notes when you hand the cat to a player whose name may change.
Test note: the greeting hand-fires, since you are a player and -player passes for you. The obedience block will NOT hand-fire unless your own test character happens to be named mira; that silence is the mask working. To hear the purr, say come while wearing the right name, or temporarily widen the mask while building, -player instead of -name +mira, and narrow it before you ship.
Script 7: Sable, The Mount With Opinions
The problem: a horse that is scenery with a saddle. Two triggers belong to the saddle. RIDE_PROG fires on the scripted MOUNT when someone climbs aboard, rider in $n. RIDING_PROG is its mirror on the RIDER's own script, mount in $t, and gets its own tiny section after this one. Around those two, a mount is just a companion like any other: it hears speech, it takes damage, it sees text, and every tool this chapter has taught applies at saddle height.
One honest sentence about the machinery first, in the spirit of the shop chapter: whether a creature CAN be mounted at all is the mount system's business, governed by ownership properties on the creature, and the riding experience itself belongs to that system, not to scripts. What belongs to you is the voice. Conveniently, the properties involved are ordinary properties, so a script can set its own on spawn, which is what the ONCE_PROG block below does: it runs once when the mob first loads, and configures Sable as a mountable pet belonging to mira. Swap the name for your rider's, or delete that block entirely if your area code already sets the creature up.
ONCE_PROG
mpset $i is_pet 1
mpset $i is_mountable 1
mpset $i mount_owner mira
~
RIDE_PROG 100
emote takes the saddle's weight with one rolling step, then turns a long black head to study her rider.
say You sit like a sack of turnips. We will work on it.
~
SPEECH_PROG faster run test
say Faster, she says. Legs she has two of, and opinions.
emote gathers herself and stretches out into a longer stride anyway.
~
DAMAGE_PROG 100
say Whoever that was, I am stepping on them next pass.
~
REGMASK_PROG dismounts from
emote shakes herself from ears to tail, glad of the lighter back.
say Off already? We were nearly getting somewhere.
~
The RIDE block is pure arrival theater, and notice it fires at the one moment a rider is guaranteed to be paying attention. Mounts talk back best at transitions: climbing on, climbing off, being hurt. Fill those three and the creature feels alive even if it never speaks otherwise.
The speech header carries a trick worth stealing for every keyword block you ever write: its word list ends with test. The builder command mudprog sable test SPEECH_PROG fires the trigger with the word test as the heard line, so a keyword block that includes test among its words can always be fired from the desk, no live rider required. Leave the word in while you build; strip it out when you ship, and the block answers only to faster and run again.
The dismount farewell is our REGMASK friend again, because climbing off prints a line containing dismounts from and the mount, of course, sees it. Check the loop rule as always: neither of Sable's farewell lines contains the pattern, so she farewells exactly once. And note there is NO dismount trigger to forget about; the text watcher IS the dismount trigger, one more example of REGMASK quietly filling every gap in the trigger list.
Variations to try. Give her a rider ledger: a note per rider name on the PLAYER, sable_ridden, counting mounts, with the turnip line reserved for the first ride and grudging respect by the tenth. Gate a second, kinder RIDE_PROG block with a zapper, -class +ranger, so rangers alone get a soft snort of approval. And a TIME_PROG block from the triggers chapter closes the stable day: at the twentieth hour she announces she is off duty, whatever anyone's plans.
The Rider's Side Of The Saddle
RIDING_PROG completes the pair, and it fires on the RIDER, which makes it the second and last trigger in this chapter that belongs on a scripted PLAYER: a creator running an event character, or a builder dressing their own arrivals. On the rider's script, $t is the mount.
RIDING_PROG 100
emote settles into the saddle and takes up the reins like someone born to them.
~
Attach that to yourself with mudprog self append and your every mounting is suddenly cinema. It hand-tests with mudprog self test RIDING_PROG, and it is honestly most useful exactly there, as event-character polish; a mount with a good RIDE_PROG carries the scene for both of you.
Script 8: Peck, The Messenger Bird
The problem: your words are stuck in the room you say them in. The messenger bird is the classic cure, and it teaches the two commands that give scripts a sense of TIME and PLACE: mpsleep, which pauses a script where it stands and resumes it later, and mpat, which runs one command as if the host were standing in another room, then brings it home in the same breath. Between them, a script becomes a little journey instead of a single instant.
SPEECH_PROG message word test
emote cocks its head at the speaker, then spreads slate-grey wings.
mpecho $I flashes up and out of sight between the rooftops.
mpsleep 2
mpat /realms/loralei/aurin/rooms/room1 say News on the wing: $G
mpsleep 1
mpecho $I drops back onto its perch in a ruffle of feathers, errand done.
say It is spoken in the Center of Town, exactly as you said it.
~
Follow the journey line by line. The header listens for the words message or word in nearby speech, plus our builder's test word from the mount section, so the desk test always works; a player saying take a message to the square launches the errand. The first two lines are immediate: the head-tilt and the departure, seen by everyone in the bird's room. Then mpsleep 2 stops the script for two seconds. Nothing about the world pauses, only this script; the room chats on, the bird is notionally in the air.
When the script wakes, mpat does the delivery. Its first word is a room path, the same slash-path a room file has, and everything after the path is one ordinary command performed THERE. The host truly does visit that room for the length of one command, so the say is heard by whoever stands in the Center of Town, and then the bird is home again before its own room misses it. The delivered line leans on $G, which is the heard speech in its ORIGINAL case, capital letters preserved, where little $g is the same line lowered; messengers should quote exactly, so messengers use $G. One more mpsleep for the return flight, and the bird lands with a report.
Two honest design notes. First, this bird repeats the WHOLE heard line, trigger word and all; a message beginning take a message arrives as take a message, which is charmingly literal and exactly what birds would do. Second, the bird announces to a fixed address. That is the nature of the pattern: the destination is written into the script, so a messenger is really a pair of fixed points with wings between them. Put the path of YOUR area's plaza in place of the Center of Town and Peck serves your streets instead.
If you want the bird to be VISIBLY absent, the flight becomes real travel: the commands chapter's mpgoto moves the host one way and leaves it there, so a bird can mpgoto out, linger through an mpsleep in the far room, and mpgoto back to its home perch by path. Write the home path in yourself, a bird knows its own roost, and be sure every road leads home again, or your messenger ends its career wherever the script stopped. The mpat form above is the forgiving one, away and back in one line with nothing to strand, and it is the right first version of every courier you build.
Delay work like this has one more rule worth carrying off: everything after an mpsleep happens LATER, and the world may have changed. The speaker may have walked away; the room may have emptied. Peck shrugs, because his closing lines are room flavor and no one in particular needs to be present. If your delayed lines are aimed at a person, the troubleshooting chapter's advice applies: aim with mpechoat only before the sleep, aim with room-wide mpecho after it.
Variations to try. A homing pigeon for a specific player: gate the header with a zapper, -name +mira, and Peck carries only his mistress's words. A round-trip circuit: several mpat lines to different rooms in sequence, one proclamation each, with mpsleep beats between them, and the town crier problem is solved without the crier ever leaving his post. And for pure atmosphere, the mpasound command from the commands chapter murmurs into ADJACENT rooms without any travel at all: wingbeats overhead, heard one street away.
Script 9: Duchess, A Cat Of Many Moods
The problem: a companion whose reactions never change is a recording. The cure is a mood system: one word of state, stored on the mob, steering every other block. This is the chapter's variables-and-flow showcase, and it earns the fanciest tool in the flow chapter, the switch.
The design first, because mood systems live or die on their design. Duchess has three moods: playful, sulking, and everything else, which we treat as her default composure. Exactly two things change her mood: affection makes her playful, and unworthy gifts start a sulk. Sulks end on a timer, because that is the fundamental truth of cats. The mood lives in the mood property, written with mpset and read with the purpose-built mood() function, following the interlude's rule: when a purpose-built reader exists, use the property. The sulk timer is a condition, following the other rule: facts that expire are conditions.
LOOK_PROG 100
switch $%mood($i)%
case playful
emote pounces on the hem of your cloak and wrestles it into submission.
break
case sulking
emote presents you with a pointed and comprehensive view of her back.
break
default
emote regards you with level green eyes, giving nothing whatsoever away.
endswitch
~
REGMASK_PROG strokes|scratches|pets
mpset $i mood playful
emote arches up into the touch, purring like a mill wheel.
~
GIVE_PROG 100
if isname($o fish)
mpset $i mood playful
mpjunk $o
emote accepts the tribute as no more than her due.
else
mpset $i mood sulking
mpcondition $i cat_sulk debuff 300
emote stares at the offering, then at $n, wounded beyond all expression.
endif
~
RAND_PROG 8
switch $%mood($i)%
case playful
emote ambushes a dust mote with terrible ferocity.
break
case sulking
if !affected($i cat_sulk)
mpset $i mood content
emote decides, in her own good time, that you may be forgiven.
endif
break
default
emote folds her paws beneath her and supervises the room.
endswitch
~
Start with the LOOK block, because it is the window players actually use: look at the cat and the cat's state looks back. The switch line deserves a careful read. You cannot write switch mood($i) bare; the switch compares TEXT, so the function must be wrapped in the substitution form, $%mood($i)%, which the variables chapter introduced: dollar, percent, the function, percent. That wrapping evaluates the function and hands the switch the resulting word, playful or sulking or whatever else, and each case supplies one performance. The default arm catches everything unlisted, which covers both her named composure, content, and the blank mood she has before anything has ever set one; a switch with a default never has a hole in it, which is why the flow chapter tells you to always write one.
The REGMASK block is how she is petted. Rogue players show affection in freeform emotes, someone types that they stroke the cat or scratch her ears, and the resulting room line contains one of those verbs. The pattern here is a real regular expression using the alternation bar: strokes|scratches|pets matches a line containing ANY of the three words. This is REGMASK's third job in one chapter, following a mistress and watching a saddle, and here it is the whole affection system in one header. Loop-check it as always: her purr line contains none of the three watched words.
The GIVE block enforces standards. Fish earns playfulness, by the same isname test the hound used. ANYTHING else starts the sulk: mood set, and beside it the cat_sulk condition applied for three hundred seconds. Note the pairing carefully, because it is the pattern to steal: the MOOD is the state, the CONDITION is the state's clock. Nothing reads cat_sulk to decide how she acts; the mood does that. The condition exists only to expire.
The RAND block closes the loop. Playful idles are kinetic, composed idles are supervisory, and the sulking arm is the clever one: it does nothing at all while the cat_sulk clock still runs, and once the condition has quietly expired, the next idle roll finds the clock stopped, forgives you, and says so out loud. Time healing a mood, in four lines, with no arithmetic anywhere. This is the shape to copy for any decaying state: grudges, fright, excitement, sugar rushes.
Variations to try. More moods are just more cases; add hungry, wire it from a CONSUME witness block like Gristle's, and suddenly the moods interlock. Persist her affections by writing a note on the PLAYER, duchess_beloved, when they reach some count of strokes, and greet beloveds differently forever after. Or let speech tease her, a SPEECH_PROG with the phrase form, p bad cat, that starts a sulk from words alone. The machinery does not care what moves the mood; that is the joy of keeping all state in one word.
The desk test: mudprog duchess test LOOK_PROG meets her before any mood is set and receives the default composure, the level green eyes. Then stroke her, look again, hand her a boot, look again, and watch one script be three different cats.
Script 10: Marrow, The Graveyard Hound
Everything above, in one script on one creature: memory, obedience, the shadow-walk, the bodyguard's veto, appetite with a clock on it, an idle life that reflects his state, and defiance when hurt. Marrow is an old grey hound who lives among the headstones of a cemetery and belongs to a groundskeeper named Mira. He is long, and he should be: he is the proof that the chapter's parts were built to fit together. Read him straight through once, then again block by block; every technique is one you have already met.
GREET_PROG 100
emote raises his grey muzzle from between two headstones and takes the measure of the visitor.
if !var($n marrow_met)
mpsetvar $n marrow_met 1
emote limps over and offers a paw with tremendous, creaking dignity.
else
emote thumps his tail once against the leaning headstone, which is as much ceremony as anyone gets twice.
endif
~
SPEECH_PROG stay
if isname($n mira)
mpsetvar $i stay 1
emote settles between the graves, a patient grey boulder.
endif
~
SPEECH_PROG heel come
if isname($n mira)
mpsetvar $i stay 0
emote rises and shakes the grave dust from his coat, ready to walk.
endif
~
REGMASK_PROG Mira leaves
if var($i stay) == 1
return
endif
if strin(northeast $g)
mpwalkto northeast
return
endif
if strin(northwest $g)
mpwalkto northwest
return
endif
if strin(southeast $g)
mpwalkto southeast
return
endif
if strin(southwest $g)
mpwalkto southwest
return
endif
if strin(north $g)
mpwalkto north
return
endif
if strin(south $g)
mpwalkto south
return
endif
if strin(east $g)
mpwalkto east
return
endif
if strin(west $g)
mpwalkto west
return
endif
if strin(up $g)
mpwalkto up
return
endif
if strin(down $g)
mpwalkto down
endif
~
CNCLMSG_PROG ATTACK mira
mpechoat $n Grey muscle hits you from the side, and your blow finds only air.
mpecho $I takes the strike meant for Mira and turns on her attacker.
mpkill $n
~
GIVE_PROG 100
if isname($o meal)
mpjunk $o
mpcondition $i well_fed buff 600
emote eats with the slow thoroughness of an old dog who has known lean winters.
else
emote sniffs the offering once and looks up, declining with perfect courtesy.
endif
~
CONSUME_PROG 100
if !affected($i well_fed)
emote watches every bite travel from hand to mouth with ancient, patient eyes.
endif
~
RAND_PROG 4
if affected($i well_fed)
emote sleeps against the warm side of a tomb, paws twitching through old chases.
else
emote noses along the cemetery wall, hopeful about nothing in particular.
endif
~
DAMAGE_PROG 100
emote staggers, sets his old legs wide, and holds his ground.
~
A few assembly notes, because combining blocks has its own small craft.
Marrow's states never collide, and that is by design rather than luck. His memory notes live on players, marrow_met; his obedience note lives on himself, stay; his appetite is a condition, well_fed. Three kinds of memory, each in the home the interlude assigned it, and because each block reads only its own state, you can delete any block without breaking another. That independence is what to aim for in a long script: blocks that share a character, not wiring.
Order within the script matters only in one place, the direction ladder, compounds before simples, exactly as in Patch. Between BLOCKS, order is mostly taste; the engine finds the right trigger type regardless of where it sits in the script. Convention from the cookbook: greeting first, then commands, then the watchers, then the appetites, then the idles, so the next builder can navigate.
And notice the restraint. No block is longer than it needs to be, the hound never speaks a word of language, and his grandest technical moment, the attack veto, spends most of its lines on description. Twelve blocks of script, and what a visitor experiences is not machinery but a very old dog with a very clear heart. That is the whole archetype, done.
The What-If Corner
Questions builders actually ask, answered before they cost you an evening.
What if I want the hound to FIGHT beside its owner, not just intercept one blow? Then you have reached the edge of what a companion script should do, and the edge is friendly: once any block puts the hound into combat, as Fang's mpkill does, the game's own combat system runs the fight, swings, defenses, and all. Your script goes back to doing what scripts do best, which is feeling things about it: a FIGHT_PROG block fires every round while the hound brawls and is the place for snarling commentary, and DAMAGE_PROG answers each wound. Round-by-round combat brains are their own archetype with their own deep-dive chapter, mudprog-combatai; a companion needs none of it.
What if the mob dies, or the area resets? The script itself lives on that one copy of the creature, as the basics chapter warns: a respawned Marrow arrives without it until a coder bakes the script into the NPC file, and the example at /domains/examples/npc/mudprog_greeter.c shows the baking. His memory behaves better than you might fear, though, and by design: everything he stored on PLAYERS, met-notes and feeding ledgers, survives anything, because players save. What dies with him is only his own state, the stay note and the well_fed clock, and a fresh hound that wakes hungry and ready to follow is correct in a way no saved state could be.
What if my area has special exits, enter crypt or climb rope, and the shadow-walker is left staring at the wall? Extend the ladder with the exact text of the departure line, and answer with the exact command, because any plain line in a script is simply a game command the creature performs: a block reading if strin(crypt $g) followed by the line enter crypt teaches the hound that trick in two lines. The ladder is yours; the ten standard directions are only the standard start.
What if the pet has TWO beloved owners? Zapper name clauses take several values and pass on any of them: -name +mira +tomas is one header serving both. The shadow-walk pattern widens the same way, because a REGMASK pattern is a real regular expression and the bar means or: a pattern of Mira leaves|Tomas leaves follows whichever of them moves. Just decide in advance what happens when they leave in different directions, because the hound will follow whichever line printed last, and that is a coin toss with fur on it.
What if I want the companion to keep different hours? Every condition the functions chapter offers stacks cleanly onto companion blocks. Gate the idle blocks with isnight() and a cat becomes crepuscular in one line; gate a greeting with isweather(rain) for a dog that judges you for tracking in mud; gate the messenger's header with a zapper so only its owner can send it. Companions are where those little functions pay their rent, one condition at a time, and the recipe stays the same: the trigger says when the moment happens, the condition says whether this creature cares.
What if the mount should refuse an unworthy rider? Today, ownership already does the refusing: the mount system's own gate means a stranger cannot climb into the saddle at all, so your RIDE_PROG only ever meets the owner. The bus codes for riding exist and are reserved, as the bus chapter notes, so scripts cannot veto a mounting the way Fang vetoes a blow; when that wiring lands, the pattern will be exactly Fang's with a different code word. Until then, let the gate do the refusing and spend your lines on the personality, which no gate can supply.
Common Mistakes, And Their Cures
Every one of these comes from a real first draft of a companion. Check your own against the list before you file a bug on the engine.
The forgotten tilde. Every block ends with a line holding only a tilde, and a missing one silently glues two blocks into nonsense. The symptom is a trigger that no longer appears when you view the script with mudprog <mob>; the parsed trigger list at the top of that display is the truth of what the engine sees.
Memory on the wrong shelf. A visit counter stored on the mob resets every reboot; the players notice, and it stings. Facts about players go ON players. The reverse mistake is subtler: an obedience flag stored on the PLAYER makes every hound in the world sit when one is told to stay, because they all read the same note. Marrow stores stay on himself for exactly that reason.
The REGMASK echo loop. A watcher whose own reaction contains its own pattern feeds itself forever, or rather until the engine's step limit stops it and logs a runaway. Read every REGMASK reaction line against its own pattern before attaching. The parrot in the exercises below is this mistake's speech-trigger cousin, tamed with an ispc guard.
Substring surprise. Keyword headers and strin both match inside words: come hides in welcome, sit in visit, east in northeast. Order your direction ladders compounds-first, choose command words that do not hide inside table talk, and reach for the phrase form, p heel boy, when a word list keeps misfiring.
Item names in the header. GIVE_PROG headers do not filter by gift; the give event carries no message for keywords to match, so a header like GIVE_PROG meat fires for everything and the builder swears the mask is broken. Gifts are filtered in the body with isname($o ...), every time.
Forgetting mpjunk. A feeding pet without mpjunk accumulates the groceries of a hundred players. If your hound has been live for a week and rattles when he walks, this is why.
Vetoes with no voice. A CNCLMSG block replaces the action it cancels, including all the messages the action would have printed. A veto that does not narrate leaves the player staring at a silent refusal, certain the game is broken. Fang and Marrow both spend two lines explaining the wall of muscle; so should anything of yours that cancels.
Expecting the hand test to defeat masks. mudprog <mob> test <TRIGGER> fires with YOU as source and the word test as the message. Blocks gated to another name, another class, or other keywords rightly stay silent; that is the gate working. The cures are in this chapter: the test keyword trick for word lists, temporarily widened zappers for masks, and a colleague for the bus blocks.
The over-chatty idle. RAND_PROG percentages add up across blocks: four blocks at 10 make a mob that performs every few seconds, and players mute it in their hearts. Keep the TOTAL of all idle chances under about ten percent; the smallest numbers make the most beloved pets.
Two performers, one audience. Two scripted companions in one room can answer each other: a parrot and a mimic, a beggar and a beggar. Any script that REACTS to speech or text should either guard with ispc($n) or watch for words no script of yours ever says. The binary star of Patch's third edge case is the movement version of the same law.
Exercises
Four exercises, each a small companion complete in itself. Build your own before reading the worked answer; every one uses only tools from this chapter.
Exercise one: the shoulder parrot. A parrot that repeats whatever players say near it, and does NOT get into a shouting match with other scripted birds. The trap is in the second clause.
A worked answer:
SPEECH_PROG 100
if ispc($n)
say $G? $G, is it? Rrk!
endif
~
The header hears everything; the guard is the whole lesson. ispc($n) is true only when the speaker is a real player, so the parrot repeats people and ignores mobs, including other parrots, including ITSELF should the engine ever route its own squawk past its own ears. Without the guard, two parrots in one room repeat each other into the step limit. $G preserves the speaker's capitals, which matters to a bird this pedantic.
Exercise two: the one-person welcome. A hound whose greeting for its owner is joy unconfined, and for everyone else is professional suspicion. One trigger, two faces.
A worked answer:
GREET_PROG 100
if isname($n mira)
emote explodes into a full-body wag, tail a blur of pure joy.
else
emote watches the newcomer with polite, professional suspicion.
endif
~
The if-else split inside ONE block is the right shape here, not two blocks with opposite gates, because a greeting always fires for everyone and simply changes tone. Compare Velvet's speech pairing, where the zapper gate meant the owner's block never even considered strangers: gates for privacy, branches for tone.
Exercise three: the feeding ledger. A hound that counts, per player, how many times that player has fed it, mentions the count, and declares the player family at the fifth gift. Seed before you add; the variables chapter's counting idiom.
A worked answer:
GIVE_PROG 100
if !var($n patch_fed)
mpsetvar $n patch_fed 0
endif
mpsetvar $n patch_fed $%math($<$n patch_fed> + 1)%
emote accepts the gift with grave ceremony. By his count, that makes $<$n patch_fed>.
if var($n patch_fed) == 5
emote decides, then and there, that $n is family now and forever.
endif
~
The seed-if guards the first gift: adding one to a note that does not exist yet yields zero, not one, so the counter is created at 0 before the arithmetic touches it. The count lives on the PLAYER, so it survives reboots and respawns and is private to each giver. The milestone uses == 5 rather than >= 5 on purpose: the declaration of family should happen exactly once, at the fifth gift, not at every gift thereafter.
Exercise four: the begging shadow. A creature that appears at the elbow of anyone who eats or drinks nearby. One block, one witness trigger.
A worked answer:
CONSUME_PROG 100
emote materialises at your elbow with the silent skill of the professionally hungry.
~
CONSUME_PROG is the witness form: it fires on a scripted mob that SEES eating or drinking, eater in $n, food in $o. The refinement worth making yourself is Gristle's: mute it with a well_fed condition while recently fed, or the shadow attends every single meal in the tavern, and what was funny at lunch is a siege by dinner.
Testing Your Companions
The desk pass first. mudprog <mob> shows the script and, crucially, the parsed trigger list; if a block you wrote is not in that list, it never parsed, and the culprit is almost always a missing tilde. Then fire each trigger by hand: mudprog <mob> test GREET_PROG, test GIVE_PROG, test LOOK_PROG, and so on. The hand test casts you as the source and carries the word test as its message, which is why this chapter keeps test in keyword headers while building, and why give-branches sneeze at your empty hands: the else side of a gift test is exactly what an itemless fire should produce.
Then the live pass, because companions are about moments the desk cannot fake. Walk out of the room and watch for the shadow at your heel. Hand over real food, mpoload /obj/meal and give it across. Emote affection at the cat and look at her after. Have a colleague swing at the owner and meet the wall of muscle. Speech blocks answer a plain say. For anything delayed by mpsleep, count the seconds with your hand off the keyboard; the pause is the point.
For raw experiments without touching a mob, scripttest runs script lines on yourself, and its runfile form exists because dollar signs typed at a telnet prompt arrive mangled; anything with a $ in it should live in a file or on a mob, not on the command line. When a script misbehaves in ways you cannot see, the troubleshooting chapter is the place to go next; its first advice will be the trigger list and the tilde, and its second will be mplog, the script's way of writing yourself a note in the server log.
The Archetype In One Page
The voice GREET_PROG for arrival, RAND_PROG under ten
percent total for idle life, LOOK_PROG for the
close-up.
The bond FOLLOW_PROG and UNFOLLOW_PROG on whatever is
followed; the shadow-walk REGMASK for a scripted
creature that trails its owner by text alone.
The obedience SPEECH_PROG keywords plus isname guard, or a
zapper header for the owner's private channel;
state in a note on the MOB.
The loyalty REGMASK on the word attacks for the growl;
CNCLMSG_PROG ATTACK with the owner's name as mask
for the interception; always narrate a veto.
The appetite GIVE_PROG with isname($o ...) in the body, mpjunk
what is eaten, a well_fed condition as the clock;
CONSUME_PROG to beg, muted while the clock runs.
The saddle RIDE_PROG on the mount, RIDING_PROG on the rider,
REGMASK on dismounts from for the farewell.
The errand mpsleep for time, mpat for there-and-back, mpgoto
for true travel with a written way home.
The mood one word in the mood property via mpset, read by
mood() inside switch $%mood($i)%; decay by
pairing the mood with a condition and forgiving
when affected() turns false.
The memory facts about players in notes ON players; the
mob's own state on the mob; anything that expires
is a condition, not arithmetic.
Companions reward patience more than any other archetype: attach one block, live with it a day, add the next. The best-loved animals on this mud will be the ones that were grown, not assembled. Go build the dog.
Everything you have scripted so far waits for a player. A greeter waits for footsteps, a questmaster waits for a keyword, a boss waits for a blade. This chapter is about the other half of a living world: scripts that act because the WORLD moved. The clock struck six. The day turned over. The season changed. Rain came in off the sea. Somewhere a player logged in, or gained a level, or spoke on a channel, or quietly crossed their tenth hour of play. None of those moments involve anyone standing in the mob's room, and yet all of them can set a script running.
The cast of this chapter, then, is the family of world-event triggers and world-reading questions:
- TIME_PROG, the hourly bell: prayers at dawn, market bells, curfews. - DAY_PROG, the calendar pulse that turns once per mud day. - season(), isseason(), ismonth() and isday(), for festivals. - weather() and isweather(), for scripts that feel the sky. - isnight() and timeofday(), for everything the dark changes. - LOGIN_PROG and LOGOFF_PROG, which notice players arriving and leaving. - LEVEL_PROG, the town crier's trigger. - AGE_PROG, which counts the hours a player has truly lived here. - CHANNEL_PROG, the gossip's ear on the mud-wide chatter.
By the end you will have built a complete scripted town: a priest who keeps the offices of the day, a market that opens and shuts by the bell, a lamplighter, a curfew watch with a long memory, a gossip, a crier, and a harbor scribe who notices every arrival in the realm. You need nothing beyond the basics chapter to follow along; every tool from the deeper chapters is re-explained the first time it appears, and every script is complete and attachable exactly as printed. And the standing promise of this guide holds here too: nothing in this chapter can break the game. A script with a mistake in it simply does less than you hoped. Attach, test, tweak, repeat.
The World's Clock, In Plain Terms
Before you can script a schedule you need to know what the schedule is written on, so here is the mud's whole system of time in one place.
The mud keeps its own clock, and it runs faster than yours. One game hour lasts fifteen real minutes, so a full game day of twenty-four hours passes in six real hours, and four whole mud days fit inside one of your real days. This is worth internalizing before you design anything: a shop that closes for the mud night is closed for a couple of REAL hours at a stretch, and a festival that lasts one mud day is a six-real-hour event. Schedules on this mud are lived at four times the speed you are used to.
Hours are numbered 0 through 23, exactly like a soldier's watch: 0 is midnight, 6 is six in the morning, 12 is noon, 20 is eight in the evening, 23 is eleven at night. There is no such hour as 24, and there is no am or pm; there are only the numbers 0 to 23, and every clock script you write will speak in them.
Above the hours sits a full calendar. A mud month is thirty days, a mud year is twelve months, and each month belongs to a season and carries its own hours of daylight:
Do the arithmetic against the fifteen-minute hour and the rhythm of the world falls out: a mud month passes in about seven and a half real days, a season in about three real weeks, and a whole mud year in about three real months. When you plan a seasonal festival, you are planning something players will see roughly four times a real year, for three real weeks at a time. The realm also names its days, twelve of them in lore, but scripts cannot ask for the weekday; the calendar a script can read is hour, day of the month, month name, and year.
Separate from the numbered hours, the world tracks the LIGHT, in four named bands: night, dawn, day, and twilight. These are the words the timeofday() function returns, and isnight() is the shortcut for the most common check of all. The bands are not fixed to the clock; they breathe with the seasons, because each month's daylight hours differ. In Deepfrost, the blackest month, dawn does not begin until half past six and full night has returned by half past five in the afternoon; in Suncrest the sky starts to lighten at four in the morning and night holds off until eight in the evening. Keep that drift in mind; there is a whole section on it below, because it is the difference between a bell and a sunrise.
Finally, the questions. All of these were introduced in the functions chapter and this chapter leans on them constantly, so here is the five-second refresher. datetime(hour), datetime(day), datetime(month) and datetime(year) read the clock and calendar as values. istime(6), isday(1), ismonth(goldharvest), isyear(2) and isseason(winter) are the yes-or-no forms. weather() and isweather(rain) read the sky. And for real-world occasions there is the isrl family, such as isrlhour(20), which reads the server's actual clock rather than the game's. Questions can be asked from ANY trigger; what this chapter adds is the triggers that fire BECAUSE of time itself.
Your First Bell: TIME_PROG From Zero
TIME_PROG is the trigger that fires when the mud clock reaches an hour you name. Here is the smallest possible clock script, a temple gong that sounds at midnight and at noon:
TIME_PROG 0 12
mpecho The temple gong sounds once, deep and slow, marking the hour.
~
Read the header carefully, because TIME_PROG is one of the special-header triggers and its header does NOT mean what a GREET_PROG header means. There is no percent chance here. The header is a LIST OF HOURS, numbers from 0 to 23 separated by spaces, and the block fires exactly when the clock turns to a listed hour. TIME_PROG 0 12 reads as "at midnight and at noon". One block can carry one hour or a dozen; the body runs once per listed hour as it arrives. The list is REQUIRED: a TIME_PROG with a blank header matches no hour at all and simply never fires.
When the block does fire, the hour that just struck rides in $g as a plain number: 6 at six in the morning, 17 at five in the evening, 0 at midnight. There is no leading zero, ever, and that detail matters more than it looks, as the mistakes section will show. The body here is one mpecho, narrator text to the mob's room, but a body can hold anything a script can hold: speech, emotes, item loading, control flow, sleeps.
Now the machinery underneath, because knowing HOW a trigger fires tells you exactly what it can and cannot do:
- The check rides the mob's own heartbeat. Every couple of seconds, a scripted mob glances at the clock, and when it notices the hour has changed to a listed value, the block fires. This means the bell rings within a heartbeat or two of the turn of the hour, not on the exact second; nobody will ever notice the difference. - Because it rides the heartbeat, TIME_PROG is a MOB trigger. Rooms and items have no heartbeat and never fire it, no matter what you attach to them. Clock scripts live on mobs; if you want a room to appear to keep time, put a quiet mob in it to do the timekeeping. - The mob must be LOADED, which means someone has visited its area since the last reboot. A shopkeeper in a town nobody has walked through does not exist yet, and an NPC that does not exist rings no bells. It simply picks up its schedule the next time the area wakes. There is no catch-up: hours that pass while a mob is unloaded, or dead and waiting to respawn, are skipped, not queued. - A freshly loaded mob quietly notes the current hour without firing. So a mob that spawns at half past five fires its six o'clock block twenty-five real minutes later, but a mob that spawns AT six o'clock does not fire the six block that day; it woke inside the hour and will wait for the next listed one. If a bell absolutely must ring on the very first hour after a spawn, give the mob an ONCE_PROG that performs the same theater on load. - The bell rings wherever the mob happens to be standing. A wandering mob with a TIME_PROG carries its schedule with it, and calls the hour in whatever room the heartbeat finds it.
A few what-ifs to complete the picture. If two different blocks in the same script list the same hour, both fire, in the order written, which is a fine way to separate the bell itself from the mob's reaction to it. If one header lists the same hour twice, the block still fires once; the turn of the hour is a single event. If two different MOBS in two different towns both list hour 6, each fires on its own heartbeat, within a couple of seconds of each other, which is exactly how a realm full of independent bell-ringers should behave. And a mob in combat still hears the clock; whether a mid-battle hour call is drama or comedy is your decision to make.
Testing Clock Scripts Without Waiting For The Sun
An hour is fifteen real minutes, which is short enough that the honest test, waiting for the bell, is genuinely practical for a final check. But while you are iterating you want the block NOW, and here TIME_PROG has a testing quirk you must understand once and then never forget.
The test command, mudprog <mob> test TIME_PROG, fires the trigger with a pretend event whose message is the single word test. For most triggers that is harmless. For TIME_PROG the engine compares the CURRENT HOUR carried in the message against your header list, and the word test, read as a number, counts as 0. The consequence is exact and worth stating twice: under mudprog test, a TIME_PROG block fires only if its header list contains 0, and a block listing only 6 and 17 will not fire from the test command at all, no matter how correct it is.
Inside a block that does fire under test, there is a second quirk: $g holds the literal word test rather than an hour number. Every hour comparison in the body therefore fails, which sounds like a nuisance and is actually a gift, because it hands you a free place to put a self-test. The idiom looks like this, and you will see it in nearly every clock script in this chapter:
- List 0 in the header if midnight is part of your design, which it very often is; midnight is the natural hour for lockups, vigils, and hauntings, and it doubles as the test hook. - End the body with a branch such as if $g == test, or a default case in a switch, containing a drill line. Under test, all the real hour branches fail, the drill branch runs, and you get visible proof the block is alive without waiting for any bell.
When a block's design has no business firing at midnight, you have three honest options. Add a 0 to the header temporarily, test, and take it back out before you walk away. Or wait for the real bell, fifteen minutes at most, doing something else. Or, as an admin, use scripttest fire TIME_PROG on <mob>, which has the same hour-zero behavior but works from anywhere in the room. Whichever you choose, do the real-bell test at least once before you call a schedule finished; only the real clock proves the real timing.
Shaping A Whole Day: The Switch And The If Chain
A real schedule has several hours doing several different things, and there are two clean shapes for that. The first is one block per hour, which you saw in the priest below and which reads beautifully when each hour is a different scene. The second is a single block listing every hour, with the body branching on $g, and it comes in two flavors.
Flavor one is the switch, best when the hours are true alternatives. Here is a gongmaster whose whole day lives in one block:
TIME_PROG 0 6 12 20
switch $g
case 6
emote swings the great gong once to open the dawn hour.
break
case 12
emote strikes the gong twice for the noon hour.
break
case 20
emote muffles the gong with a cloth for the evening hour.
break
case 0
emote lets the gong rest silent at midnight and bows to it.
break
default
say This is only a test, and the gong stands ready.
endswitch
~
The header lists all four hours, so the block fires at each of them, and the switch reads $g once and runs exactly one case: the 6 case at dawn, the 12 case at noon, and so on. The default case can never be reached by the real clock, because the block only fires at listed hours, and that is precisely why it is there: under mudprog test, $g is the word test, no case matches, and the default speaks. The default case IS the drill branch, built into the shape of the switch for free. Leave yourself one in every switch-shaped schedule.
Flavor two is the if chain, best when hours share work or when you want several small independent reactions. Here is a market keeper, Maren, whose bell opens the stalls at eight, closes them at twenty, and locks up at midnight:
TIME_PROG 0 8 20
if $g == 8
yell Market bell! The stalls are open for the day!
emote props the shutters wide and sets out the first tray.
mpoloadroom /obj/meal
endif
if $g == 20
yell Market bell! Last trades now, we close at the twentieth hour!
emote begins packing the unsold goods into crates.
endif
if $g == 0
emote locks the money box and turns the lamp down low.
endif
if $g == test
say The market bell rings just fine, and that was only a drill.
endif
~
Each if asks its own question of $g and the hours never overlap, so at any real bell exactly one branch runs. The opening branch does real work: yell carries the cry beyond the room the way a market bell should, and mpoloadroom sets out fresh goods, so the market literally restocks each morning. The final if is the drill branch again, in its if-chain form: under test, $g is the word test, the three hour branches fail, and the fourth confirms the bell works. On the real clock the word test never equals a number, so the drill line never leaks into play.
Which shape to choose? If every hour is a distinct scene, separate blocks per hour read best and can be tested one at a time. If the hours form one character's tight routine, the single block keeps the whole day on one screen. Both are correct; pick for readability.
While the header is fresh in your mind, the four ways it goes wrong, all silent, all common. Writing commas, as in a header of 0, 8, 20, breaks the match, because the engine splits on spaces and the token 0, with its comma is not the token 0; use spaces only. Writing a leading zero, as in TIME_PROG 08, never fires, because the clock announces the hour as 8 and the text 08 is not the text 8. Writing TIME_PROG 24 never fires because the day ends at 23 and rolls to 0. And writing TIME_PROG 100, on the instinct that 100 means always on other triggers, waits for the hour one hundred, which is to say forever; on this one trigger the header is hours, never a percent.
The Priest Of The Turning Hours: A Complete Clock NPC
Now a full character built from separate hour blocks: Brother Aldous, who keeps the offices of the chapel day. Dawn prayers at six, vespers at seventeen, and a small-hours vigil at midnight, plus a greeting that knows whether the lamps are lit:
GREET_PROG 100
say Welcome to the chapel of the turning hours, $N.
if isnight()
say The lamps are lit for the vigil. Sit with us if sleep will not come.
else
say The light is a blessing. The morning office has been sung already.
endif
~
TIME_PROG 6
emote kneels at the altar as the sixth hour arrives.
say The dawn office begins. Let the day be honest and the roads kind.
mpasound A slow chant drifts out from the chapel.
~
TIME_PROG 17
emote lights the evening candles one by one.
say Vespers now. Whatever the day took, let it rest.
~
TIME_PROG 0
emote begins the small hours vigil, alone before the altar.
mpecho The candle flames seem loud in the emptied chapel.
~
Walk it through. The greeting is ordinary GREET_PROG work, but notice how one isnight() check makes a static welcome feel like a place with a schedule: visitors at different times of day meet different chapels. The three clock blocks are three separate scenes, one block per hour, each in its own voice. The dawn office pairs an emote, a say, and mpasound, which speaks into every ADJACENT room but not the chapel itself; together the room hears the prayer and the street outside hears the chant drifting out, which is how sound actually works. Vespers is quieter, just candles and a line. The midnight vigil talks to nobody, an emote and an mpecho for whoever happens to be sitting in the pews at midnight, and there is always someone eventually.
Testing him: mudprog aldous test TIME_PROG fires only the midnight block, because only its header lists 0; the 6 and 17 blocks silently sit out the test, which is the hour-zero rule from the testing section in action, and seeing it happen once on a real script will fix it in your memory permanently. Test the greeting separately with mudprog aldous test GREET_PROG, then let one real bell arrive to prove the schedule end to end.
The Fixed Clock And The Drifting Sun
Brother Aldous has a subtle flaw, and it is one of the finest teaching moments in all of schedule scripting: his dawn office is at six by the CLOCK, but dawn itself MOVES. Remember the daylight table: the light bands stretch and shrink with the months. In Suncrest, the brightest month, the sky begins to lighten around four and it is full day by five, so his six o'clock office happens in broad morning light. In Deepfrost, dawn does not even begin until half past six, so the same office is sung in the dark. Neither is wrong, and for a priest it is arguably perfect, the office is at the sixth hour because the rule says the sixth hour, and the sun can keep its own counsel. But you must KNOW which one you are scripting.
The rule of thumb: TIME_PROG is a bell, and bells are civic. Markets, curfews, watches, rents, offices, anything humans schedule, belongs on fixed hours. The SUN is a condition, not an hour, and sun-driven behavior, lighting lamps, closing shutters against the dark, hauntings that rise with the night, belongs to isnight() and timeofday(), whose four answers are the words night, dawn, day and twilight. The two tools combine beautifully: fire on a fixed hour, then let the body consult the light. A watchman whose twenty-two o'clock round says one thing under a lingering summer twilight and another under true winter dark is one if away, and feels alive in a way no fixed text can.
One honest limitation to close the loop: there is no trigger that fires AT the moment night falls. The bands are questions, not events. When you want something to happen once as darkness arrives, the idiom is a TIME_PROG on an hour that is reliably dark all year, hour 21 or later, or a low RAND_PROG whose body checks isnight() and a variable latch so it acts only once per night; the variables chapter's latch pattern fits exactly.
A Schedule With Teeth: The Curfew Watch
A curfew is a schedule plus consequences, and it shows how a clock script can reach past the moment of the bell by leaving notes on the players themselves. Meet Sergeant Hobb:
TIME_PROG 0 22
if $g == 22
yell Curfew bell! Honest folk indoors before the next stroke!
mpasound A curfew bell rolls out across the darkened streets.
endif
if $g == 0
emote walks a slow circuit, lantern high, noting who is still abroad.
endif
if $g == test
say The curfew bell hangs ready, but tonight we only drill.
endif
~
GREET_PROG 100
if isnight()
mpsetvar $n out_after_dark 1
say Out after the bell again, $N? The watch keeps a long memory.
else
if var($n out_after_dark) == 1
mpsetvar $n out_after_dark 0
say Daylight suits you better, $N. The watch noted you abroad last night.
else
say Fair day to you, $N. Keep to the lit streets once the bell sounds.
endif
endif
~
The clock block is the familiar if chain: the warning at twenty-two, delivered with yell plus mpasound so the whole neighborhood hears the bell, the midnight circuit, and the drill branch. The greeting is where the teeth are. Anyone who walks up to Hobb at night gets a note written on them, mpsetvar $n out_after_dark 1, a named value stored ON THAT PLAYER, exactly the per-player memory pattern from the variables chapter. The next time they meet him in daylight, the var() question finds the note, he mentions it, and he clears it by setting it back to 0. A one-line note, and suddenly the curfew has a memory that survives the night and makes it personal.
Now the design question every builder asks next: can the watch DO something to curfew breakers? Mechanically, some things are possible and almost all of them are mistakes. Do not teleport, strike down, or purge a player from a schedule script; hard consequences from a clock, with no player action to answer for, feel arbitrary and are the fastest way to make players hate a town. The strong designs are all softer: memory, as above; reputation, an mpfaction nudge for repeat offenders; theater, doors closing and lamps going dark; and physical barriers, a night-locked gate built with the veto pass from the mudprog-bus chapter, where a door refuses entry politely and in character. The cookbook's toll-gate recipe is the closest cousin. A curfew should be a mood, not a trap.
DAY_PROG: The Calendar Pulse
One step up from the hourly bell is the daily one. DAY_PROG fires once per mud day, at the midnight rollover, and unlike TIME_PROG it is a WORLD-WIDE trigger: it fires on every loaded scripted object that defines it, wherever that object stands, with no one in the room to cause it. Like TIME_PROG, its header is a required list, but of DAY NUMBERS, days of the mud month, and the day number rides in $g.
Here is Old Fennick, an almanac keeper who marks three dates:
DAY_PROG 0 2 16 31
if $g == 2
say The almanac turns. The new month is properly underway.
endif
if $g == 16
say Midmonth by the almanac. Rents fall due and tempers fray.
endif
if $g == 31
say The last night of $%datetime(month)% is on us. Tomorrow the calendar turns.
endif
if $g == test
say My almanac has no day at all on this page, so we are testing.
endif
~
Before the walkthrough, the fine print, because DAY_PROG has some and you should learn it from a book rather than from a silent script.
The Calendar's Fine Print
First: the day numbers DAY_PROG actually announces run from 2 to 31, not 1 to 30. The pulse fires at each midnight with the number of the day that is beginning, so the night that ends day 1 announces day 2, and so on up the month. At the FINAL midnight of a month, the engine announces one number past the month's length, 31, and only then turns the calendar page; by the time anyone could ask, the date reads day 1 of the new month, but the pulse that started that day said 31. The consequences, plainly:
- A header listing 1 waits forever. The number 1 is never announced. - To mark the TURN of the month, list 31. That pulse is the month's midnight boundary, and note in passing that at that moment the month name still reads the OLD month, which is why Fennick's 31 line says the last night of the month it names; that is accurate, and a line that greeted the NEW month by name there would name the wrong one. - A header listing 0 never fires from the calendar at all, because no day is numbered 0, and that makes 0 the perfect harmless test hook. It is exactly why Fennick's header carries one: under mudprog fennick test DAY_PROG, the word test counts as day 0, the block fires, the three real branches fail, and the drill line speaks. On TIME_PROG a 0 means midnight and fires nightly; on DAY_PROG a 0 means nothing and never fires. Same digit, different calendars, opposite meanings; keep them straight.
Second: when you want an event on a specific CALENDAR DATE, morning is sturdier than midnight. The robust pattern is not DAY_PROG at all but a TIME_PROG on a daytime hour whose body checks the date with isday() and ismonth(); by any waking hour the calendar reads the new date plainly and the questions answer exactly what you mean. The festival section next door is built on this.
Third, the shared-timing note: the DAY_PROG pulse and any TIME_PROG 0 blocks land at the same midnight, within a heartbeat of each other, in no guaranteed order. Never write a midnight TIME block that depends on a DAY block having already run, or the reverse; on the night the order flips, the seam will show.
Now Fennick reads simply: day 2 greets the working month, day 16 calls the midmonth, day 31 closes the book, the 0 is his test hook, and $g carries the day number into each comparison the same way TIME_PROG carried the hour. World-wide means he fires in his study with nobody watching, which is fine; the room hears him, and an empty room keeps its counsel.
Festivals: Catching One Date In The Year
A festival is a date, and a date is a day plus a month. Here is the whole pattern in a baker's dozen of lines, a herald who proclaims the Feast of First Harvest on the morning of the first of Goldharvest:
TIME_PROG 0 7
emote checks the almanac by the light of the new hour.
if isday(1) and ismonth(goldharvest)
yell The Feast of First Harvest begins today! Bread for everyone!
mpoloadroom /obj/meal
endif
~
The shape is exactly the sturdy pattern promised above: a fixed morning bell, hour 7, and a body that asks the calendar two questions. isday(1) is true all through the first day of any month; ismonth(goldharvest) narrows it to one month of the twelve; joined with and, they are true on one morning in three hundred and sixty. Every other day of the year, the herald checks his almanac, finds nothing, and says nothing, which costs the world one quiet emote per day and reads as character rather than waste. The 0 in the header is the midnight firing doubling as the test hook: under test the emote proves the block alive, and unless you happen to be testing on festival morning itself the calendar questions correctly come up false.
Month names are single words, always: goldharvest, not gold harvest. The question is forgiving about capitals but not about spaces, and ismonth(gold harvest) matches no month that exists. The same one-word rule will return in the weather section with more force.
Want a LONGER festival? Ask a looser question. A three-day feast is if datetime(day) <= 3 and ismonth(goldharvest), using the value form of the calendar so the comparison can say up to rather than exactly. A festival EVE is isday(30) in the month before. And an annual once-only reward, a gift for attending the feast, is this pattern plus the per-player latch from the curfew section: note the year with mpsetvar $n feast_year $%datetime(year)% when you hand over the gift, and refuse politely while the note matches the current year.
Seasons: Dressing The World By The Quarter
Season work needs no trigger at all, and that is its charm: the season is simply true for three mud months at a stretch, so any script that asks isseason() in passing is seasonal, automatically, forever. The four answers are spring, summer, autumn and winter, and season() hands you the current word for splicing into speech. Here is a festival hall that redecorates itself four times a year without a single schedule:
GREET_PROG 100
say Welcome, $N. Every season keeps a feast in this hall.
if isseason(spring)
emote gestures to garlands of new blossom strung along the rafters.
endif
if isseason(summer)
emote gestures to sheaves of sunwheat bound above the hearth.
endif
if isseason(autumn)
emote gestures to strings of dried apples and bright leaves overhead.
endif
if isseason(winter)
emote gestures to evergreen boughs and pale candles in the windows.
endif
say Just now the hall is dressed for $%season()%, in the month of $%datetime(month)%.
~
Exactly one of the four ifs is true on any given day, so every visitor sees one set of decorations, and when Stormtide gives way to Suncrest the hall re-dresses itself with no builder lifting a finger. This is the deepest lesson of the section: a schedule is not always a bell. Sometimes it is just a question asked at the right moment, and the world's own calendar does the scheduling for you. Sprinkle isseason checks into greetings, RAND_PROG idle lines, and consult answers across a town, and the whole place breathes with the year at a cost of nothing.
Plan season content against real time: three real weeks per season, about three real months for the full cycle. Content gated to winter is absent for nine real weeks at a stretch, which is long enough that players genuinely miss it and greet its return as an event. That rhythm, presence and absence, is what makes seasonal building worth the effort.
Weather: Scripts That Feel The Sky
Weather is the third world-condition, and like the seasons it is a QUESTION, not an event: there is no WEATHER_PROG, no trigger that fires as rain begins. What the engine gives you is isweather(<word>), true when the word appears in the current weather condition where the scripted object stands, and weather(), the condition itself as text.
How the sky actually works: each realm of the world carries one weather condition at a time, shared by every room in the realm, and it drifts to a neighboring condition every thirty to ninety real minutes, sliding along sensible paths, clear to cloudy to overcast to drizzle, rather than leaping from sunshine to blizzard. The condition is always exactly one of these twelve machine words:
Two of them, partly_cloudy and heavy_rain, are single words joined by an underscore, and that underscore is load-bearing. The isweather question matches by SUBSTRING, which is a quiet superpower once you see it: isweather(rain) is true in rain AND in heavy_rain, one check covering both; isweather(cloud) covers partly_cloudy and cloudy together; isweather(storm) catches thunderstorm; isweather(heavy) catches heavy_rain alone. But isweather(heavy rain), with a space, matches nothing on the list and is silently false forever, the same one-word rule as month names, with more ways to get it wrong. When in doubt, match a fragment: rain, snow, fog, storm, cloud, heavy, clear.
Since there is no weather trigger, weather scripting is a guard on triggers that fire anyway. On a greeting:
GREET_PROG 100
say The sky does what it pleases, $N. Step in out of it.
say My knees call the weather $%weather()%, and my knees are never wrong.
if isweather(rain)
emote wrings out the hem of a cloak hung by the door.
endif
if isweather(snow)
emote knocks a dusting of snow from the boot rack.
endif
if isweather(fog)
emote peers past you into the murk and shudders.
endif
~
Every visitor gets the welcome; the emotes come and go with the sky, so the inn feels different on a foul day, which is precisely the goal. One small blemish to notice and forgive: weather() returns the machine word, so on a gray morning the innkeep's knees say partly_cloudy, underscore and all. For polish, map the word to prose with a switch, one case per condition; the exercise section leaves that as a variation.
And on the idle pulse, for continuous atmosphere:
RAND_PROG 100
emote studies the sky like a farmer reading tomorrow in the clouds.
if isweather(rain) or isweather(drizzle) or isweather(storm)
say Rain again. The barrels will be glad, even if my boots are not.
endif
~
The 100 is for the demonstration, so a single test shows it working; on a live mob drop it to 6 or so, or the farmer studies the sky every other heartbeat, and see the basics chapter on RAND_PROG rates. The shape is the thing: an unconditional texture line, then weather-gated extras. Layer guards freely, isweather with isnight with isseason; a snow emote gated to winter nights is three questions on one if line and reads like hand-placed seasonal content.
Judgment call to close: the weather is the REALM'S sky, and the question answers the same deep in a cellar as it does on the street. The engine will happily let a cave hermit complain about drizzle he cannot possibly see; whether that is a bug or the hermit's rheumatism is up to you, but keep weather scripts on mobs that plausibly know the sky.
LOGIN_PROG And LOGOFF_PROG: The World Notices You
Now the world-wide people-triggers. LOGIN_PROG fires on every loaded scripted object that defines it, wherever it stands, each time any player enters the game; LOGOFF_PROG is its mirror, firing as a player quits. The header stays BLANK on both. In the body, $n is the player in question, and here is the part that changes what these triggers are for: that player is almost never in your mob's room. They are logging in at their own bind point, possibly a realm away.
So the body works in two directions. Local lines, say and emote and mpecho, play in the SCRIPTED MOB'S room, to whoever is standing there: the world visibly reacting to news of an arrival. And mpechoat $n crosses any distance, delivering one private line to the player wherever they stand, because it speaks to a person, not to a room. Both directions in one scribe:
LOGIN_PROG
emote notes a fresh arrival in the harbor ledger.
mpechoat $n Far off in the harbor, a scribe adds your name to the day's ledger, $N.
~
LOGOFF_PROG
emote rules a neat line under a name in the harbor ledger.
~
The emote is local color for the harbor office; the mpechoat is a whisper of a wider world for the player who just arrived. The logoff block is local only, which is deliberate: the player is leaving, and a farewell whispered into a closing connection is wasted words.
Restraint is the whole art of these triggers, and it needs saying plainly: EVERY scripted listener in the whole world fires for EVERY login. If six builders each give their town a login whisperer, every player logs in to six whispers, every session, and what was charming once is spam by the third day. House rules that keep it lovable: at most ONE mpechoat greeter per town, local-only flavor everywhere else, and never a login mpecho on a mob that stands where players gather in numbers, or the room chants arrivals all day. Test with mudprog scribe test LOGIN_PROG, where you stand in as the arriving player from inside the room, then have a friend actually log in for the true cross-realm proof.
The lovely upgrade is memory, the same per-player notes the curfew used. On logoff, stamp the calendar onto the player: mpsetvar $n last_seen $%datetime(day)%. On login, read it back with var() and $<...> and the scribe can greet a returner with how long the ledger says they were away. The registrar exercise below builds the first-time half of exactly this.
Choosing Who To Greet: Masks On World Triggers
A blank header means everyone, but world triggers also accept the zapper mask, the dash-clause filter from the triggers chapter, and it turns a broadcast into an aimed one. The classic use is the newcomer's welcome:
LOGIN_PROG
emote dips a pen, ready to log the new arrival.
~
LOGIN_PROG -level 1-10
mpechoat $n A runner finds you bearing welcome from the harbor scribe, who greets every newcomer personally.
~
Two blocks, two audiences. The first, blank-headed, fires for every login: local flavor only. The second fires only when the arriving player passes the mask, level one through ten, and IT carries the personal touch, so veterans are spared a welcome they outgrew years ago. All the mask clauses work here: -race and -class for themed welcomes, -name for one particular regular, a range or floor on -level. One testing note so the silence never fools you: under mudprog test YOU are the arriving player, so a mask your own character does not pass keeps that block silent, which is the mask doing its job; the blank block's line is your proof the trigger fired at all.
LEVEL_PROG: The Town Crier
LEVEL_PROG is the celebration trigger: world-wide, blank header, fired each time any player gains a level, with the NEW level riding in $g as a number. Only genuine players fire it, only on the way UP, and a jump of several levels at once announces once, with the final level. Meet Boldo the crier, who has learned the first law of crying, which is knowing when not to:
LEVEL_PROG
switch $g
case 10
yell Hear ye! $N has reached the tenth level! The apprentice days are done!
break
case 25
yell Hear ye! $N stands at level twenty-five! Half a legend already!
break
case 50
yell Hear ye! Level fifty! $N walks among the mighty of the realm!
break
default
emote polishes the crier's bell, saving his voice for milestone levels.
endswitch
~
The switch on $g picks out three milestone levels for the full civic roar, and the default, which catches the other ninety-odd levels, spends only a quiet emote. That proportion is the design: levels are common, and a crier who bellows for every one becomes wallpaper by lunchtime. The default doubles, as always, as the test branch, since under mudprog test $g is the word test and matches no case. Variations that stay tasteful: an mpechoat $n line inside a milestone case, a private word of congratulation crossing the realm to the player themselves; a header mask of -level 50 on a second block so a guard mob salutes only the mighty; or a low-numbered case, level 2 perhaps, with a gentle line aimed at brand-new players finding their feet.
AGE_PROG: Milestones Of A Life Played
AGE_PROG measures something levels do not: time actually lived in the world. It is world-wide, blank-headed, and fires each time any player crosses another full hour of total PLAYED time, with the new count of hours in $g. Ten fires means ten real hours in the realm, across any number of sessions. That makes it the trigger for anniversary moments, and because it fires every hour for every player, the body almost always wants two things: a narrow filter on $g, and a once-only latch so the moment cannot repeat. Both together, in the Warden of the Hourglass:
AGE_PROG
if number($g) == 10 and var($n decade_gift) != 1
mpsetvar $n decade_gift 1
mpechoat $n Ten full hours you have walked this world, and somewhere a warden turns a glass in your honor.
mploadquestobj /obj/meal
endif
if $g == test
say The sand behaves itself, so this turn of the glass is a test.
endif
~
The first if is the whole craft in one line. number($g) == 10 aims the block at one hour of one lifetime; the number() wrapper makes the comparison honestly numeric, and has the tidy side effect that the word test counts as zero and never sneaks past. The var() check is the latch: the block marks the player before acting, so even if the same hour were somehow announced twice, the gift happens once per lifetime. Then the payoff crosses the world: mpechoat for the private line, and mploadquestobj, which conjures the item straight into the PLAYER'S pack wherever they are, no floor, no room, no chance of the gift landing at the warden's feet a realm away from its owner. The second if is the visible drill branch for testing. Scale the idea freely: cases at 10, 50, 100 and 500 hours with escalating gifts make a lovely veterans' ladder, and the latch pattern repeats per milestone with one variable each.
CHANNEL_PROG: The Gossip Who Hears Everything
The last world trigger listens to the mud's chat channels. CHANNEL_PROG fires on every loaded scripted listener whenever anyone speaks on a channel, anywhere. In the body, $n is the speaker and the message arrives as one line of text shaped channel name first, then the words: a player saying hello all on the chat channel arrives as chat hello all. As usual $g is that line in lower case and $G keeps the original capitals.
The header is a keyword filter matched against that WHOLE line, which has two consequences worth learning before your first listener. A blank header hears every channel. A header word narrows it, and because the channel name is part of the line, putting a channel's name in the header is how you listen to one channel, but the match does not know names from words: a header of newbie hears the newbie channel AND any sentence on any channel that happens to contain the word newbie. For most gossips that fuzziness is fine, even charming. One local quirk: what players call the gossip channel is delivered as chat, so listen for chat, not gossip.
Here is Widow Casska, who keeps a notebook:
CHANNEL_PROG
mpsetvar $i rumor $G
emote scribbles something scandalous into a dog-eared notebook.
~
SPEECH_PROG rumor news word
if var($i rumor) != ""
say Straight from my notebook, $N, exactly as it reached me.
say $<$i rumor>
else
say Not a whisper worth repeating today, $N. Give it an hour.
endif
~
The channel block is two lines: store the latest line on herself with mpsetvar $i rumor $G, and one emote of local theater. The speech block is the payoff, pure room-scale conversation: ask her for the news and she reads her note back with the angle form $<$i rumor>. She reads it VERBATIM, channel label and all, so her retelling begins with the word chat, like a clerk citing sources; the engine has no tool for trimming a word off a stored string, so the honest move is to let the verbatim reading be part of her character. Until the first channel message of her lifetime arrives the note is empty, and the else branch covers that morning-after gap.
The Echo Trap
Casska repeats what she hears INTO HER ROOM, with say, and that choice is a safety rule, not a stylistic one. The tempting upgrade, a mob that answers the channel ON the channel with mpchannel, is how you build a feedback loop: the mob's own channel message is itself channel traffic, which fires CHANNEL_PROG again, which sends again, forever, and two such mobs will happily do this to each other across the whole mud. The engine's runaway guards stop a single script run, but each echo is a fresh event, so the loop lives BETWEEN runs where no budget can catch it. The rule, absolute: never send to a channel from inside CHANNEL_PROG. If a talking-back gossip is truly wanted, route the reply through a different trigger, the way Casska answers speech in her room, or gate the send behind a latch variable that a slow TIME_PROG resets at most once an hour. Local repetition needs no such care: say cannot re-fire a channel trigger, which is why the notebook pattern is the safe default.
The Town That Keeps Time: A Complete Day Cycle
Now assemble everything. A town square, and around it a cast of six, each carrying scripts from this chapter. Their combined timetable:
hour 0 the priest's vigil, the market lockup, the lamps trimmed,
the watch's circuit, and the DAY_PROG almanac pulse
hour 4 the baker lights the ovens
hour 6 the dawn office at the chapel
hour 7 hot bread, and on one morning a year, the Feast
hour 8 the market bell opens the stalls
hour 16 day sixteen only: rents fall due
hour 17 vespers
hour 18 the lamplighter makes his round
hour 20 the market bell closes the stalls
hour 22 the curfew bell
always the gossip listens, the crier watches the levels, the
harbor scribe logs arrivals and departures
You have already built most of the cast: Brother Aldous, Maren and her market bell, Sergeant Hobb, Old Fennick, Boldo, Casska, the scribe. Two more round out the square. First the lamplighter, Serl, whose work is the town's visible pulse of light:
TIME_PROG 0 18
if $g == 18
emote sets a ladder against the first post and coaxes the wick alight.
mpecho One by one, the street lamps take the flame and hold it.
endif
if $g == 0
emote trims each lamp down to a thrifty midnight glow.
endif
if $g == test
emote checks the lamp fittings, calling it a drill.
endif
~
TIME_PROG 6
emote climbs the ladder again and snuffs the lamps against the growing light.
mpecho The street lamps go out one by one, giving the morning back to the sun.
~
SPEECH_PROG lamp lamps lantern
say Eighteen strokes lights them and six puts them out, $N. Between those bells the dark answers to me.
~
Serl is deliberately built on FIXED hours, eighteen and six, even though his subject is the light itself, and now you can articulate exactly what that trade is: in Suncrest he lights lamps under a sky still bright at eighteen, and in Deepfrost the streets are dark for an hour before he arrives. A town that finds that endearing keeps him as is; a town that wants precision gives him isnight() checks and a wider spread of hours. Either answer is right because it is CHOSEN. His speech block, meanwhile, lets him explain his own schedule, which is a touch worth copying: mobs who can talk about their routine make the clockwork legible to players.
The baker, Odo, appears in the exercises below and slots into hours four and seven. And that is a town: nine or ten PROG blocks spread across half a dozen mobs, every one of them a pattern from this chapter.
Choreography Notes For A Scripted Town
The deepest design fact of the finale is what is MISSING: the cast never coordinate. No messages pass between them, no shared state, no mob waits on another. They cannot, comfortably: script variables live on objects in reach, and mobs in different rooms are out of each other's reach, while the mud-wide globals of mpgset are write-only to scripts, as the variables chapter explains. But they do not need to, and this is the insight to carry into your own towns: THE CLOCK IS THE CONDUCTOR. Every mob follows the same clock privately, so the town appears orchestrated while every player is really watching a dozen soloists share a metronome. Independence is also robustness: one mob dead, purged, or unloaded subtracts its own verses and nothing else; the rest of the town plays on.
Practical staging rules, learned the hard way so you need not:
- Stagger the hours. Midnight is already crowded, four of our six act at hour 0, plus the calendar pulse, and every additional midnight event flattens the others into a wall of text for anyone standing in the square. Dawn theater is better spread across 4, 6 and 7, as the cast does, than piled onto 6. - Mind the shared rooms. Yell and mpasound carry beyond their room by design; if three criers share a street, their big moments should not share an hour. - Remember the loading rule. The whole town keeps time only while its mobs are loaded; the first visitor after a reboot wakes the square, and earlier bells are simply gone. Towns players frequent are effectively always awake, and a truly empty town rings bells for nobody anyway; spend no worry here, just know the rule. - Test mob by mob, then watch one full mud day, six real hours with occasional glances, before calling the town done. Rhythm mistakes, two bells colliding, a spammy hour, only show at full speed.
Exercises
Four tasks, each a head builder's one-paragraph request, each with a hint and a complete worked solution. Build your own before reading the answer; every tool needed is somewhere above.
Exercise one, the baker. Odo lights his ovens at four in the morning and cries fresh bread at seven, with a real meal appearing for the early customers. Make him testable without waiting for either hour. Hint: one if-chain block, one test hook.
TIME_PROG 0 4 7
if $g == 4
emote lights the ovens, and the smell of proving dough slips into the street.
endif
if $g == 7
yell Hot bread! First batch out of the oven!
mpoloadroom /obj/meal
endif
if $g == test
say All is in order at the bakery, though this is only a drill.
endif
~
The header carries both working hours plus the 0 hook, the four o'clock branch is pure atmosphere, seven is the cry and the loaf, and the drill branch answers the test. The 0 does mean Odo's block fires at midnight and finds no matching branch, doing nothing visibly, which is a harmless price; a fastidious builder adds a midnight branch, emote banks the oven coals for the night, and turns the side effect into character.
Exercise two, the ferryman. Grigg takes travelers across the river, but not under a dangerous sky: any storm, blizzard, or heavy rain grounds him, and he should say so in character while offering fair passage otherwise. Hint: greeting plus weather guards, and remember the one-word rule.
GREET_PROG 100
say Wanting the far bank, are you, $N? You have that look.
if isweather(storm) or isweather(blizzard) or isweather(heavy)
say Not under this sky. The river and I have an agreement, and today it says stay ashore.
else
say The crossing is fair enough today. Say the word ferry when you are ready.
endif
~
The three guards read almost like weather words and are really substring fragments: storm catches thunderstorm, blizzard names itself, and heavy catches heavy_rain without touching plain rain, the underscore trap dodged by matching the half of the word that cannot miss. Everything else falls to the else. Note what the script does NOT do: it does not stop anyone from doing anything, because refusal theater is a greeting's job and true refusal is a veto's, over in the mudprog-bus chapter; this Grigg trusts players to respect the fiction, which for most towns is exactly enough.
Exercise three, the registrar. The town hall keeps a great register: every login is visibly logged, first-time visitors get a private word of welcome from across the realm, and logoffs close the page. Hint: LOGIN plus LOGOFF, and the once-only latch.
LOGIN_PROG
emote writes a name in fresh ink across the great register.
if var($n town_registered) != 1
mpsetvar $n town_registered 1
mpechoat $n Far away in the town hall, your name enters the rolls for the first time.
endif
~
LOGOFF_PROG
emote blots the register dry and sets a ribbon at the day's page.
~
The emote fires for every arrival, the town hall's local heartbeat. The latch inside makes the mpechoat a genuine once: the first login of a player's LIFE trips it, the variable is stamped, and every later login finds the note already made. That is the AGE_PROG gift pattern wearing different clothes, and it is the single most reusable idiom in world-trigger scripting. The logoff block stays local, closing the day's page for whoever is in the hall to see.
Exercise four, the rent collector. Mistress Vane cries the rents due on the sixteenth of every month, and can prove her ledger works on demand. Hint: the calendar pulse, and the digit that never comes.
DAY_PROG 0 16
if $g == 16
yell Midmonth! Rents fall due, and the ledger knows no mercy!
endif
if $g == test
say The inkwell is full and the ledger is balanced, so we drill.
endif
~
Sixteen is a real day number, safely inside the 2-to-31 range the pulse actually announces, so the cry lands once per month; the 0 is the pure test hook that no calendar can ever reach. If you built this with 1 instead of 16 and wondered at the silence, reread the fine print, then keep the lesson: on DAY_PROG, never schedule the first of the month by the number 1.
Common Mistakes With Fixes
The chapter's traps, gathered in one place for the day a schedule goes quiet on you.
1. A TIME_PROG or DAY_PROG with a blank header. Percent-style triggers
treat blank as always; these two treat it as an empty list, which
matches nothing, ever. Fix: list the hours or days, always.
2. TIME_PROG 100, meant as always. On this trigger the header is
hours, so this waits for the hour one hundred, which never comes.
Fix: list every hour you actually mean, or use RAND_PROG with an
isnight or istime guard for continuous behavior.
3. Leading zeros: a header of 08, or 05 on DAY_PROG. The clock
announces 8, and the text 08 is not the text 8, so the block never
fires and nothing complains. Fix: bare numbers only. The same
spelling strictness applies to commas in the list.
4. Testing a block whose header has no 0 with
mudprog test and concluding it is broken. The test
event counts as hour zero and day zero, so only lists containing 0
fire under test. Fix: the drill-branch idiom, a temporary 0, or
fifteen minutes of patience for the real bell.
5. Expecting $g to hold an hour under test. It holds the word test, so
every case and comparison built for numbers fails, silently. Fix:
that is what the drill branch and the switch default are FOR.
6. A number header on a world trigger: AGE_PROG 10 meaning at ten
hours, or LEVEL_PROG 20 meaning at level twenty. On these triggers
a bare number is a PERCENT chance, so AGE_PROG 10 fires for one
hour-crossing in ten, at random, which looks like haunted dice.
Fix: blank header, filter in the body on number($g), or use a
zapper mask like -level 20 where the mask expresses it.
7. DAY_PROG 1, waiting for the first of the month. The midnight pulse
announces 2 through 31 and never 1; the month's first day arrives
wearing the number 31. Fix: list 31 for the month-turn, or use the
morning-after pattern, TIME_PROG plus isday(1) and ismonth().
8. Clock scripts on rooms or items. TIME_PROG rides the mob heartbeat,
which rooms and items do not have; the script parses, registers,
and never fires. Fix: a quiet mob does the timekeeping.
9. Two-word arguments to one-word questions: isweather(heavy rain),
ismonth(gold harvest). Both are silently false forever. Fix: the
underscore form heavy_rain or the fragment heavy; month names as
single words.
10. Answering a channel from CHANNEL_PROG. The reply is new channel
traffic, which re-fires the trigger, and the loop runs between
script runs where no runaway guard lives. Fix: reply locally with
say, or gate any mpchannel behind a latch a slow TIME_PROG resets.
11. Midnight pileups. Hour 0 already hosts the calendar pulse and
everyone's lockups; each additional midnight event buries the
others. Fix: stagger; the mud gives you twenty-three other hours.
12. Depending on order at shared moments: a TIME_PROG 0 block that
assumes the DAY_PROG pulse already ran, or the reverse. They land
in the same breath in no promised order. Fix: make each block
self-sufficient.
13. Waiting for a bell from a mob that is not loaded, freshly dead, or
spawned mid-hour. Unloaded areas skip bells without queueing them,
and a mob born inside an hour does not fire that hour's block.
Fix: visit the area, let a full hour boundary pass, then judge.
Where To Go Next
You now hold the full vocabulary of world-driven scripting: the fixed bell and the calendar pulse, the drifting sun, the seasonal question, the weather guard, and the five world-wide people-triggers, plus the craft rules, test hooks, latches, restraint, and staggering, that keep a town charming instead of noisy. The mudprog-triggers chapter is the reference card for every trigger touched here; mudprog-functions catalogs the whole question toolbox including the real-world isrl family this chapter only waved at; mudprog-variables deepens the latch and note patterns; and the mudprog-bus chapter supplies the vetoes that give curfews and closing time real teeth. The cookbook's night watchman and the shopkeeper chapter's dawn shutter are cousins of everything here, worth reading with fresh eyes now. Then claim a quiet square in your own area and give it a day worth living through twice: once as a player passing at noon, and once at midnight, when the lamps are low, the vigil candles are lit, and the town keeps time whether anyone is watching or not.
This chapter is a deep dive into one archetype: the fighting NPC. Guards who taunt, brutes who enrage, cowards who run, necromancers who raise help, bodyguards who step in front of a blade, and full three-phase bosses with summons, phase speeches, loot, and last words. If the basics chapter taught you to make a mob talk, this one teaches you to make a mob FIGHT with personality, and it assumes nothing beyond that basics chapter: every idea is introduced from zero, every example is complete, and every example can be pasted onto a practice mob exactly as printed.
One promise before we start. Nothing in this chapter changes how hard a mob hits with its ordinary attacks, how much health it has, or how accurate it is. Those numbers belong to the mob itself and to the balance system, and a coder sets them. What a combat script adds is everything AROUND the numbers: the voice, the timing, the surprises, the theater. A boss with good numbers and no script is a math problem. A boss with good numbers and a good script is a story your players will retell in the tavern, wrongly, for months. That second thing is what we are building.
The Fight As The Script Sees It
Before writing a single block, it helps to know what a fight looks like from inside the engine, because all combat scripting hangs off five moments the engine notices for you.
A fight is a sequence of rounds. A round is roughly two seconds long, and during each round everyone in the fight takes their swings. You never script the swings themselves; the mob keeps attacking, defending, and moving exactly as its nature dictates whether it carries a script or not. What you script is what happens AROUND the swings, and the engine offers you these hooks:
- FIGHT_PROG fires once every round, for as long as the scripted mob is in combat. It is the pulse. Taunts, special attacks, rotations, and timers all live here. - HITPRCNT_PROG fires every round that the mob's health is at or below a threshold you name in the header. It is how a fight changes shape as the mob gets hurt: phases, desperation moves, panicked retreats. - DEATH_PROG fires at the moment the mob dies, before its corpse takes its place. Last words and loot. - KILL_PROG fires when the mob kills its target. Gloating, recovery, and tidying up for the next fight. - DAMAGE_PROG is the reserved hook for reacting to each individual hit the mob takes. It has an honest limitation today, covered in its own section below.
While any of these runs, the snapshot codes from the basics chapter point at the people involved, and in combat they settle into a simple pattern worth memorizing now:
- In FIGHT_PROG and HITPRCNT_PROG, $n is the mob's current enemy. So is $t; during a fight the source and the target are the same person, the one the mob is trying to kill this round. If several players are fighting the mob, $n is whichever one the mob is focused on at that moment, and the focus can change between rounds. Write your lines so they still read well no matter whose name lands in them. - In HITPRCNT_PROG, $g additionally carries the mob's current health percent as a number, in case a condition wants to read it. - In DEATH_PROG, $n is the killer. - In KILL_PROG, $n is the victim who just fell. - $i is, as always, the scripted mob itself.
That is the entire map. Five triggers, one pattern of dollar codes. Everything else in this chapter is technique.
The Combat Toolbox
These are the commands and functions this chapter leans on. Each gets a one-line introduction here and a proper workout in the sections below; the full reference for every one of them is in the mudprog-commands and mudprog-functions chapters.
Commands for saying and showing:
say <text> - The mob speaks to the room.
emote <text> - The mob performs a visible action.
mpecho <text> - Narrator text to the whole room.
mpechoat <who> <text> - Narrator text to one person only.
mpechoaround <who> <text> - Narrator text to everyone else.
mpasound <text> - Narrator text to adjacent rooms.
Commands for hurting, helping, and hindering:
mpdamage <who> <amount> [type] - Direct damage, no attack roll.
mpheal <who> <amount> - Restore health points.
mprejuv [who] - Full restore: health, spell, stamina.
mpaffect <who> <id> <secs> - Apply a debuff condition.
mpcondition <who> <id> <type> <secs> - Apply any condition, full control.
mpunaffect <who> <id> - Remove a condition early.
mpcast <spell> [target] - The host casts a spell it knows.
mpforce <who> <command> - Another living performs a command.
mpkill <who> - The host attacks the target.
mpflee - The host turns and runs.
Commands for creating things:
mpmload <path> - Clone a mob into the room. It is
flagged to despawn on area reset.
mpoloadroom <path> - Clone an item onto the floor. Loot
made this way is NOT despawn-flagged,
so rewards survive until picked up.
mpset <who> short <text> - Rewrite a thing's display line.
mpmoney <who> <amount> - Grant gold directly.
Commands for timing and memory:
mpsleep <seconds> - Pause this script, then continue.
mpalarm <seconds> <line> - Run one line later; continue now.
mpcallfunc <name> - Run a FUNCTION_PROG routine.
mpsetvar <who> <name> <value> - Store a note that survives between
trigger runs.
mpargset <slot> <value> - Park a value in a $0..$9 slot for
the rest of this one run.
And the questions this chapter asks in if lines: rand(n) rolls an n-percent chance; randnum(n) picks a number from 1 to n; math(...) does arithmetic; var($i name) reads a stored note; hitprcnt($i) is the mob's health percent; hp($i) is its exact health; isfight($i) answers "am I in combat right now"; affected($i name) answers "is this condition on me"; and ispc($n) answers "is this a player". If any of those look unfamiliar, skim the mudprog-functions chapter once and come back; this chapter uses them constantly and explains each at first use.
A word on mpdamage, because it is the backbone of every scripted special attack: it deals its damage directly, with no attack roll and no chance to miss, but the damage still travels the normal mitigation pipeline, so armor and resistances soften it exactly as they would a real blow. The type word matters for that reason. The choices are blunt, cutting, thrusting, pierce, heat, fire, cold, ice, shock, lightning, and magic, and if you leave it off you get magic. A fire drake should breathe fire, not magic, so that fire protection means something in its lair. Give players a reason to prepare and they will love you for it.
And a word on mpcast: it makes the host genuinely attempt the spell, exactly as if it had typed the cast command, which means the host must actually know the spell. A mob with no magic fails quietly, in private, and the fight carries on. The same is true of any plain skill command you put in a script body: the engine hands words it does not recognize to the mob as ordinary game commands, so a line like bash $n works only on a mob that has the bash skill. This is why the special attack section below builds its abilities out of mpdamage plus theater instead: that combination works on EVERY mob, no class required, and the room cannot tell the difference. When you want the real mechanical version of an ability on a boss, that is the coder-side NPC ability system; ask a senior builder, and keep the script for the personality on top.
FIGHT_PROG: The Pulse Of The Round
FIGHT_PROG is where combat scripting begins. Once per round, roughly every two seconds, every FIGHT_PROG block on a fighting mob gets its chance to run. Here is the smallest possible fighting personality:
FIGHT_PROG 100
say You will regret every step that brought you here, $N!
~
Attach that, poke the mob, and it threatens you every single round, which teaches you the first and most important lesson of FIGHT_PROG by sheer irritation: the header percent is your volume knob, and 100 is almost always too loud. A fight lasts maybe fifteen to thirty rounds. At 100 the mob speaks thirty times. At 25 it speaks roughly seven times, which reads as a character; at 10 it speaks two or three times, which reads as a professional. The examples in this chapter carry a header of 100 so that when you test them the body fires on demand; before any of them ships on a live mob, lower the number. This chapter will remind you again, because every builder ships a chatterbox exactly once, and after the first complaint, never again.
Two structural facts about FIGHT_PROG that the rest of the chapter builds on. First, you may write several FIGHT_PROG blocks on one mob, and every round each of them separately rolls its own header. That is not a flaw; it is the professional layout. One block at 100 can be the quiet engine that counts rounds and manages timers without printing anything, while two or three blocks at low percents carry the flavor. The blocks run in the order written, so put the engine block first and the flavor blocks after it; that way, anything the engine computed this round is already fresh when the flavor blocks read it.
Second, a keyword header does nothing on FIGHT_PROG. Keywords filter the text rider $g, and a combat round carries no text, so a header like FIGHT_PROG bloodlust does not mean "fire when someone says bloodlust"; it quietly means "fire every round", which is a 100 you did not intend. On FIGHT_PROG the header is a percent, a zapper mask, or nothing.
One warning before the fun starts: do not try to pace a fight with mpsleep inside FIGHT_PROG. An mpsleep pauses only the one run that is sleeping; the next round fires a brand new run of the same block regardless, and now two copies of your sequence are talking over each other, then three. The round pulse itself is your clock, and the counter section below shows how to slow things down properly. Save mpsleep for triggers that fire once, like DEATH_PROG farewells or GREET_PROG speeches.
Taunt Tables: Variety With randnum
A mob that repeats one taunt is a recording. A mob that draws from a table of them is a personality. The tool is the switch block from the flow chapter combined with the randnum function: randnum(3) returns 1, 2, or 3, fresh each time, and the $%...% wrapping pastes that result into the switch line, which then picks the matching case:
FIGHT_PROG 100
switch $%randnum(3)%
case 1
say The pit has swallowed better than you.
case 2
emote drags a claw along the pit wall, raising sparks.
case 3
say Your friends will hear about the pit. From me.
endswitch
~
Each firing rolls once and performs one entry: a threat, a gesture, or a promise. Three entries is a demonstration; six to ten is a character. Mix says with emotes so the mob does not only talk, and give one entry to mpasound so the roar leaks into neighboring rooms; there is no better advertising for a boss chamber than the fight being audible from the hallway. Live, this block would carry a header of 20 or so, giving a varied bark every few rounds.
Want to avoid hearing the same entry twice in a row? Remember the last roll in a note and reroll around it: store $%randnum(3)% into a slot with mpargset, compare it against var($i lasttaunt), and if they match, treat it as the next entry up instead; then save the one you used back into the note with mpsetvar. It is three extra lines and pure polish; build it after the variables chapter feels comfortable, not before.
Choosing Your Words By Foe: Zapper Taunts
The header of a FIGHT_PROG can be a zapper mask instead of a percent, and in combat the mask is judged against $n, the current enemy. That means a mob can carry different fighting words for different foes:
FIGHT_PROG -player
say Flesh and blood at last. The dead make dull sparring partners.
~
FIGHT_PROG -class +mage +necromancer
say Keep your spellwork, robe. It will not save you.
~
The first block fires only when the current enemy is a player. The second fires only when the enemy is a mage or a necromancer, so casters get needled about their robes while a warrior never hears it. You can filter by -race, by -level with a number or a range like 30-40, by -name, even by -deity; the full mask grammar is in the triggers chapter. Two honest notes: a zapper header always fires when it matches, every single round, so a live version wants the flavor rolled inside the body with rand(), as in if rand(20); and because $n can switch between enemies mid-fight, a party with one mage in it will sometimes hear the robe line and sometimes not, which is exactly the right amount of uncanny.
Scripted Special Attacks
Here is the move that turns a stat block into a monster: the scripted special attack. It is three lines long and the pattern never changes. Announce, show, strike:
FIGHT_PROG 100
say Burn with the rest of them, $N.
mpecho A wash of cinders bursts across the room in a hissing fan.
mpdamage $t 15 fire
~
Read it as a piece of stagecraft. The say is the tell, the beat where the players' eyes come up from their skill bars. The mpecho is the effect, narrated to the whole room so even bystanders flinch. The mpdamage is the substance: fifteen points of fire, delivered to $t, the mob's current combat target, softened by whatever fire protection the victim was wise enough to wear. Fifteen is a pinch for a mid-level character; against a boss you might see forty. Numbers are balance questions, and the honest guidance is: start small, hit your own test character with it, and tune. A special attack should sting and be seen, not delete somebody from a script line.
Live, this block should NOT fire every round. The lazy fix is a header of 30. The elegant fix is a cooldown, and conditions make wonderfully honest cooldowns because they expire by themselves:
FIGHT_PROG 100
if !affected($i ember_cd)
mpcondition $i ember_cd debuff 15 0 0
say The forge never empties, fool.
mpecho Embers stream from the creature's jaws in a scorching gout.
mpdamage $t 12 fire
else
emote smolders, banked heat crackling under its hide.
endif
~
Walk through it. The function affected($i ember_cd) asks whether a condition named ember_cd is currently on the mob itself; the leading exclamation mark means not. The first round of a fight, no such condition exists, so the attack fires, and the very first thing the body does is give the mob that condition for fifteen seconds with mpcondition. For the next fifteen seconds, about seven rounds, the if answers no and the else branch plays a smoldering tell instead, which is lovely: observant players learn to READ the cooldown. When the condition quietly expires, the next round breathes fire again. No counters, no cleanup, no way for the timer to leak: the condition system does the bookkeeping.
The condition id ember_cd is a name I invented on the spot, and that is allowed: a condition with a made-up id has no mechanical effect of its own, which makes it a perfect inert timer. The ids with real teeth, rooted, stunned, and their kin from the condition catalog, are a different tool; mpaffect $t rooted 6 genuinely pins the target for six seconds, and a breath that roots as well as burns is a boss move. Use real ids deliberately and sparingly; being stunlocked by a script is nobody's idea of a good evening.
Counting Rounds
Every technique in the rest of this chapter wants to know what round it is: rotations advance on it, enrages trip on it, drummers drum on it. The engine does not hand you a round number, and it does not need to; the counter idiom from the variables workbook builds one in four lines. Here it is doing nothing else, so the machinery is easy to see:
FIGHT_PROG 100
if var($i round) == ''
mpsetvar $i round 0
endif
mpargset 1 $%math($<$i round> + 1)%
mpsetvar $i round $1
emote raises one clawed finger, marking round $1 of the bout.
~
Line by line, because this shape must become muscle memory. The opening if is the seeding step: the very first round of the mob's very first fight, the note called round has never been written, and doing arithmetic on an empty note gives nonsense, so we file a 0 first. Every later round finds a number there and skips past. The mpargset line is the whole engine: innermost, $<$i round> reads the stored count as text; around it, math(... + 1) adds one; outermost, mpargset parks the answer in temporary slot 1, readable as $1 for the rest of this run. The next line files $1 back into the note so the count survives to the next round, and the emote spends it. Cabinet to workbench to cabinet.
Because this block prints something every round it is a teaching toy, not a shippable script. In real use the counter block is silent: it counts, it stores, and the blocks after it read $1 and decide. Which brings us to rotations.
Ability Rotations
A rotation is a repeating pattern of moves: frost, then fire, then shadow, then frost again. Players learn the pattern, anticipate the beats, and feel clever doing it, which is precisely the fun. The tool is the round counter plus the math function's remainder operator: the percent sign, in arithmetic, means "the remainder after dividing", so math($1 % 3) cycles forever through 1, 2, 0 as $1 climbs. Test the remainder in an if line for each beat:
FIGHT_PROG 100
if var($i turn) == ''
mpsetvar $i turn 0
endif
mpargset 1 $%math($<$i turn> + 1)%
mpsetvar $i turn $1
if math($1 % 3) == 1
say First the frost takes your feet, $N.
mpcast frostbolt $n
endif
if math($1 % 3) == 2
say Then the fire takes the rest.
mpcast fireball $n
endif
if math($1 % 3) == 0
mpecho A ribbon of shadow snaps out like a whip.
mpdamage $t 14 magic
endif
~
Round one takes the frost beat, round two the fire beat, round three the shadow beat, and round four is frost again, forever. The two mpcast lines are real casts: on a mob that actually knows frostbolt and fireball they resolve as genuine spells, with all their normal costs and effects. On a mob that knows neither, they fail silently and the rotation is carried entirely by its third beat and its spoken tells, which still reads fine in play; but the honest build is to put casting rotations on casting mobs and mpdamage rotations on everything else. Notice the third beat IS an mpdamage special attack; mixing real casts and scripted strikes in one rotation is normal and nobody in the room can tell the seam.
One technical note you will eventually need, recorded here so it finds you: the remainder operator lives happily inside the condition form math(...) as written above, but do NOT move it into the substitution form $%math(...)%. Inside $%...%, the percent sign already has the job of closing the substitution, so a remainder written there ends the substitution early and the arithmetic silently dies. Addition and subtraction are safe in either form; remainders belong in if lines.
Slowing a rotation down is one division: gate the whole ladder behind if math($1 % 2) == 0 and the beats land every other round. Opening moves are one comparison: a block with if $1 == 1 fires exactly once, on the first round of the fight, which is the natural home for a battle cry or an opening buff. And if you want a healer mob to assist mid-rotation, mpforce reaches across the room: a line like mpforce acolyte cast heal $i makes the named mob, if it is present, perform the command as its own. If the acolyte is dead or elsewhere, the line does nothing, silently, which is exactly what you want from theater.
HITPRCNT_PROG: Phases And The One-Shot Latch
FIGHT_PROG gives a fight rhythm. HITPRCNT_PROG gives it a SHAPE: a beginning where the boss toys with you, a middle where it starts trying, an end where it stops holding anything back.
The header of HITPRCNT_PROG is not a percent chance. It is a health threshold: each round of combat, the engine compares the mob's current health percent against the header, and the block fires when health is at or below it. HITPRCNT_PROG 50 means "while I am at half health or worse". And the single most important fact about it is this: it fires EVERY qualifying round, not once. A mob at 40 percent health with a bare say line under a 50 header repeats that line every two seconds until something dies. Here is that mistake in the flesh:
HITPRCNT_PROG 60
say The wound stings, $N! You will pay for it!
~
Attach it, whittle the mob below sixty percent, and enjoy the echo. Every phase block you ever write therefore wants the one-shot latch: a note that records "this already happened", checked on the way in.
HITPRCNT_PROG 60
if !var($i stung)
mpsetvar $i stung 1
say The wound stings, $N! You will pay for it just the once.
endif
~
The condition !var($i stung) is true only while the note named stung on the mob itself has never been set; the first qualifying round sets it and speaks, and every qualifying round after that walks in, finds the latch closed, and leaves in silence. This latch is the single most important pattern in boss scripting. Here it is again at half health with something meatier inside, the shape you will actually ship:
HITPRCNT_PROG 50
if !var($i bloodied)
mpsetvar $i bloodied 1
say Half measures are over, $N.
mpecho The beast plants its feet and stops toying with its food.
endif
~
A phase change earns its drama with all three channels at once: a spoken line for the character, an mpecho for the room, and, in the richer examples below, a mechanical change so the fight actually FEELS different afterward. A self-buff through mpcondition, a summon, a new entry in the rotation: something the players can measure. Theater plus substance; either alone is half a phase.
Phases In Order, Even Under Burst
A boss with several phases has several HITPRCNT blocks, and two rules keep them honest. Write them in descending header order, seventy before forty-five before fifteen, and chain them with a single phase note so each phase requires the previous one:
HITPRCNT_PROG 70
if !var($i stage)
mpsetvar $i stage 1
say You have my attention. Few earn it.
endif
~
HITPRCNT_PROG 35
if var($i stage) == 1
mpsetvar $i stage 2
say And now you have my anger. None survive it.
mpcondition $i risen_fury buff 60 5 0
endif
~
The first block latches on "stage has never been set" and opens stage one. The second requires stage to be exactly 1, so it can never fire first, and it advances the note to 2 so it can never fire twice.
Now consider the nightmare scenario for phase scripts: a burst. A heavy crit drops the boss from eighty percent to twenty in a single round. Health is now at or below BOTH headers, so this round both blocks fire, in the order written: the seventy block runs first and sets stage to 1, then the thirty-five block runs, finds stage exactly 1, and advances to 2. Both phase speeches play, back to back, in one round, in the right order, and nothing is skipped. That is why the descending order matters: written the other way around, the thirty-five block would check stage before the seventy block had set it, and the anger phase would silently wait an extra round. Order plus chain makes phase scripts burst-proof, and you get that safety for free just by writing the blocks top to bottom the way the fight will meet them.
What about healing? If your boss regains health above a threshold, the latch stays closed; phases are one-way by default, which is usually right. A boss that should re-arm, a creature that calms down when its health is restored, can reopen its own latch: in whatever block does the healing, set the phase note back with mpsetvar, and the phase will fire again on the way back down. Do that deliberately or not at all; a phase that flickers on and off around a threshold as heals land is noise, not drama.
One tester's blessing worth knowing early: mudprog <target> test HITPRCNT_PROG pretends the mob's health is effectively zero, so EVERY HITPRCNT block on the mob qualifies at once and the whole ladder cascades in written order, one test, every phase speech in sequence. It is the fastest way to proof read an entire boss arc without swinging a sword, and if the phases come out in the wrong order on the cascade, they are in the wrong order in the script.
Being Hurt: DAMAGE_PROG And Its Live Substitutes
DAMAGE_PROG is the trigger reserved for "the mob just took a hit", with $n as the attacker. Here is what one looks like:
DAMAGE_PROG 100
emote rocks back a step, then bares broken teeth in something like a grin.
say Good. I was starting to think you could not hit.
~
Now the honest part, stated plainly so it never wastes an afternoon of yours: the live combat code does not yet call this trigger's hook. Today a DAMAGE_PROG block fires only under mudprog <target> test DAMAGE_PROG, which makes it a fine place to draft reactions for the future, and a poor place to put anything a live fight depends on. Write it, test it, keep it in your back pocket; when the hook is wired, scripts written like the one above will simply begin working.
Meanwhile, everything players actually want from "react to being hurt" is reachable through the two triggers that DO fire live. Coarse reactions are HITPRCNT_PROG's whole job: crossing seventy, fifty, twenty-five percent health IS being hurt, arranged in dramatic order. And fine-grained reactions, "that particular blow was enormous", can be rebuilt inside FIGHT_PROG with a watchdog: remember your health each round, compare it against the round before, and react to the difference. Here is a mob that notices any forty-point round:
FIGHT_PROG 100
if var($i lasthp) == ''
mpsetvar $i lasthp $%hp($i)%
endif
mpargset 3 $%hp($i)%
mpargset 4 $%math($<$i lasthp> - $3)%
if $4 >= 40
say That one I felt, $N. There will not be another like it.
mpcondition $i guarded_stance buff 20 3 0
endif
mpsetvar $i lasthp $3
~
Walk it through. The seed if files the current health the first time the block ever runs. Each round after that, slot 3 takes a fresh reading of hp($i), slot 4 computes last round's health minus this round's, and the if asks whether the fight cost the mob forty or more points since the last pulse. If it did, the mob says so and buys itself twenty seconds of a guard stance buff; either way the last line files the fresh reading for next round's comparison. The watchdog is silent in an ordinary round, which is what makes its one line land when a barbarian finally connects. Tune the forty to taste; against level differences it is the FEEL of "a big hit for this mob" you are encoding, not a universal number.
Summoning Adds With mpmload
Nothing changes the temperature of a boss fight like the door bursting open. In scripting terms, reinforcements are the mpmload command: it clones a mob from a file path into the room, mid-fight, exactly when your script calls for it. Three facts govern it.
First, whatever you load is remembered as $b, the "last loaded thing", for the rest of the same trigger run. That matters because of the re-skin trick: mpset $b short <text> rewrites the newcomer's display line on the spot, before anyone has read it. The examples in this guide summon /obj/torch, the harmless practice object, and re-skin it into whatever the story needs, so you can paste them anywhere without hunting for monster files. On a real boss you would point mpmload at a genuine NPC file from your area, and then the add has real teeth: it fights, it dies, it drops things. Ask a senior builder which files are fair game in your realm.
Second, $b does not survive into other trigger runs. Load and re-skin in the same block, always. A later block that says $b gets nothing.
Third, anything loaded by mpmload is automatically flagged to despawn when the area resets, so a wiped party does not leave the room permanently crowded with your reinforcements. Loot from mpoloadroom is deliberately NOT so flagged; summons are temporary, rewards are not.
Here is the classic single-latch summon phase:
HITPRCNT_PROG 60
if !var($i called)
mpsetvar $i called 1
say To me, kennel-born!
mpmload /obj/torch
mpset $b short a slag-hound
mpmload /obj/torch
mpset $b short a slag-hound
mpecho Two slag-hounds bound out of the vents, trailing sparks.
endif
~
The latch is doing heavy lifting here, and it is worth saying why: without it, this block would summon two more hounds EVERY ROUND the boss sat below sixty percent health, and in a minute the room is carpeted. Summon blocks are the most expensive thing a combat script can repeat, so they are always latched, or capped by a counter as in the necromancer exercise at the end of this chapter, or both. If you prefer counting what is actually standing there, the ishere() function can check for a survivor before calling more; just remember that a re-skinned torch still answers to the name torch, because mpset ... short changes only the display line, not the thing's true name. Re-skins are costumes, not surgery.
When your adds are real NPCs, two lines make them feel intentional. An aggressive NPC file will join the fight on its own; a neutral one can be sent in with force: mpforce $b kill $n, written in the same block as the load while $b still points at the newcomer, turns the add loose on the boss's current enemy immediately. And a summon wants scale sometimes; a for loop delivers a wave in three lines:
HITPRCNT_PROG 30
if var($i wave) != 1
mpsetvar $i wave 1
say Rise, all of you. Earn your keep.
for $3 = 1 to 3
mpmload /obj/torch
mpset $b short a grave-ember
next
mpecho Grave-embers flare to life in a ring around the room.
endif
~
The loop runs its two lines three times, and because each mpmload updates $b, each fresh clone gets its costume before the next arrives. Notice the loop counter lives in slot 3, leaving slots 1 and 2 free for any round counter running elsewhere on the same mob; slots are shared across the whole run, so give each job its own number and you will never chase a collision.
How many adds is too many? A good default: never more standing than the party can see the point of. One add changes a fight's geometry, two split the healers' attention, three read as a wave event. Ten is a slideshow. And always, always give the room the mpecho; reinforcements that appear without narration read as a bug, and the same creatures arriving inside a sentence read as a set piece.
Enrage Timers
An enrage timer is a promise: finish this fight before the clock runs out, or it finishes you. Even a soft version, more damage and louder lines rather than instant doom, gives a long fight a third act. You have three clocks to build one from, and choosing between them is the whole design decision.
The first clock is the round counter you already own. Rounds are about two seconds each, so round fifteen is roughly the thirty-second mark: take the counter block from earlier, and after the count is stored add a latch that trips at the chosen round, if $1 >= 15 guarded by a note in exactly the one-shot shape from the phase section. Round-based enrages track the FIGHT: if the boss spends time out of combat between victims, the count simply resumes where it left off, which suits a patient monster.
The second clock is a condition used as an egg timer, the ember_cd pattern from the cooldown section turned inside out: at the start of the fight give the mob a harmless condition lasting sixty seconds, and let a FIGHT_PROG block watch for its ABSENCE; when affected($i my_timer) turns false, time is up. Condition clocks track REAL time and clean themselves up, but they keep ticking even if the fight ends, so guard the payoff with isfight($i).
The third clock is the sharpest: mpalarm, which schedules one line to run after a delay and lets the script carry on now. Pair it with a FUNCTION_PROG routine, the named-subroutine trigger from the triggers chapter, and you get a wall-clock enrage with its whole payoff written in one tidy place:
FIGHT_PROG 100
if !var($i clockset)
mpsetvar $i clockset 1
say Thirty breaths, little hero. Then I stop playing.
mpalarm 30 mpcallfunc stopplaying
endif
~
FUNCTION_PROG stopplaying
if isfight($i)
mpecho Behind the creature's eyes something goes out, leaving appetite.
mpcondition $i blood_frenzy buff 90 10 0
endif
~
Walk through the timeline. The first round of the fight, the latch opens: the boss announces the deadline, and mpalarm files one line, mpcallfunc stopplaying, to run thirty seconds from now. The fight carries on normally; the script does not pause, because mpalarm is a split, not a sleep. Thirty seconds later the filed line runs, and mpcallfunc invokes the FUNCTION_PROG named stopplaying. That routine begins with the only guard that matters on any delayed payoff: is the mob still fighting? If the party won, fled, or died in the meantime, the routine goes silently back to sleep, and no empty room ever hears an enrage speech. If the fight is still on, the room gets the line and the boss gets ninety seconds of a frenzy buff, which your FIGHT blocks are free to notice too: a taunt block guarded by affected($i blood_frenzy) gives the enraged boss a whole new vocabulary for its final act.
Two habits make timer bosses shippable. First, the announce line is not decoration; a deadline the players never heard about is indistinguishable from random difficulty. Telegraph, then punish. Second, the clockset latch must be reset between fights or the second group ever to fight this boss gets no timer at all; the next section is about exactly that hygiene.
Last Words And Loot: DEATH_PROG
DEATH_PROG fires at the moment the mob dies, with $n as the killer, and it fires exactly once, so it needs no latch, no percent, no header argument at all. It is the shortest window in combat scripting and the most remembered: whatever you put here is the last thing the mob ever does. Here is a full farewell, with a guaranteed reward and a rare one:
DEATH_PROG
say So the coals... finally... cool...
mpoloadroom /obj/armor
mpset $b short a pit-forged breastplate
mpecho The beast collapses, leaving a pit-forged breastplate in the ash.
if rand(20)
mpoloadroom /obj/torch
mpset $b short a sliver of everburning coal
mpecho Deep in the ash, a sliver of everburning coal still glows.
endif
mpmoney $n 25
~
Read it as three acts. The say is the last words; write them in character, and remember the room is mid-fight and full of adrenaline, so shorter is stronger. Then the loot: mpoloadroom clones the reward onto the floor, and the re-skin line dresses the stock item for the story before anyone reads it, exactly as with summons. The mpecho ties the drop into the fiction, which matters more than it sounds: an item that simply appears in the room listing is scenery, and an item the narrator hands you is a trophy. The rand(20) wraps a SECOND drop in a one-in-five roll, which is the entire recipe for rare loot: roll, load, re-skin, narrate. And mpmoney $n 25 presses twenty-five gold directly into the killer's purse, the tidy way to guarantee a bounty without trusting the floor.
On a real boss, point the load lines at genuine treasure files from your area instead of re-skinned props, or lean on the world drop system that already rolls loot for every kill and let the script add only the signature piece. And remember from the toolbox: loot loaded with mpoloadroom stays until someone takes it. Drops from a boss whose party wiped are still lying there when the next group arrives, which is either a lovely found-story or a duplication faucet, depending on the item; for anything powerful, prefer the rand gate low and the item humble, or hand the real reward through a quest.
Three things do NOT belong in DEATH_PROG, each a classic afternoon waster. Delayed lines: an mpalarm filed here has no mob left alive to run it when the delay ends, so it does nothing, silently; if the death should have echoes later, script the ROOM, which survives. Healing: by the time DEATH_PROG runs the death is a fact, and mpheal or mprejuv here cannot cancel it, so a death-cheat mechanic belongs in a HITPRCNT_PROG 10 block that fires while there is still someone to save. And revenge: commands that lash out at $n, the killer, from inside a death block read as spite from beyond the grave and play terribly; if the boss must punish its killer, a condition applied in the final phase, while it still lives, tells that story better.
If you write several DEATH_PROG blocks, they all run, in order, which is a pleasant way to separate the theater block from the loot block so each stays readable.
Winning: KILL_PROG
The mirror moment: KILL_PROG fires on the scripted mob when it kills its target, with $n as the fallen. Most builders forget this trigger exists, which is why most monsters win in silence. A single gloat line, written in character, is worth ten taunts, because it lands at the emotional bottom of somebody's evening:
KILL_PROG
say Sleep now, $N. The pit keeps what it takes.
mprejuv $i
mpsetvar $i phase 0
mpsetvar $i rounds 0
~
The say is the gloat, and notice $N works here even though the victim just died; the engine remembers who fell. Then the housekeeping, and this is the professional part of the block: mprejuv restores the victor to full, so the next group finds a fresh monster instead of a wounded one, and the two mpsetvar lines wipe the phase and round notes so every latch in the script is re-armed. A boss that beats one party and then meets the next still enraged, still half-phased, and still convinced it already summoned its hounds is the single most common bug in shipped boss scripts, and these two lines are the cure. Whatever notes YOUR boss keeps, reset them here, in the winning block, while the fight is definitely over.
KILL_PROG is also where escalation lives. Count victories in a note with the counter idiom and the gloats can grow teeth over time: a courteous line at one kill, a bored professional's line at five, and something the whole region hears about at ten, each behind a simple if on the count. One design warning: in a group fight, KILL_PROG fires per victim, so a wipe plays your gloat several times in a row. Keep it short, or vary it with a randnum table like any taunt.
Between Fights: The Reset Habit
KILL_PROG only covers one ending. Fights also end with the party fleeing, the boss losing interest, or a wandering healer scooping everyone off the floor, and none of those fire any trigger at all. The safety net is a low-frequency RAND_PROG that notices peace and tidies up. Here is the pair: a phase latch, and the block that re-arms it whenever the fight is over.
HITPRCNT_PROG 50
if !var($i marked)
mpsetvar $i marked 1
say Half my blood for half of yours, $N!
mpcondition $i grim_resolve buff 60 5 0
endif
~
RAND_PROG 100
if !isfight($i) AND var($i marked)
mpsetvar $i marked 0
mprejuv $i
mpecho The brute shakes itself, wounds closing as its fury banks low.
endif
~
The RAND block rolls every heartbeat, in and out of combat, and its if line is the whole trick, two questions joined by AND: is the mob NOT fighting, and is the marked note still set from a fight that happened? Both true means a fight ended without the housekeeping running, so the block clears the latch, restores the mob, and plays one line so anyone lingering in the room sees the monster visibly reset, which is honest signaling: this fight is over; the next one starts from the top. After the cleanup, the marked note is 0, the AND fails, and the block costs one quiet question per heartbeat. The 100 header is for testing; live, 25 is plenty, since catching the peace within a few heartbeats is as good as instantly.
Make this pairing a reflex: every note a fight sets, SOMETHING must clear. The winning block clears it after victories, the reset block clears it after everything else, and both do the same list of mpsetvar lines. When you add a latch to a boss, walk straight down to those two blocks and add its reset before you forget. Future you, watching a second party fight a mysteriously pre-enraged boss, will have no idea what past you is being thanked for, which is how good hygiene always feels.
Protecting Allies: The ATTACK Veto
Everything so far has been about the mob's own fight. This section is about preventing somebody ELSE's: the bodyguard who steps in front of the blade, the shrine spirit under whose gaze no pilgrim may be touched.
The tool comes from the message bus, covered in full in the mudprog-bus chapter, and the short version is this: before combat begins, the engine asks permission. The moment somebody tries to start a fight, an ATTACK message passes through the room, and every scripted object present gets a chance to veto it with a CNCLMSG_PROG block. If the block's header matches, the block RUNS INSTEAD of the attack: no swing happens, no combat starts, and your script supplies what happens in its place. When the block runs, $n is the attacker, $t is the intended victim, and $g carries the victim's name, which is the detail that makes protection selective, because the header's keyword mask filters on exactly that text. Here is a bodyguard sworn to someone called the curator:
GREET_PROG 100
say Mind yourself around the curator, $N. Her safety is my whole employment.
~
CNCLMSG_PROG ATTACK curator
mpechoat $n An arm like an iron bar slams across your chest, stopping you.
mpechoaround $n The bodyguard slams an arm out, barring $N's lunge cold.
say Not while I breathe.
~
The header reads: cancel ATTACK messages whose text contains curator. Anyone may brawl with anyone else in the room, and anyone may attack the bodyguard itself, but the moment the intended victim is the curator, the lunge dies in the guard's arm instead. The body then OWES the room an explanation, and that is a rule, not a suggestion: a cancelled action with no message is the game silently refusing, which reads as a bug every single time. This script explains it three ways, private, public, and spoken, using the mpechoat and mpechoaround pair so the would-be attacker feels the iron arm and the room sees the save.
Because the veto fires when an aggressive CREATURE tries to start a fight too, the same block quietly protects the curator from wandering monsters, which is a lovely detail for free. And if your bodyguard should do more than block, one more line in the veto body, mpkill $n, sends the guard straight at the offender: the attack on the ward becomes a fight with the guard instead, which is precisely a bodyguard's job description. Add it only when the character would truly escalate; a guard who blocks a dozen times before drawing steel is a personality, a guard who instantly murders curious pokers is an incident report.
Two design notes to keep this honest. First, the veto runs before the fight EXISTS; once combat is underway, ATTACK is no longer asked, so a veto cannot break up a brawl already in progress. It is armor against the first swing, not a riot police. Second, the observing side of the bus, EXECMSG_PROG, does not carry ATTACK today; you cannot passively WATCH fights start through the bus, only veto them. When you want a third party to notice a fight and react without cancelling anything, give the protected one a script of their own; their FIGHT_PROG fires every round they are in danger, and that is your observation hook:
FIGHT_PROG 100
mpecho The curator cries out, her voice cracking with fear.
mpforce bodyguard kill $n
~
Put that on the curator herself, and the round she is attacked she cries out and her guard, if a mob answering to bodyguard is in the room, wades in through mpforce. Between the veto pattern, which makes an ally untouchable, and the cry-for-help pattern, which makes an ally expensive, you can tune protection from absolute to merely dangerous, and the second kind usually makes the better story.
A Complete Three-Phase Boss, Line By Line
Time to spend everything the chapter has earned. Meet Vhalric, warden of a furnace vault: a boss with an entrance, a greeting, a quiet engine, a taunt table, a breath weapon on a cooldown, three phases, adds, loot, a gloat, and full between-fight hygiene. The script is long, but there is not one line in it you have not already met; a boss is only ever the small patterns of this chapter, assembled with intent. Here is the whole thing, followed by the tour:
ONCE_PROG
mpecho Heat shimmers as Vhalric drags his greatblade out of the coals.
~
GREET_PROG 100
if ispc($n)
say Another key for my vault, $N. They all burn in the end.
endif
~
FIGHT_PROG 100
if var($i rounds) == ''
mpsetvar $i rounds 0
endif
mpargset 1 $%math($<$i rounds> + 1)%
mpsetvar $i rounds $1
if math($1 % 4) == 0 AND !affected($i breath_cd)
mpcondition $i breath_cd debuff 20 0 0
say Breathe deep, $N.
mpecho Vhalric vents a fan of white-hot cinders across the chamber.
mpdamage $t 20 fire
endif
~
FIGHT_PROG 25
switch $%randnum(3)%
case 1
say The vault remembers every soul I have fed it.
case 2
emote rolls his shoulders, embers shaking loose from his cloak.
case 3
say Your armor will make a fine shelf piece.
endswitch
~
HITPRCNT_PROG 70
if !var($i phase)
mpsetvar $i phase 1
say You scratch the paint. Very well. In earnest, then.
mpcondition $i cinder_guard buff 90 5 0
mpecho Slag plates grind tighter across Vhalric's frame.
endif
~
HITPRCNT_PROG 45
if var($i phase) == 1
mpsetvar $i phase 2
say Vault, lend me your hungry dead!
mpmload /obj/torch
mpset $b short a cinder wisp
mpmload /obj/torch
mpset $b short a cinder wisp
mpecho Two cinder wisps whirl up from the grates, spitting sparks.
endif
~
HITPRCNT_PROG 15
if var($i phase) == 2
mpsetvar $i phase 3
say Enough. The furnace takes you all.
mpcondition $i furnace_wrath buff 120 10 0
mpdamage $t 25 fire
mpecho The vault floor itself glows as Vhalric burns away restraint.
endif
~
DEATH_PROG
say The vault... keeps... its own...
mpoloadroom /obj/armor
mpset $b short Vhalric's slag-forged cuirass
mpecho The furnace light dies, leaving a slag-forged cuirass in the ashes.
if rand(20)
mpoloadroom /obj/torch
mpset $b short a still-burning vault ember
mpecho Something rarer glints in the ash, a still-burning vault ember.
endif
~
KILL_PROG
say The coals thank you for your visit, $N.
mprejuv $i
mpsetvar $i phase 0
mpsetvar $i rounds 0
~
RAND_PROG 50
if !isfight($i) AND var($i phase)
mpsetvar $i phase 0
mpsetvar $i rounds 0
mprejuv $i
mpecho The forge-light settles as Vhalric banks his fury once more.
endif
~
Now the tour, block by block, with the reasoning that chose each line.
The ONCE_PROG is the establishing shot. It runs a single time, about a second after the mob loads, so the first player ever to reach the chamber, and the first after every respawn, sees the warden arm himself. One narrated line; no latch needed, since ONCE fires once by nature.
The GREET_PROG is the threat on the doorstep. The ispc($n) check keeps him from monologuing at wandering rats and returning cinder wisps; only players get the line. Note what the greeting does for the fight that follows: "they all burn in the end" is the breath weapon, promised in advance. Telegraph, then punish.
The first FIGHT_PROG is the engine, and it runs at 100 on purpose: it is the silent bookkeeping heart of the boss and must not miss a round. Its first four lines are the counter idiom, verbatim: seed the rounds note, add one into slot 1, file it back. Then the breath gate, one if line asking two questions joined by AND. The first, math($1 % 4) == 0, is the metronome: true on rounds four, eight, twelve, and so on, the remainder trick from the rotation section. The second, !affected($i breath_cd), is the cooldown from the special attack section, and carrying BOTH is deliberate belt and braces: the metronome gives the breath its rhythm, the twenty-second condition stops any surprise, like a stun dragging the round clock, from ever landing two breaths close together. Inside, the attack is the full announce, show, strike stanza: a spoken tell with the victim's name in it, a room-wide mpecho, and twenty points of fire to the current target through armor and resistance. Every fourth round, the chamber learns why you do not fight the warden in dry leathers.
The second FIGHT_PROG is the voice, at a live-tuned 25 so he barks about once every four rounds. It is the randnum taunt table from earlier: two threats and a physical tell, one of which is pure menace-by-inventory, because a boss who has already imagined your armor on his shelf is doing psychological damage the balance system cannot measure. The engine block sits ABOVE the taunt block in the script for the reason the FIGHT section gave: blocks run in written order, so the round count is always fresh before anything else fires.
Then the phase ladder, three HITPRCNT blocks in descending order, chained through one note named phase. Seventy is the wake-up: the latch !var($i phase) opens exactly once, sets phase 1, and pairs its speech with substance, a ninety-second defensive buff, and a room line that SHOWS the buff, grinding slag plates, so players see the fight harden rather than reading it in a combat log. Forty-five is the summons: it requires phase to be exactly 1, advances it to 2, and calls the adds, two re-skinned props here, two real wisp NPCs on a live build. The speech comes before the loads so the shout summons the wisps rather than narrating them after the fact; order inside a block is stagecraft too. Fifteen is the desperation phase: phase 2 required, phase 3 set, a two-minute wrath buff, a punctuating burst of fire at the current target, and the room line that tells everyone the last act has started. Because the ladder is descending and chained, a monstrous burst that drops Vhalric from eighty percent to ten fires all three blocks in one round, in order, and the fight still tells its whole story, just breathlessly.
The DEATH_PROG is the funeral. Last words cut short by the dying, a guaranteed trophy re-skinned from the stock armor and narrated into the fiction, and a one-in-five vault ember for the lucky, exactly the rare-loot stanza from the death section. Nothing here schedules anything later and nothing here fights back; the warden loses with his dignity intact.
The KILL_PROG is the other ending. One gloat with the fallen's name in it, then the hygiene: full restore, phase zeroed, rounds zeroed. The next group meets the whole boss, not the dregs of him. And because parties also FLEE, the RAND_PROG safety net closes the loop: out of combat with a phase still set means a fight ended untidily, so it clears the same notes, restores him, and banks the forge-light in one visible line. The breath_cd and buff conditions need no line in either cleanup; conditions expire on their own, which is why the script used them for every timer it could.
That is a complete boss: eleven blocks, no code, every pattern from this chapter doing its one job in its right place. Attach it to a practice mob and walk the whole arc: entrance on load, greeting on entry, then mudprog <target> test HITPRCNT_PROG to cascade all three phases in order, test DEATH_PROG for the loot, test KILL_PROG for the gloat, and idle a few seconds out of combat to watch the safety net bank the fires. Then change every name, every line, and every number, and it is yours.
Testing A Combat Script
Combat scripts have more moving parts than greeting scripts, so they earn a testing ritual. The one that follows catches nearly everything this chapter has warned about, in about two minutes per boss.
First, parse check: mudprog <target> and read the Triggers line. Every trigger you wrote should be listed; a missing one means a missing tilde above it, as always.
Second, fire each trigger cold, in story order: mudprog <target> test GREET_PROG, then FIGHT_PROG, then HITPRCNT_PROG, then DEATH_PROG, then KILL_PROG. Remember the mercies of the test harness: percent headers still roll their dice, so a 25-header taunt table may stay silent a few tries, and that is the dice, not the script; and a HITPRCNT test pretends health is gone, so the whole phase ladder cascades at once, which is the fastest proofread of phase ORDER there is. Watch for the latches too: the second consecutive test of a latched HITPRCNT block should do nothing, and if it speaks twice your latch has a hole in it.
Third, the live rehearsal, because only a real fight tests the round pulse, the cooldowns, and the reset net. Spawn a practice mob or borrow a sparring dummy, attack your scripted boss with a character that can survive it, and let the fight run a dozen rounds. You are listening for cadence: does the engine block stay silent, does the breath land on its metronome, do the taunts come often enough to have presence and rarely enough to stay welcome? Then flee, wait out of combat, and confirm the safety net line plays and a fresh fight starts from phase zero. That last check, the second fight, is the one most builders skip and the one that catches the stale-latch bug every time.
While anything misbehaves, the mudprog log is your friend; sprinkle mplog lines to record which branches run, and pull them out before shipping. A script that blows its step budget writes itself up in the script_runaway log with the trigger name attached, so even a runaway leaves a trail.
Common Mistakes And Their Fixes
Every one of these has shipped on a live mob at least once. Learn them here instead.
Mistake one: the chatterbox. A FIGHT_PROG at 100 with a bare say line, left that way after testing. Symptom: the mob speaks every two seconds for the entire fight and the players mute it emotionally by round five. Fix: headers of 10 to 30 for flavor blocks; 100 is reserved for silent engine blocks and cooldown-guarded attacks. The grep test: if a block at 100 prints something every time it runs, it is a chatterbox.
Mistake two: the unlatched phase. A HITPRCNT block with speech or, far worse, a summon, and no one-shot latch. Symptom: the phase line repeats every round below the threshold; the summon version fills the room with adds until the step budget or the players give out. Fix: the !var($i note) latch, set in the first line of the body, exactly as in the phase section; for summons, a latch AND a headcount.
Mistake three: the stale latch. Latches everywhere, resets nowhere. Symptom: the boss fights perfectly once, then greets its second party pre-enraged, phase-locked, and silent, because every latch closed in fight one is still closed. This is the most reported "broken boss" in existence and it is never the engine. Fix: the KILL_PROG hygiene lines plus the out-of-combat RAND_PROG safety net, clearing the same notes in both.
Mistake four: pacing a fight with mpsleep. Symptom: doubled and tripled lines interleaving as new rounds fire fresh runs alongside paused ones. Fix: the round counter and the remainder trick are the fight's clock; mpsleep belongs to one-shot triggers only.
Mistake five: keyword headers on combat triggers. FIGHT_PROG rage or HITPRCNT_PROG desperate look meaningful and are not: FIGHT carries no text for keywords to match, so the header silently means always, and HITPRCNT reads its header as a number, so a word there is a threshold of zero and the block never fires at all. Fix: percents, masks, or numbers, per the trigger's own header rules; the words go in the body.
Mistake six: revenge and delays in DEATH_PROG. Symptom: nothing visible, which is the trap; the mpalarm quietly evaporates with the mob and the promised aftermath never arrives. Fix: DEATH_PROG is for the present tense; aftermath belongs to a script on the room, which outlives the fight.
Mistake seven: the overtuned mpdamage. A special attack pasted from a level-eighty boss onto a level-ten guard. Symptom: a corpse and a complaint, since mpdamage never misses. Fix: numbers are content; test every attack against a character of the mob's own level and start softer than feels impressive. The armor pipeline will protect the prepared, which is precisely the point of typing your damage.
Mistake eight: trusting $b across blocks. A summon in one block, a re-skin or an mpforce aimed at $b in another. Symptom: the second block does nothing, because $b empties between runs. Fix: load, dress, and command the newcomer in the same block, while $b still points at it.
Exercises
Four little combatants to build on a practice mob, each using only this chapter's patterns. Try each from the task alone before reading its solution; the reading is the answer key, not the assignment.
Exercise one, the hired blade with a survival instinct. Task: a mercenary who fights normally until his health crosses one quarter, then announces, once, that this is above his pay grade, and runs for it. Hint: one trigger, one latch, one command from the toolbox.
A worked solution:
HITPRCNT_PROG 25
if !var($i fled)
mpsetvar $i fled 1
say This is more than they pay me for!
mpflee
endif
~
Walkthrough. The 25 header arms the block at quarter health; the latch guarantees the line plays once, because without it he would re-announce his cowardice every round he failed to find the door; and mpflee sends him through an exit exactly as a panicking player would. If the first flee fails, because the exit was blocked, the fight continues and the block stays silent thanks to the latch; a crueler version moves the mpflee OUTSIDE the latch so he keeps scrambling for the door every round while only speaking once. Both are correct; they are different men. For completeness, remember he keeps the fled note forever unless something clears it, so a shipped version adds the reset habit: an out-of-combat RAND block clearing fled, or a clearing line in KILL_PROG for the fights he somehow wins.
Exercise two, the war drummer. Task: a drummer who counts the beat aloud every round, and on every third beat strikes the great drum, visibly rallying himself. Hint: the counter idiom plus the remainder trick, and remember which form of math the remainder must use.
A worked solution:
FIGHT_PROG 100
if var($i beat) == ''
mpsetvar $i beat 0
endif
mpargset 1 $%math($<$i beat> + 1)%
mpsetvar $i beat $1
say Beat $1!
if math($1 % 3) == 0
mpecho The great drum booms, and every blow lands in time with it.
mpcondition $i drum_surge buff 6 3 0
endif
~
Walkthrough. The first four lines are the counter idiom untouched: seed, add one, file back. The say spends the count every round, which is the assignment's point and, yes, deliberately noisy; he is a drummer. The remainder check fires on beats three, six, nine, using the condition form math($1 % 3) because, as the rotation section warned, the remainder operator must never ride inside $%...%. The payoff pairs show with substance: a room line and a six-second self-buff, refreshed with every third beat while the drumming continues. Variation: give the buff to the room's OTHER fighters by targeting a name, or make silence deadly by having the buff simply expire when the fight drags him below half health and his hands are needed for an axe, which is one HITPRCNT latch and an mpunaffect.
Exercise three, the frugal necromancer. Task: a necromancer who, once she is at or below half health, raises a thrall each round she qualifies, but never keeps more than two raised in the whole fight. Hint: this is a capped counter, not a latch; the block should fire many times and be refused politely after the second thrall.
A worked solution:
HITPRCNT_PROG 50
if var($i thralls) == ''
mpsetvar $i thralls 0
endif
mpargset 1 $<$i thralls>
if $1 < 2
mpargset 2 $%math($1 + 1)%
mpsetvar $i thralls $2
say Up, bones. Earn your rest again.
mpmload /obj/torch
mpset $b short a shambling thrall
mpecho A shambling thrall claws its way up out of the loose earth.
endif
~
Walkthrough. The seed is the familiar first move. Then the count is copied into slot 1 and the if compares it against the cap: raises happen only while the tally is below two. Inside, the tally goes up by one FIRST, so even a script interrupted mid-block cannot raise extras, and then the summon stanza plays: speech, load, costume, narration. The first qualifying round raises thrall one, the next raises thrall two, and every round after that walks in, reads a 2, and walks out silently: a cap, not a latch. Notice what this design does NOT do: it does not notice thralls dying. Kill her second thrall and she stays satisfied with her tally, which suits a frugal mistress of two servants for life. A vengeful version would count the standing dead instead of the raised total, using ishere() to look for a survivor before consulting the tally, and the difference between those two women is one if line, which is the entire craft of this archetype. For hygiene, clear thralls in KILL_PROG and the out-of-combat reset, as always.
Exercise four, the oathbound shield. Task: a shieldbearer sworn to someone called the lantern-bearer. Nobody may so much as begin an attack on the lantern-bearer while the shieldbearer stands in the room; the offender should be stopped cold, told why, and answered with steel. Hint: this is a bus veto with a keyword, and the punishment is one command in the veto body.
A worked solution:
GREET_PROG 100
say The lantern-bearer walks under my oath, $N. Test it if you must.
~
CNCLMSG_PROG ATTACK lantern
mpechoaround $n The shieldbearer wrenches the blow aside before it can land.
mpechoat $n Your strike is turned aside so hard your arm goes numb.
say An oath is not a suggestion.
mpkill $n
~
Walkthrough. The greeting posts the rule where every arrival reads it, which is what keeps the veto feeling fair instead of arbitrary. The veto header matches ATTACK messages whose victim's name contains lantern, so brawls that have nothing to do with the ward pass through untouched. The body pays the messaging debt every veto owes, both perspectives plus the spoken oath, and then mpkill turns the shieldbearer on the offender: the fight they wanted happens, only not with the target they chose. Test the veto honestly: attach it, name a practice mob so it answers to lantern, try to attack that mob, and watch the swing die; then attack the SHIELDBEARER and confirm he fights without any veto, since his own name never matches the mask. And notice the whole solution has no latch anywhere: vetoes fire per attempt by design, because an oath does not have a cooldown.
Where To Go Next
You now hold the complete combat archetype: the round pulse and its volume knob, taunt tables, zapper-filtered fighting words, the announce-show-strike attack stanza, condition cooldowns, round counters and remainder rotations, burst-proof phase ladders, summons with caps and costumes, three kinds of enrage clock, deaths that pay out and victories that clean up, the between-fights safety net, and the ATTACK veto that turns a bystander into a bodyguard. More usefully, you hold the habits: latch what must happen once, reset what a fight sets, telegraph what will hurt, and narrate everything the engine does silently.
For the pieces this chapter leaned on, the mudprog-flow chapter owns if, switch, and the loops in full; mudprog-variables owns notes, slots, and the counter idiom's fine print; mudprog-functions owns every question an if line can ask; and mudprog-bus owns the veto system whose combat corner you met here. The cookbook's pit warden is a compact cousin of Vhalric worth reading now that you can see every seam in it, and the workbooks will keep your hands busy. When your boss needs mechanics beyond theater, real resource bars, real area attacks, tuned hit points, that is the coder-side ability and difficulty system; bring a senior builder your script and your numbers, and the two systems will meet in the middle, which is where every memorable monster on this mud actually lives. Now go make something your players will be wrong about in the tavern for months.
This chapter is an archetype deep-dive: one craft, explored to the bottom. The craft is conversation. Everything a talking NPC can do lives here: the keyword trees that let players steer a chat, the questions an NPC can ask back and actually hear answered, the slow-paced cutscene that turns six lines of text into a performance, the memory that lets a character know you the second time you meet, the friendship counters that open doors kindness by kindness, two NPCs trading lines on cue, a full branching interrogation, and, to close, a complete romance subplot written out and explained line by line.
You do not need to have read anything beyond mudprog-basics to follow along. Every idea from the other chapters is re-explained the first time it appears, and every script in this chapter is complete: you can attach any of them to a practice mob exactly as printed and talk to it. Where a technique has depths, the deeper chapters are the reference shelf: mudprog-triggers for the WHEN, mudprog-commands for the WHAT, mudprog-variables for the memory, mudprog-flow for the branching, and mudprog-functions for the questions an if line can ask.
And the same promise every chapter of this guide makes: nothing here can break the game. A dialogue script with a mistake in it simply says less than you hoped, or says it at an odd moment. Attach, test, tweak, repeat.
What Dialogue Is Made Of
Strip any talking character down to parts and you find only four:
- An ear. The NPC must notice what players say near it. That is the SPEECH_PROG trigger and its keyword headers, plus one stranger tool, REGMASK_PROG, for hearing things SPEECH_PROG cannot. - A voice with timing. Anyone can say six lines in one instant; a character pauses, gestures, and lets a line land. That is mpsleep and its cousin mpalarm. - A way to ask back. Real conversation runs both directions. The mpprompt, mpconfirm, and mpchoose commands let the NPC pose a question and catch the player's typed answer. - A memory. The difference between a vending machine and a character is that the character remembers you. That is mpsavevar and the notes it writes onto players, which survive logouts, reboots, and the death of the NPC itself.
One recap before we start, because every script below leans on it. When a trigger fires, the engine gathers a small cast for that one event, and the dollar codes name the members: $n is the source, whoever caused the event, almost always the player talking to your NPC. $N is that person's name as text. $i is the host, the scripted NPC itself. $g is the message riding with the event, which for a speech trigger is the sentence spoken, in lower case. If any of that feels new, read the first half of mudprog-variables and come back; it is twenty minutes well spent.
The Ear: Speech Triggers Revisited
SPEECH_PROG fires when a player in the room says something, with the scripted mob listening. The header is not a percent chance like most triggers; it is the list of words to listen for:
- SPEECH_PROG harbor ships fires when a spoken line contains harbor or ships, anywhere in the sentence, capitals ignored. - SPEECH_PROG p open the gate, with a leading letter p, fires only when the whole phrase open the gate appears, in order. - SPEECH_PROG all fires on any speech at all. Use it rarely; a mob that answers everything talks over everyone.
Two facts about the ear that shape everything in this chapter. First, the match is by substring, so the keyword art also matches start and party. Choose distinctive words. Second, EVERY speech block is checked against every spoken line, so one sentence can set off two blocks at once if their keyword lists overlap. Keep the lists disjoint, or gate the blocks with memory, which a later section shows.
And one fact that surprises everyone: the mob never triggers on its own speech, and speech blocks only hear PLAYERS. Another NPC talking in the room does not fire SPEECH_PROG at all. That is a kindness, because it makes runaway two-mob arguments impossible by accident, but it also means two NPCs cannot chat through speech triggers. The section on duets shows the two tools that actually do that job.
Here is the smallest complete talker: a market gossip with a greeting that advertises her topics, and one block per topic.
GREET_PROG 100
say Fresh gossip and fresher bread, friend. Ask me about the harbor or the weather.
~
SPEECH_PROG harbor docks ships
say The harbor? Three ships in this week, and one of them flying no flag at all.
~
SPEECH_PROG weather rain sky
say The weather? My knees say rain by nightfall, and my knees are never wrong.
~
Attach it, walk out and in for the greeting, then try it: say what news of the harbor, or is it going to rain. Any sentence containing a listed word gets its answer.
The greeting is doing quiet, essential work. Players cannot read your script; they only know which words matter if the character tells them. That is the first design law of dialogue scripting, and it is worth stating in capitals of the mind: ADVERTISE YOUR KEYWORDS. Every reply that expects a follow-up should name the word that continues the conversation. Watch the law operating in every example in this chapter, and when a tester says nobody talks to my NPC, check this first: the NPC probably never told anyone what to say.
A note on testing before you build bigger. The builder command mudprog <target> test SPEECH_PROG fires the trigger with the single word test as the pretend spoken line, so a block headed SPEECH_PROG harbor will NOT fire from it, because harbor does not appear in the word test. Keyword blocks are tested by actually saying a keyword out loud in the room. This trips up every builder exactly once; let this paragraph be your once.
Going Deeper: The Keyword Tree
One layer of topics makes a signpost. Conversation begins when topics lead to topics, and the shape that emerges is a tree: the greeting is the trunk, each keyword opens a branch, and each branch's reply plants the keyword for the next one. Players experience it as discovery. They asked about the news, the news mentioned a fire, the fire mentioned a stranger, and suddenly they are three questions deep in a story you wrote as four short blocks:
GREET_PROG 100
say Evening, $N. If it is news you want, just say the word news.
~
SPEECH_PROG news
say News enough. The gristmill burned on Frostday, and no lightning did it. Ask about the fire if you have the stomach.
~
SPEECH_PROG fire burned
say I was there. The flames went up blue, $N, blue as deep ice. And a stranger stood watching from the treeline. Ask me of the stranger, if you must.
~
SPEECH_PROG stranger treeline
emote leans close, voice dropping to a whisper.
say Tall, cloaked, and it never once blinked. When the roof fell, it smiled. That is all I know and more than I wanted to.
~
Walk the design, because the discipline matters more than the words. Every reply ends by naming the next keyword: news plants fire, fire plants stranger, stranger is the payoff and plants nothing, which is how a player knows the branch is done. Each branch also listens for a second, natural word (burned, treeline) so a player who phrases the question their own way still lands on it. And the payoff block opens with an emote before the say, which is the cheapest scene-setting there is: the leaning close costs one line and doubles the weight of the whisper.
Three what-ifs, because trees fail in predictable ways:
What if a player says a sentence that matches two branches, like tell me about the burned stranger? Both blocks fire, back to back, in the order they appear in the script, and the mob delivers a small monologue. For a gossip that is almost in character. For anything serious, keep keyword lists disjoint, and remember substring matching when you choose them: if one branch listens for mill and another for miller, the word miller sets off both, because mill is inside it.
What if a player jumps straight to the deep branch, walking up and saying stranger with no greeting, no news, no fire? It fires. Keyword blocks have no memory of each other; the tree's order lives only in how players discover the words, not in the engine. Often that is fine, a friend told them the magic word and the gossip gossips. When it is not fine, when the deep branch is a secret that must be EARNED, you gate it, which is the next section.
What if two branches genuinely need the same keyword? Merge them into one block and branch inside it with if lines instead. One ear, one block; decisions belong inside.
How deep should a tree go? Three levels is the sweet spot, five is the ceiling. Past that, players lose the thread of which words they have tried, and you lose track of which reply plants which keyword. A big NPC is better built as several shallow trees, one per subject, each rooted in its own advertised topic, than as one deep one.
Gates: Dialogue That Unlocks
A secret is only a secret if the NPC will not say it to just anyone. The tool is a note stored on the player: the early branch writes the note, and the deep branch refuses to answer unless the note is there. This is the first taste of memory, a subject with its own section below; here it plays doorman.
GREET_PROG 100
say A word of advice, $N: in this town, say ledger to the right people and doors open.
~
SPEECH_PROG ledger
mpsavevar $n asked_ledger yes
say Keep your voice down. Yes, there is a ledger, and it is not in the counting house. Ask me where when nobody is watching.
~
SPEECH_PROG where hidden
if var($n asked_ledger == yes)
say Under the third flagstone behind the chandlery. You never heard it from me.
else
say Where is what, friend? You are ahead of your own story.
endif
~
The mpsavevar line in the middle block writes a note named asked_ledger onto the player, with the value yes. The deep block opens with an if that reads the note back through the var function: var($n asked_ledger == yes) asks does this player carry that note with that value. A player who skipped the ledger step gets the brush-off in the else branch, and, notice, the brush-off is written in character. A gate that answers I do not understand is a bug report; a gate that answers you are ahead of your own story is a character guarding a secret. Always write the locked door as carefully as the open one.
Because the note is stored on the PLAYER, the gate is personal and permanent. Each player unlocks it for themselves, and a player who asked about the ledger last month is still trusted after a reboot, because player notes ride in the saved character. If you wanted the gate to reset every time the mob respawns instead, you would store the note on $i, the mob, whose notes die with it. Choosing the owner of a note is choosing how long the memory lasts; the memory section below has the full table.
One honest wrinkle to know about: notes never expire on their own. There is no timer that erases asked_ledger after an hour. For secrets and progress flags that is exactly right. For anything that should fade, the pattern is to overwrite the note at the natural reset moment, the way the romance script at the end of this chapter clears a once-per-visit flag in its greeting.
Asking Back: mpprompt, mpconfirm, mpchoose
Everything so far reacts to what players say in the open. Sometimes the NPC should pose the question: what is your name, do you swear it, which of the three do you choose? Three commands do this, and they share one mechanism, so learn it once.
mpprompt <text> prints the text to the triggering player, then quietly catches the NEXT line that player types, whatever it is, before the game ever sees it. The caught line is not spoken aloud, not run as a command, and not shown to anyone else; it is stored, trimmed of stray spaces but otherwise exactly as typed, in a note named prompt_answer on that player.
mpconfirm <text> is the same idea shaped for yes-or-no. It adds a yes/no reminder to the printed question, and it cleans the caught answer before storing it: yes or y becomes yes, anything else becomes no, and the result lands in a note named confirm_answer.
mpchoose <text> is mpprompt under a second name. Use it when the question is a menu, purely so the script reads well; the engine treats them identically, and the answer lands in prompt_answer just the same.
Now the part that makes or breaks every prompt script, so read it twice: THE SCRIPT DOES NOT WAIT. When the mpprompt line runs, the question is asked and the trap for the answer is set, and then the very next line of your script runs immediately, long before the player has typed anything. The answer arrives whenever the player gets around to typing, which might be two seconds later or never. So every prompt script has the same skeleton: ask now, come back later, read the note.
There are two good ways to come back later, and one famous wrong way.
Coming back with mpsleep. Put the prompt line first, then sleep, then read the note with var. The question is asked before the sleep begins, the player types while the script dozes, and the reading happens after. This keeps everything in one block and is the form to prefer when the whole exchange is one question:
GREET_PROG 100
say The cards are restless tonight, $N. Say fortune and I will ask them about you.
~
SPEECH_PROG fortune cards fate
mpsetvar $n confirm_answer
say Very well. But the cards do not answer twice, so be certain.
mpconfirm Shall I turn the cards for you? Type yes or no.
mpsleep 6
if var($n confirm_answer == yes)
emote turns three cards face up with one smooth sweep.
say A road, a crown, a crossed blade. Travel finds you, fortune follows, and trouble follows the fortune.
else
say The cards keep their secrets, then, and you keep yours. Wise, perhaps.
endif
~
Note the first line of the fortune block: mpsetvar with a name and no value ERASES the note. That clearing line is not optional politeness; it is load-bearing. Answer notes are ordinary player notes, which means they persist forever, across sessions and reboots, until something overwrites them. A player who answered yes last week still carries confirm_answer yes today, and without the clearing line your question would appear to answer itself the moment it was asked. The rule: ALWAYS clear the answer note in the same breath as asking, before the prompt line. Every prompt script in this chapter does it, and yours should too.
Notice also what the else branch quietly handles. After the clear, the note holds one of three things when you read it: yes if they agreed, no if they typed anything else, or nothing at all if they never typed, since mpconfirm only files an answer when a line is actually typed. The if catches yes; the else catches both refusal and silence, and for most questions treating silence as a polite no is exactly right. When silence needs its own reply, test for the empty value separately with var($n confirm_answer == ''), two single quotes standing for nothing.
Coming back with mpalarm. The second pattern splits the asking and the judging into different blocks: the prompt block asks and sets an appointment, and a named FUNCTION_PROG block, a routine that only runs when called, does the reading when the appointment lands. It is a little more machinery, and it earns its keep the moment the judging is long, is shared by several questions, or the asking block has other work to finish. Here is a doorkeeper who asks your name once and keeps it:
GREET_PROG 100
emote squints into the lamplight.
if var($n door_nick == '')
say Faces I keep, names I lose. What do they call you? Type the name plainly and nothing else.
mpsetvar $n prompt_answer
mpprompt Well? Your name, friend.
mpalarm 8 mpcallfunc learnname
else
say Back again, $<$n door_nick>. The fire is where you left it.
endif
~
FUNCTION_PROG learnname
if var($n prompt_answer == '')
say Shy, are we? Nameless it is, then.
else
mpsavevar $n door_nick $<$n prompt_answer>
say $<$n door_nick>, is it? I will keep that safe with the faces.
endif
mpsetvar $n prompt_answer
~
Read the flow like the engine does. First visit: no door_nick note, so the greeting asks, clears the old answer, sets the trap with mpprompt, and books an appointment: mpalarm 8 means in eight seconds, run this one command, and the command is mpcallfunc learnname, which invokes the routine by name. The player types their name; it vanishes into prompt_answer. Eight seconds after the question, learnname wakes, copies the answer into a permanent note with mpsavevar $n door_nick $<$n prompt_answer>, greets them by it, and tidies up by clearing prompt_answer again. Every visit after that, forever, the else branch of the greeting uses the stored name. An NPC that asks your name once and never again is a small thing that players remember for years.
Now the famous wrong way, so you recognize it in your own script at midnight. It is tempting to skip the routine and put the judging straight into the alarm line:
mpalarm 8 say So your name is $<$n prompt_answer), then?
This always speaks the OLD answer, usually nothing. The reason is subtle and worth owning: the text of an mpalarm command has its dollar codes filled in when the alarm is SET, not when it goes off. At setting time the player has not answered yet, so the substitution reads an empty note, and eight seconds later the alarm faithfully speaks the emptiness it captured. The routine pattern exists precisely to dodge this: the alarm line carries only mpcallfunc and a name, nothing to substitute too early, and the reading happens inside the routine at run time. The rule in one line: never read an answer note in an mpalarm command; read it inside the routine the alarm calls, or after an mpsleep.
Four more edges of the prompt family, each of which will bite somebody, so let it not be you:
- The trap catches the next line WHATEVER IT IS. If the player, not realizing a question is pending, types north or inventory, that word becomes their answer and the command never executes. Soften this by asking only when the player plainly expects a question, saying type in the question text, as every example here does, and judging unanswered or nonsense answers gracefully. - One trap at a time. If a script asks a second question while the first is still unanswered, the second trap simply fails to set, and the next typed line goes to the first question. One question per beat; get the answer, then ask the next. - Comparisons forgive capitals. The var function compares text without caring about case, so a player who answers YES or Yes still matches yes. What it does not forgive is extra words: my name is Tam stores exactly that, all four words. Say type the name plainly and nothing else, and mean it. - The answer notes are ordinary notes on the player. You can read prompt_answer in text with $<$n prompt_answer>, test it with var, copy it somewhere permanent with mpsavevar, and clear it with a bare mpsetvar. There is no other magic to them beyond who writes them.
How long should the appointment be? Give players longer than you think: eight to ten seconds for a word, fifteen to twenty for anything they must think about. The examples in this chapter run short so your testing moves quickly; on a live NPC, be generous. Nothing feels worse than an NPC declaring silence is also an answer while you were still typing.
Pacing: mpsleep And The Art Of The Cutscene
Type six say lines in a row and the engine performs all six in the same instant: a wall of text, read in any order, felt as none. Put two seconds between them and the same six lines become a scene. The pause is not decoration; it is where the audience feels.
mpsleep <seconds> pauses the script where it stands and resumes with the remaining lines after the delay. Whole seconds only, minimum one. Everything the script knows, its cast, its slots, survives the pause untouched. Here is a lamplighter whose entire character is rhythm:
GREET_PROG 100
emote touches a taper to the first lamp, and the flame steadies.
mpsleep 2
say Every dusk I light nine lamps, $N. One for each of the old wards.
mpsleep 3
emote moves down the row, and shadows retreat one pace at a time.
mpsleep 3
say The ninth lamp I light last, and I light it for the ones who never came home.
mpsleep 2
mpecho For a moment, all nine flames bend the same way, though there is no wind.
~
Ten seconds, five beats, and a room that goes quiet. Study the shape, because it is the shape of every good cutscene: open with an ACTION, not a word, so the room turns to look; alternate voice and gesture so neither goes stale; save the strangest image for last; and end on narration, the mpecho line, which belongs to nobody and therefore lands on everybody. The sleeps are two and three seconds, which is the natural range; one second reads as hurried, five as frozen. And the whole scene runs ten seconds, which brings us to the first rule of cutscene length: under thirty seconds, always. Players arrive mid-scene, players want to ask their question, players WILL walk away from a five-minute monologue, and a room-locked performance that outstays its welcome trains everyone to avoid the room.
Now the edges, because a paused script lives in a world that keeps moving:
The player can leave mid-scene. The script does not stop; the remaining lines play to the room as written. Room-directed lines, say and emote and mpecho, behave perfectly, an actor finishing the scene to whoever is left. But privately aimed lines have a quirk: mpechoat $n delivers to the player WHEREVER THEY NOW ARE, even three rooms away, which reads as a ghostly whisper through the walls. Sometimes that is delightful and you will use it on purpose. When it is not, keep personal lines before the first sleep and let everything after the pause belong to the room.
The host can die mid-scene. If the mob is killed or swept away while asleep, the rest of the scene is simply dropped, no error, no half-lines from beyond. A narrator who dies mid-sentence stays interrupted, which is exactly right.
Sleeps and loops do not mix. This one is an engine honesty note: an mpsleep inside a for or while finishes the current pass after the pause and then ABANDONS the remaining passes, continuing after the loop. A loop that tolls a bell five times with a sleep inside it tolls once. Write the five tolls out as lines with sleeps between them, or use an alarm ladder:
mpalarm 2 mpecho The bell tolls once.
mpalarm 4 mpecho The bell tolls twice.
mpalarm 6 mpecho The bell tolls a third time, and holds.
which sets three independent appointments in one instant and needs no loop at all. Inside if branches, by contrast, mpsleep works exactly as you would hope, and the interrogation and romance scripts below sleep inside branches freely.
mpsleep or mpalarm? One breath versus an appointment. mpsleep suspends this script and resumes it, keeping everything in one readable column; it is the tool for a paced SEQUENCE. mpalarm schedules exactly one command and moves on immediately; it is the tool for a delayed afterthought, a repeating clock built of several appointments, or the prompt-judging pattern from the previous section. When you find yourself writing three mpalarms that tell one continuous story, you wanted mpsleep; when you find yourself sleeping just to run one final line, you wanted mpalarm.
Two last touches of craft. A cutscene that greets every arrival becomes wallpaper by the third viewing; the once-per-player gate from the memory section below fixes that, and one of the exercises at the end of this chapter has you build exactly it. And rooms can host cutscenes too: attach a script to the room itself with mudprog here edit and a GREET_PROG plays your entrance atmosphere with no mob in sight, which is how a haunted chapel breathes.
Memory: NPCs That Remember You Between Meetings
You have now seen notes twice in passing, gating a secret and keeping a name. Time to look straight at them, because memory is the single cheapest way to make players love an NPC.
mpsavevar <object> <name> <value> writes a note: onto whichever object you name, under whatever name you choose, holding whatever text follows. mpsetvar is the identical command under a second name; the engine does not distinguish them. This chapter uses the two spellings to signal intent to the human reader: mpsavevar for memories meant to LAST, mpsetvar for scratch values like prompt answers that are cleared and rewritten constantly. Adopt the convention or not, but know the engine treats them the same.
Where you put the note decides how long the memory lives, and this table is the whole law of it:
- A note on the mob ($i) lasts as long as that particular copy of the mob: death, an area reset, or a reboot wipes it. Right for moods, scene state, and anything that should start fresh with a fresh mob. - A note on the room lasts until the room reloads. Right for state a whole scene shares. - A note on the player ($n) is written into their saved character. It survives logging out, reboots, and the mob dying a hundred times over. Right for everything this section is about: met-before flags, learned names, favors owed, affection earned.
The trick to hold onto: for memory BETWEEN meetings, the note goes on the player, never on the mob, because the mob you scripted today will be dead and respawned by the weekend while the player sails on. The memory travels in the player's pocket; every fresh copy of the mob reads it back and carries on as if it never died. Players experience continuity; you know it is a note called way_drink riding in their save file.
Here is the pattern at its purest, an innkeeper who learns your usual:
GREET_PROG 100
say Evening, and welcome to the Waystone.
if var($n way_drink == '')
say First time under my roof? Name your drink sometime: ale, cider, or wine, and I will remember it.
else
emote sets a cup of $<$n way_drink> on the bar before you reach it.
say Your usual, of course. Some things a good host never asks twice.
endif
~
SPEECH_PROG ale cider wine
if strin(ale $g)
mpsavevar $n way_drink ale
endif
if strin(cider $g)
mpsavevar $n way_drink cider
endif
if strin(wine $g)
mpsavevar $n way_drink wine
endif
say Noted, friend. From now on your cup fills itself.
~
The greeting asks one question of the note: empty or not? Two single quotes mean nothing-at-all, so var($n way_drink == '') is exactly is this a stranger. The speech block listens for any of the three drink words, then uses the strin function, which asks does this word appear in that text, to check the actual spoken line, $g, for each drink in turn, filing the one it finds. Say I suppose cider then, walk out, walk back in: the cup of cider is on the bar before you are through the door, and it still will be a month from now.
Three honest details, each a small lesson:
If a player names two drinks in one breath, ale then wine, every matching if runs in order and the LAST one wins, so wine is filed. For a drink order, shrug; for anything that matters, make the checks exclusive by nesting each later if inside the previous else.
The substring rule follows strin everywhere: ale hides inside tale and pale, so a player saying what a pale sunset would file ale. Live with small absurdities in small scripts; for precision, listen for longer words or a p phrase header.
Notes store the substitution, not the code. mpsavevar $n greeted_by $I files the host's short description as it reads at that moment; if the mob is renamed later, old notes keep the old text. Usually you want exactly that, a memory of how it was. Just know it.
One tester's tip that will save you an evening: while iterating on a memory script, you constantly want to be a stranger again. Admins can wipe a note on themselves in one line with the script scratchpad, for example scripttest mpsetvar $i way_drink, because in that harness $i is you. No admin access? Store your test notes on the MOB while developing, where a quick clear of the script and a fresh mob resets everything, then switch the owner to $n when the logic is proven.
Counters: Friendship, Reputation, Trust
A yes-or-no note remembers THAT something happened. A counter remembers HOW MUCH, and how much is where relationships live: five kindnesses is a friend, one is a stranger with good manners. A counter is just a note holding a number, plus one idiom to move it, and you have already met every piece of the idiom in mudprog-variables. Spelled out once, slowly:
Line one and two are the seed: if the note has never been written, file a zero, so the arithmetic that follows always has a number to chew on. Do not skip the seed; adding one to an empty note quietly produces zero, and your counter sticks there forever, a bug with no error message. Line three is the increment: the angle form $<$n marn_friend> reads the current count into the sentence, MATH does the sum, and mpargset parks the result in temporary slot 1. Line four files the new total back. Seed, add, file: every counter in every script below is these four lines wearing different names.
Now put a counter to work. Marn is a fence, a dealer in goods of flexible provenance, and Marn warms to people slowly:
GREET_PROG 100
emote looks up from a tray of mismatched rings.
if var($n marn_friend >= 3)
say My favorite customer. For you, the good stock is always out.
else
say Browse if you like. Kindness is the only coin that gets you past the counter, mind.
endif
~
SPEECH_PROG thank kind fine work
if var($n marn_friend == '')
mpsavevar $n marn_friend 0
endif
mpargset 1 $%MATH($<$n marn_friend> + 1)%
mpsavevar $n marn_friend $1
if var($n marn_friend >= 3)
say You have a good way about you, friend. Say backroom sometime, and mean it.
else
say Hah. Flattery costs you nothing and pays me plenty. Keep it coming.
endif
~
SPEECH_PROG backroom
if var($n marn_friend >= 3)
say Through the curtain, past the crates, knock twice. Tell them Marn counts you a friend.
else
say The backroom is for friends, and we are not there yet, you and I.
endif
~
The moving parts: any spoken line containing thank, kind, fine, or work, which between substrings catches thanks, kindness, and fine work, bumps the counter by one and answers in one of two voices depending on where the count now stands. The greeting reads the same counter and warms accordingly. And the payoff, the backroom password, is a gated branch exactly like the ledger secret earlier, except the key is not a flag but a threshold: three kindnesses. Notice the block itself TELLS you when you cross the line, say backroom sometime, obeying the advertise-your- keywords law even for secrets; the count decides who hears the advertisement.
The threshold comparisons deserve one careful look. var($n marn_friend >= 3) reads is the note a number that is three or more; an unwritten note counts as below every threshold, so strangers fail all gates without any special case. When an NPC has several tiers, always test the HIGHEST threshold first and walk downward through nested elses, the way the romance greeting below does with four tiers. Test lowest first and the lowest tier swallows everyone, because a count of six is also more than one, and your beloved regular gets the stranger speech forever. This is the classic counter bug; when tiers misbehave, check the order before anything else.
Variations, briefly, because counters are a whole toolbox once you see them:
- Counters go down. mpargset 1 $%MATH($<$n marn_friend> - 1)% on an insult block, and rudeness has a price. Let the greeting handle negative counts with a cold tier and the relationship can genuinely sour. - Counters can live on the mob instead. A note on $i counts everyone together, all visitors pooling toward one shared threshold, and dies with the mob: right for a communal effort, wrong for a friendship. - One event, different weights. A kind word can add one while a real favor, delivered through a GIVE_PROG when the player hands over an item, adds two or five. The romance script below prices a home-cooked meal at exactly double a compliment. - No clocks. There is no timer to decay a counter daily; script engines here do their work when triggers fire. If a relationship should cool with neglect, do the cooling at the next meeting, inside GREET, which is when anyone would notice coldness anyway.
And a naming commandment, once, in this chapter too: every script shares one pocket of notes per object, so a note called friend will collide with the next builder's friend on the same player. Prefix every note with your NPC's name, marn_friend, elara_aff, weasel_heat, as every script in this chapter does. Future you is one of the builders you are protecting.
Two NPCs Talking To Each Other On Cue
Sooner or later you want a double act: the guard and the recruit, the haggling old couple, the conspirators who go quiet when you walk in. And here you hit the wall this chapter warned you about at the start: SPEECH_PROG only hears players. One NPC saying lines does not fire another NPC's speech blocks, ever. The wall is a safety feature, it makes accidental infinite arguments impossible, but it means a duet needs one of two deliberate designs.
Design one, and the one to prefer: THE CONDUCTOR. Both actors stand in the room, but only one carries the script. It speaks its own lines with say, and it speaks the PARTNER'S lines by puppeting them with mpforce, which makes any named creature in the room perform any command as if it had typed it. Sleeps set the rhythm. The partner needs no script at all; it is an instrument, and the conductor plays it:
GREET_PROG 100
say Recruit! Eyes front. We have an audience, so we drill it clean.
mpsleep 2
mpforce recruit say Sir, yes sir! Clean as rain, sir!
mpsleep 2
say Shield high. A shield you can see over is a shield the enemy sees under.
mpsleep 2
mpforce recruit emote snaps the drillshield up to the bridge of his nose.
mpsleep 2
say Better. Tomorrow we do it in the mud, and you will thank me the day it matters.
~
Attach that to a drill sergeant, stand any mob answering to the name recruit in the room, and every arriving player catches thirty seconds of theater. To the audience it is two characters; on the page it is one column of text, which is precisely the conductor's advantage: the whole scene lives in one place, its timing is one set of sleeps, and there is no way for the halves to fall out of step, because there are no halves. If the recruit is missing, dead, or wandered off, the mpforce lines quietly do nothing and the sergeant carries the scene alone, barking at an empty parade ground, which is not even wrong for the character. When you write a duet, write a conductor unless you have a reason not to.
The reason not to arrives when the second NPC must REACT rather than be puppeted: it stands in another builder's area, it has its own script and personality, or the cue might come from anywhere. For that there is design two: THE LISTENER, built on REGMASK_PROG, the strangest trigger in the engine and the one tool that hears NPCs. It fires when ANY line of text the scripted object SEES, from anyone or anything, matches the header, which is a pattern rather than a keyword list. Where speech blocks listen to players' spoken words, a mask trigger reads everything printed in front of the object: other NPCs' says, emotes, narration, combat spam, all of it.
REGMASK_PROG pieces of eight
say Rawk! Eight for the captain, none for the crew! Rawk!
~
Attach that to a tavern parrot and ANY voice in the room that utters pieces of eight, player, pirate NPC mid-script, or a narrated mpecho from the room itself, gets the squawk. Now a conductor NPC can end its scene with a line containing an agreed cue phrase, and the scripted parrot across the room answers on its own initiative: two separate scripts, cooperating through nothing but a heard sentence. That is NPCs cueing NPCs.
Mask triggers come with three sharp edges, and you must respect all three:
First, the header is a pattern, matched letter for letter including capitals, against the raw printed line. Keep cue phrases lowercase words that appear mid-sentence, as spoken text passes through in exactly the case the speaker typed. The pattern can use regular expression tricks, the bar character for alternatives is the useful one, laughs|giggles matches either word, but a plain phrase is a perfectly good pattern and all a duet needs.
Second, in a mask block the cast is NOT what you expect: the trigger has no idea who produced the text, so $n is the listener itself, not the speaker. The heard line rides in $g. Write mask bodies about the listener and the line, never about a speaker you cannot name.
Third, and this is the one to tattoo somewhere: A MASK CAN HEAR ITS OWN VOICE. If the parrot's reply contained the words pieces of eight, the reply would appear in front of the parrot, match the pattern, fire the trigger, produce the reply, which would match the pattern, forever, a mob heckling itself at machine speed until someone clears the script. The engine's runaway guards stop a single script run, but each squawk is a fresh run, so the volley just continues. Two rules keep you safe: a mask body must NEVER contain text its own header matches, and cue chains between NPCs must run one direction only. A cues B is a scene; A cues B cues A is a perpetual motion machine, and you have built a room nobody can stand in. When a cued reply must mention the cue subject, use mpechoaround $i for it, which shows the line to everyone EXCEPT the scripted object itself, so its own ears stay clean.
Which design when, in one breath: conductor for a scene one author controls end to end, listener for a reaction that must survive the speaker being anyone. The best set pieces use both, a conductor running the scene and one listener elsewhere waiting on the final cue.
Worked Scene: A Branching Interrogation
Time to spend everything at once. The scene: a captured smuggler, tied to a chair, and a player free to question him. The design goal: he lies, he cracks under pressure, and what he tells you depends on what you have already dragged out of him. This is a keyword tree, a counter, gates, a switch, and a confirm question, all in one character.
Before the script, the design, because set pieces are designed on paper first. Three topics: the cargo, the ship, the boss. One pressure counter, weasel_heat, stored per player, driven up by threats. The ship is free information, he gives it to seem cooperative. The cargo has a lie at heat zero and the truth at heat one or more. The boss is the prize, gated behind heat two, and even then he bargains: swear an oath or get nothing. Every reply advertises the words that continue the scene. Now the script:
GREET_PROG 100
mpecho A lamp swings from a low beam, and a rope-burned smuggler slouches in the chair beneath it.
say Ask your questions, then. The cargo, the ship, the boss, I have heard them all before.
~
SPEECH_PROG cargo crates hold
if var($n weasel_heat >= 1)
say Fine. Fine! Salt on the manifest, silver under the salt. Happy?
mpsavevar $n weasel_cargo yes
else
say Salted fish. Barrels of it. Terribly exciting work, smuggling.
endif
~
SPEECH_PROG ship tide gullwing
say The Gullwing, out of Brine Hollow. That much is chalked on the harbor board, so I give it away free.
~
SPEECH_PROG gallows rope hang noose
if var($n weasel_heat == '')
mpsavevar $n weasel_heat 0
endif
mpargset 1 $%MATH($<$n weasel_heat> + 1)%
mpsavevar $n weasel_heat $1
switch $1
case 1
emote laughs, but the laugh dies early.
say The gallows. Yes. I have seen them. Shorter drop than you would think.
break
case 2
emote pulls at the ropes, sweat shining at his temples.
say All right. All right! Ask about the cargo again and I will tell it true this time.
break
default
say You have made your point twice over. Ask your questions.
endswitch
~
SPEECH_PROG boss name who
if var($n weasel_heat >= 2)
mpsetvar $n confirm_answer
say The name is worth my neck. Swear you will cut me loose after, and I talk.
mpconfirm Do you swear it? Type yes or no.
mpsleep 6
if var($n confirm_answer == yes)
emote sags in the chair, all the fight gone out of him.
say Varric. Varric of the counting house, with the kind smile and the clean hands. The silver is his, and so am I.
else
say No oath, no name. Hang me if you like. Varric does worse.
endif
else
say Some questions are priced in blood, and yours is not on the table yet.
endif
~
Play it in your head as a player would. You walk in; the room itself sets the scene, an mpecho, then the prisoner names the three topics, the advertising law at work. Ask about the ship: free truth, and note the craft in it, a liar volunteers something checkable to buy credit. Ask about the cargo: salted fish, the lie, because your heat is zero. Mention the gallows: the counter climbs to one, and the switch, which reads the fresh count from slot 1 where the increment idiom parked it, picks the case for one, bravado with a crack in it. Threaten again: case two, and he TELLS you the cargo lie is ready to fall, ask about the cargo again. This is the trick that makes pressure feel real: the script re-advertises an old keyword when its answer has changed, and the player experiences going back over his story. Ask about the cargo: silver under the salt. Ask about the boss: heat is two, the gate opens onto a bargain, and the confirm question runs the full pattern from earlier in the chapter, clear, ask, sleep, judge. Swear the oath and you get a name and a character portrait in one line. Refuse, and the refusal characterizes Varric better than the confession does, no oath, no name, Varric does worse. Notice the third switch case, too: threaten a third time and he flattens, the script refusing to reward button mashing, which quietly teaches the player that the scene has structure.
Design notes worth stealing. The heat counter lives on the PLAYER, so each interrogator cracks him separately, and your progress survives his respawn; store it on $i instead and the whole town shares one slowly breaking prisoner, which is a different, also interesting, scene. The keyword lists are deliberately generous, who catches who is the boss and who runs the silver, but generosity has a price: the word who appears in many sentences, and a player asking who chalked the harbor board fires the boss block too. In a one-room set piece that is harmless; in a busy tavern, listen for narrower words. And the weasel_cargo yes note that the truth branch files is a hook this scene never uses, deliberately: a magistrate NPC two rooms away can gate HER dialogue on it, if var($n weasel_cargo == yes), and suddenly your interrogation feeds a quest chain. Notes on players are how NPCs gossip about you behind your back.
Variations to try. Let evidence substitute for threats: a GIVE_PROG that recognizes a ledger item, isname($o ledger), and jumps heat straight to two, so the thorough player skips the ugly part. Pace his crack with an mpsleep and a held silence before Varric's name. Or give him a REGMASK_PROG listening for varric spoken by ANYONE in the room, so the moment the name first crosses anyone's lips, he flinches, a listener-design touch that sells the fear better than any speech.
Worked Character: A Complete Romance Subplot
The chapter closer. A romance is every tool in this chapter aimed at one heart: an affection counter with tiers, keyword flirting with a once-per-visit limiter, gifts weighed through GIVE_PROG, and a gated, confirmed proposal with permanent consequences. Meet Elara, who sells lilies on the harbor road.
The design first, on paper. One counter, elara_aff, on the player. Four greeting tiers: stranger, remembered, friend at three, sweetheart at six. Two ways to raise it: a compliment, worth one but only once per visit, and the gift of a warm meal, worth two, any time. One milestone: at six or better, the word marry opens a proposal, run as a confirm question; yes sets a permanent flag that rewrites her greeting forever, no costs two affection, because words like that are not for juggling. Every branch, including every refusal, is written in her voice.
GREET_PROG 100
emote looks up from a cart of harbor lilies.
mpsetvar $n elara_flirted
if var($n elara_wed == yes)
say There you are. The whole day sits easier now.
else
if var($n elara_aff >= 6)
say I saved the best of the morning lilies back, in case it was you. It is always you, lately.
else
if var($n elara_aff >= 3)
say Well met again, $N. The cart feels less heavy when the company is good.
else
if var($n elara_aff >= 1)
say Oh, I remember you. Lilies, was it? Or only the smell of the sea?
else
say Lilies, fresh cut. A copper a stem, and a kind word comes free.
endif
endif
endif
endif
~
SPEECH_PROG lovely beautiful lilies pretty
if var($n elara_aff == '')
mpsavevar $n elara_aff 0
endif
if var($n elara_flirted == '')
mpsetvar $n elara_flirted yes
mpargset 1 $%MATH($<$n elara_aff> + 1)%
mpsavevar $n elara_aff $1
emote tucks a loose strand of hair back, not quite hiding the smile.
say Flatterer. Take a lily for that, and mind the thorns are only on the roses.
else
say You have said so once today already, and I am still blushing from the first.
endif
~
GIVE_PROG 100
if isname($o meal)
if var($n elara_aff == '')
mpsavevar $n elara_aff 0
endif
mpargset 1 $%MATH($<$n elara_aff> + 2)%
mpsavevar $n elara_aff $1
emote lifts the cover and breathes in like the harbor wind just turned kind.
say You cooked for me? Sellers eat standing up and cold, usually. Not today, it seems.
else
say Keep it, truly. The thought arrived, and the thought is the gift.
mpput $o $n
endif
~
SPEECH_PROG marry wed
if var($n elara_wed == yes)
say We did, and I would say yes again before you finished asking.
else
if var($n elara_aff >= 6)
mpsetvar $n confirm_answer
say Careful, now. Words like that are not for juggling. Ask me plainly, then: is it truly marriage you mean?
mpconfirm Do you mean it, truly? Type yes or no.
mpsleep 6
if var($n confirm_answer == yes)
mpsavevar $n elara_wed yes
emote sets down the shears, takes your hands, lilies and all.
say Then yes. Yes, twice over. The harbor can gossip itself hoarse.
mpecho Somewhere down the quay, a fiddle strikes up as if it had been waiting.
else
mpargset 1 $%MATH($<$n elara_aff> - 2)%
mpsavevar $n elara_aff $1
say Then do not say the word again until you do. Lilies wilt from less.
endif
else
say You hardly know which flowers I hate, and marriage is a long time to guess.
endif
endif
~
Block by block, because this is the script to truly understand.
The greeting opens with an unconditional emote, the look up from the cart, which plays for everyone from stranger to spouse; consistent physical business is what makes a character feel continuous across branches. Then the housekeeping line: mpsetvar $n elara_flirted with no value ERASES the flirted flag, and this placement is the whole once-per-visit mechanism, the visit begins, the compliment becomes available again. Then the tiers, and read the shape carefully: the married flag is checked before anything else, because marriage outranks arithmetic, and then the thresholds descend, six, three, one, stranger, each deeper else a cooler acquaintance. Highest first, always; reverse the order and, as the counter section warned, the coldest tier swallows every warm one.
The compliment block is the counter idiom wrapped in the limiter. Seed if empty; then the flag check: only if elara_flirted is still clear does the count rise, the flag get set, and the blush play. The else is the limiter speaking IN CHARACTER, still blushing from the first, which turns a mechanical cap into charm. Without this flag, a player types lovely five times and speed-runs her heart, and nothing kills a romance subplot deader than discovering it is a vending machine after all.
The gift block is GIVE_PROG doing what it always does: the item is in her hands by the time the trigger fires, $o names it, and isname asks what it is. A warm meal, and the counter takes two, double a compliment, because doing outweighs saying and your systems should say so. Anything else and she declines, and here is the give-back pattern every polite GIVE script needs: mpput $o $n returns the item to the giver, because a refusal that keeps the goods is theft with a pretty line attached. Note the refusal itself, the thought arrived, and the thought is the gift; declined branches are where characters are made.
The proposal block runs the gates in order of dignity. Already married: the sweetest line in the script, and no machinery at all. Affection under six: a rebuff that is also a map, you hardly know which flowers I hate, telling the player exactly what kind of effort is missing. And at six or more, the full confirm pattern one last time, clear the note, ask in her voice, let the engine add its yes-or-no reminder, sleep six seconds, judge. Yes writes elara_wed, the permanent note that rewrites her greeting forever, and the scene ends on narration, the fiddle down the quay, because the biggest moments belong to the room, not the speakers. No, and silence judged as no along with it, costs two affection and closes with the best refusal in the chapter. Ask idly and the sweetheart tier may cool to friend; words have prices here.
Extensions, if Elara has caught your builder's heart. A RAND_PROG at a low percent that only fires for the wedded, gated with if var, pining gently at her cart, gives the marriage idle texture. A jealousy note is one counter on the mob, shared, if two players are courting her at once, though think hard before scripting heartbreak. Her wedding could file notes other NPCs read, the innkeeper toasting you, the harbor gossip from the first section acquiring a new topic; notes on players are how one love story leaks into a whole town. And a proper wedding scene is a conductor script away: she cues, the fiddler NPC answers on a mask, and the drill from the duet section becomes a procession.
Common Mistakes In Dialogue Scripting
The ways this chapter goes wrong in the field, each with its symptom and its fix, so you can diagnose in a minute what took someone else an evening.
1. The unadvertised keyword. Symptom: the tree is perfect and nobody ever walks it. Players cannot say words they were never given. Fix: every reply that expects a follow-up names the word that continues it, and the greeting names the roots.
2. The stale answer. Symptom: an NPC seems to know the answer before the player types, or reacts to last week's answer. Cause: prompt_answer and confirm_answer are permanent player notes, and something forgot to clear before asking. Fix: a bare mpsetvar of the note directly before every prompt line, no exceptions.
3. Reading an answer inside an mpalarm line. Symptom: the judged answer is always empty no matter what was typed. Cause: alarm text substitutes its dollar codes when the alarm is SET, before the player answered. Fix: the alarm carries only mpcallfunc and a routine name; the routine does the reading.
4. Overlapping keyword lists. Symptom: one innocent sentence and the mob delivers three answers in a row. Cause: speech blocks all match the same line, including by substring, mill inside miller. Fix: disjoint, distinctive keywords, or merge the blocks and branch inside with if.
5. Tiers tested lowest first. Symptom: a maxed-out counter still gets the stranger speech. Cause: a count of six also passes the one-or-more test, and the first passing branch wins. Fix: highest threshold first, descend through the elses.
6. The unseeded counter. Symptom: a counter that stays at zero forever. Cause: arithmetic on a never-written note yields nothing useful. Fix: the seed lines, if empty file zero, before every increment.
7. The sleep in the loop. Symptom: a five-toll bell tolls once. Cause: an mpsleep inside for or while finishes the current pass and abandons the rest of the loop. Fix: write the beats out flat with sleeps between them, or use an mpalarm ladder.
8. The self-hearing mask. Symptom: a mob repeating one line at machine speed until someone clears its script. Cause: a REGMASK_PROG body that prints text its own header matches, or two mobs cueing each other in a circle. Fix: reply text never contains the cue, cue chains run one direction, and room-facing mask replies go out through mpechoaround $i.
9. Memory on the wrong owner. Symptom: the NPC that swore to remember you forgets everything overnight. Cause: the note was stored on $i, and the mob died or the area reset. Fix: memory between meetings lives on the player, $n, always.
10. Testing keyword blocks with the test command. Symptom: mudprog test SPEECH_PROG fires the block headed all but never the keyword ones. Cause: the test event's message is the single word test. Fix: walk up and say the keyword out loud; the block was fine all along.
11. The out-of-character gate. Symptom: players hit a locked branch and report the NPC as broken. Cause: the refusal says nothing, or says something no character would say. Fix: write every locked door in voice, the way Elara maps the path to her own heart while refusing it.
Exercises
Three commissions, in rising order. Build each before reading its solution; the solutions are complete scripts, but the learning is in the reaching.
Exercise one: the grateful beggar. A beggar who asks arrivals for something warm to eat. Hand him a meal and he never forgets you: from then on he greets you as a friend and tells the alley about you. Hand him anything else and he declines it politely, and gives it back. You need GIVE_PROG, isname, a permanent note, the give-back pattern, and a greeting that branches on the note.
A solution:
GIVE_PROG 100
emote cradles the offering in both cracked hands.
if isname($o meal)
mpsavevar $n beggar_fed yes
say A whole meal. You will not remember this by winter, but I will.
else
say Bless you for trying, but I cannot use this. Food is the thing.
mpput $o $n
endif
~
GREET_PROG 100
if var($n beggar_fed == yes)
emote straightens up from the wall, face brightening.
say The kind one returns! I told the whole alley about you, you know.
else
say Spare a meal for a hungry man, friend? Anything warm.
endif
~
The unconditional emote before the verdict is the beat of theater that sells the weighing; the note on the player is what makes you will not remember this by winter, but I will literally true, since he, the mob, will indeed die and respawn while the note sails on in your save file. The give-back line keeps him a beggar rather than a mugger.
Exercise two: the watchman's one story. A wall watchman with a short, paced story he tells each player exactly once, ever. The second visit gets a single warm line instead. You need a once-per-player gate, an mpsleep cutscene inside the gated branch, and the discipline to open with a line that plays for both branches.
A solution:
GREET_PROG 100
emote glances up from the coals of a watch brazier.
if var($n watch_story == yes)
say You know my one story already, friend. Warm yourself and stand a while.
else
mpsavevar $n watch_story yes
say Stand by the fire. I will tell you the thing I tell everyone once, and never twice.
mpsleep 3
say Twenty years I have walked this wall, and in twenty years the horn has sounded once.
mpsleep 3
emote holds up one finger, letting the number hang in the smoke.
mpsleep 2
say Once was enough. Now I watch so it stays at once. That is the whole story, and the whole job.
endif
~
Two choices to notice. The note is filed BEFORE the story plays, not after, so a player who walks out mid-story does not get a fresh performance later, once means once. And the story is three beats and eight seconds, a story a player is glad to have caught rather than one they waited out.
Exercise three: the beast sorter. A mystic who, on request, asks one typed question, when trouble comes, do you meet it with teeth, with wings, or with stillness, files a beast on you according to your answer, and pronounces it. You need the full prompt machinery: an advertised opening, a clear before asking, mpchoose since the question is a menu, an alarm calling a routine, and a switch in the routine judging the answer, with a default for silence and nonsense.
A solution:
GREET_PROG 100
say They call me the sorter, $N. Say sort me, and I will tell you your beast.
~
SPEECH_PROG p sort me
mpsetvar $n prompt_answer
say One question, then. Answer with a single word.
mpchoose When trouble comes, do you meet it with teeth, with wings, or with stillness? Type teeth, wings, or stillness.
mpalarm 10 mpcallfunc sortbeast
~
FUNCTION_PROG sortbeast
switch $<$n prompt_answer>
case teeth
mpsavevar $n sorter_beast wolf
break
case wings
mpsavevar $n sorter_beast hawk
break
case stillness
mpsavevar $n sorter_beast heron
break
default
mpsavevar $n sorter_beast mule
endswitch
say The beast in you is the $<$n sorter_beast>. Wear it well.
mpsetvar $n prompt_answer
~
The phrase header, p sort me, keeps the mystic from sorting people who merely said the word sort in passing. The switch reads the answer through the angle form, matches it case-insensitively, and the default files mule for the silent and the flippant alike, a judgment with a sense of humor, which is the cheapest way to make even the failure branch worth finding. The routine ends by clearing the answer, leaving the machinery clean for the next seeker. And because sorter_beast is a permanent note, any OTHER script in your area can now read it: a door that opens only for herons is one if line away, and suddenly a personality quiz is a puzzle mechanic.
Where To Go Next
You now hold the whole toolkit of the talking character: trees that advertise themselves, gates and counters that make talk consequential, questions that catch their answers, sleeps and alarms that give words timing, masks and puppets that let scenes span more than one throat, and notes on players that let a mortal NPC keep immortal promises.
Where each thread continues: mudprog-triggers is the full catalog of speech, mask, and greeting triggers with every header form; mudprog-commands covers mpforce, mpalarm, and the rest of the verbs your scenes will want; mudprog-flow goes deeper on switch, nesting, and the loops this chapter only warned you about; mudprog-variables is the formal treatment of notes, slots, and substitution; and the cookbook and workbook chapters put these same tools to work on sphinxes, tolls, and night criers. The short reference card for everything is help mudprog.
Now go make someone's favorite NPC. It will not be the one with the biggest sword; it will be the one that remembers their name.
This chapter is the idiom library: thirty-two small, named, reusable patterns that every scripter on this mud ends up copying forever, plus three exercises to make them yours. A pattern is smaller than a recipe. The cookbook chapter builds whole characters; this chapter builds the bricks those characters are made of. Each pattern gets a name, a story about the problem it solves, a complete working script you can attach exactly as printed, a walkthrough of every moving part, the mistake people make with it, and variations to push it further.
Nothing here assumes you can code. If you have read the basics chapter, you know everything this chapter needs: what a PROG block is, that a trigger line starts it and a tilde ends it, that say and emote are just the mob typing, and that mudprog <target> edit attaches a script. If any of that sounds new, read help mudprog-basics first and come back; this chapter will still be here.
Why Patterns Have Names
Builders talk to each other. When a senior builder says put a one-shot flag on that, or that needs a cooldown gate, they are naming a shape of script the way a carpenter names a joint. Learning the names does three things for you. It lets you recognize the shape inside someone else's script at a glance. It lets you ask better questions. And it stops you reinventing, badly, something that was solved years ago in four lines.
Every pattern below is built from the same handful of engine facts, so here they are once, briefly, before we start. A number in a trigger header is a percent chance, and the word all or a blank means always. The variable commands are the heart of almost everything: mpsetvar <who> <name> <value> stores a note on any object, the var function reads it back in an if line, the angle form $<who name> splices it into text, and mpsetvar with no value erases the note. A note that was never set reads back as empty, which counts as no in a condition, so if !var($i sprung) is true exactly until somebody sets sprung. Notes on a mob or item last as long as that copy of the object; notes on a player are saved with the character and last forever. Dollar codes fill in live values: $n and $N the person who set the trigger off, $i the scripted object itself, $o the item involved, $g the text that rode along with the event, $b the last thing the script loaded. The form $%func()% pastes a function's answer into a line of text. And everything fails soft: a bad argument does nothing, a runaway loop is stopped by the engine, and the worst a broken pattern can do is stay quiet.
The example scripts use the stock items /obj/meal, /obj/torch, /obj/armor and /obj/container, which exist on this mud, so every script works exactly as printed. Where a header reads RAND_PROG 100 or FIGHT_PROG 100, that is so the pattern fires instantly while you test it; the walkthrough tells you the sane live number. Indentation is for your eyes only; the engine trims it.
One habit to carry through the whole chapter: variable names are a shared space. Every script on an object reads and writes the same pocket of notes, and notes on a player are shared by every script that player ever meets. Prefix your names with something distinctive, the way these examples use pat_ on player notes, and you will never stampede someone else's quest flag.
Part One: First Habits
Two tiny shapes appear inside nearly every pattern that follows, so they go first.
Pattern 1: The Return Guard
The problem: a block that should only ever run for the right kind of audience. A doorman who lectures every wandering rat that noses into the room is noise; you want him to check first and bow out silently when the check fails. You could wrap the whole body in an if, but as bodies grow, deep wrapping gets hard to read. The idiom is to turn the question around: test the disqualifier at the top, and if it hits, the return command simply ends the block on the spot.
GREET_PROG 100
if isnpc($n)
return
endif
say You have the look of paying custom. Come in, come in.
emote sweeps an arm toward the good tables.
~
Walk through it. GREET_PROG fires for every arrival, player or mob. The function isnpc($n) answers yes when the arrival is a game-run creature, and in that case return stops the block dead: no speech, no gesture, no fuss. Everything below the guard can now be written as if only players exist, with no extra indentation. That flat shape is the whole point. Compare the alternative, an if ispc($n) wrapping six lines, and then imagine three more conditions each adding a layer. Guards stack flat: one disqualifier per guard, one after another, and the real body stays at the left margin.
The common mistake is putting the guard after some output. If the say came first and the guard second, every rat would still get the greeting, and you would stare at the script wondering why the check does nothing. Guards go first, always, before a single visible line.
Variations. Guard on isfight($i) so a busy mob does not chat mid-swing. Guard on isimmort($n) to keep staff out of tourist theater. Guard on var($i muted) to build the master switch, pattern 31. Any yes-or-no function from the functions chapter can stand in the guard's slot.
Pattern 2: The One-Shot Flag
The problem: something that must happen exactly once in an object's life, no matter how many times the trigger fires. A curtain is unveiled once. A trap springs once. A boss speaks his entrance line once. The idiom is a latch: check a note that starts empty, and the very first run through sets it, closing the door behind itself.
GREET_PROG 100
if !var($i unveiled)
mpsetvar $i unveiled 1
mpecho A dusty curtain slides back for the very first time.
else
mpecho The bare stage stands where the curtain once hung.
endif
~
Walk through it. The note unveiled lives on $i, the scripted object itself. On the first firing it has never been set, so it reads back empty, !var answers yes, and the first branch runs: it writes the note AND performs the once-only moment, in that order. Every firing after that, forever, the note reads 1 and the else branch runs instead. If you want silence rather than a second message, leave the else off entirely; a matched trigger with nothing left to do is perfectly legal.
Set the latch at the TOP of the branch, before any sleeps or long performances, not at the bottom. If the once-only body takes time, a second firing can slip in before a bottom-of-block latch is written, and your one-shot happens twice. Latch first, perform second; the paced beat, pattern 18, is this rule stretched to its limit.
The mistake everyone makes: expecting the latch to survive forever. Notes on a mob or item last as long as that copy of the object. When the area resets and a fresh copy spawns, the fresh copy has an empty pocket and the one-shot happens again. For a once-per-mob-lifetime event, that is exactly right, and it is why bosses reliably do their entrance theater for each new group. For once-EVER-per-player, store the latch on $n instead, which is the next pattern.
Pattern 3: The Guest Book
The problem: a mob that treats newcomers and regulars differently, and keeps treating them differently across days, deaths, and reboots. The idiom is the one-shot flag with the note moved onto the player, where notes are written into the character save and persist indefinitely.
GREET_PROG 100
if !var($n pat_guestbook)
mpsetvar $n pat_guestbook 1
say A new name for the guest book. Sign here, stranger.
else
say The guest book already knows you. In you come.
endif
~
Walk through it. Identical shape to pattern 2, but the note rides on $n, the arriving player. The first time any given player ever walks in, their own copy of pat_guestbook is empty, they get the stranger line, and the note is written into their character. Every visit after that, this year or next, the mob knows them, even though the mob itself has died and respawned a hundred times in between. The memory travels with the player; each fresh copy of the mob just reads it.
Every player carries their own copy of the note, which is the entire difference from pattern 2. One shared latch on $i answers has ANYONE done this; a latch on $n answers has THIS PERSON done this. Choosing the wrong host for a latch is the single most common memory bug in scripting, so make the choice out loud every time you write one.
Variations. Store a word instead of a 1 and you get graded memory: mpsetvar $n pat_guestbook friend after a favor, then a third greeting branch for friends. Combine with the seeded counter, pattern 5, to count visits per player. And because the note persists, clean up after tests on yourself with mpsetvar $n pat_guestbook and no value, which erases it.
Pattern 4: The Spent Flag
The problem: a right that is granted in one place and consumed in another. A blessing that works once. A ticket good for one entry. A toll paid for one crossing. The idiom is a flag with three moments in its life: granted, checked, and, crucially, spent at the moment it is honored.
SPEECH_PROG all
mpsetvar $n pat_blessing 1
say One blessing, stored and waiting. Use it wisely.
~
GREET_PROG 100
if var($n pat_blessing) == 1
mpsetvar $n pat_blessing 0
say Your stored blessing warms you once, then fades.
else
say No blessing waits for you today.
endif
~
Walk through it. The speech block is the grant: speak to the hermit and he writes pat_blessing 1 onto you. The greet block is the check and the spend together: if the flag reads 1, the very first thing the honoring branch does is set it back to 0, and only then delivers the goods. Walk out and back in and you land in the else, because the blessing was spent on the first crossing.
Spend first, deliver second. That order is the pattern. If the delivery came first and the spend at the bottom, anything that interrupted the branch in between, a sleep, a long performance, a second trigger firing, could deliver twice on one grant. Written spend-first, the flag is already gone before the goods appear, and double-dipping is impossible. This is the same order-of-operations discipline as the toll gate in the cookbook, where the clerk takes the five gold BEFORE writing toll_paid.
Variations. Spend down a count instead of a flag for a three-use charm: grant 3, check greater than 0, spend with the math decrement from pattern 5. Or leave the flag spent at 0 rather than erased, so a later script can tell never blessed from blessed-and-used, which the plain !var test cannot distinguish.
Part Two: Counters
Numbers that grow. Everything here rests on one three-line move worth memorizing as a unit.
Pattern 5: The Seeded Counter
The problem: counting things. Visits, insults, attempts, kills. Text notes do not add on their own; you must seed the number once, then read, add, and write back each time. The idiom is exactly those three moves in a row.
GREET_PROG 100
if !var($i visitors)
mpsetvar $i visitors 0
endif
mpsetvar $i visitors $%math($<$i visitors> + 1)%
say This door has swung open $<$i visitors> times on my watch.
~
Walk through it. The if is the seed: the first time anyone arrives, the note visitors has never been set and reads as empty text, so the script files a 0 to start from. Seeding matters because empty text plus one is not arithmetic; skip the seed and the count silently starts life wrong. The mpsetvar line is the whole engine of the pattern: $<$i visitors> reads the current count as text, the math function adds one to it, the $%...% wrapping pastes the answer back into the line, and mpsetvar files the new total. The say line then splices the total straight into speech with the same angle form.
Where the counter lives decides what it counts. On $i it is a house total, everyone's visits pooled, and it resets when this copy of the mob dies or the area replaces it. On $n it is per player and permanent: swap every $i for $n above and the doorman counts YOUR visits, forever. Both are useful; choose on purpose.
The mistake: writing $i visitors + 1 without the math wrapper and wondering why the note now literally contains the text 1 + 1. The angle form only READS; only math does arithmetic. If a counter ever starts speaking gibberish, view the raw note by having any script echo $<$i visitors> and you will usually find text where a number should be, which points straight at a missing seed or a missing math.
Pattern 6: The Milestone Bell
The problem: doing something special every Nth time. Every third customer gets a free round; every tenth insult starts a fight. The idiom is the seeded counter plus a threshold branch that resets the count when it triggers, so the cycle starts over.
GREET_PROG 100
if !var($i tankards)
mpsetvar $i tankards 0
endif
mpsetvar $i tankards $%math($<$i tankards> + 1)%
if var($i tankards) >= 3
mpsetvar $i tankards 0
mpecho The barkeep rings the brass bell above the bar. A free round!
else
say Another guest! The bell wants $%math(3 - $<$i tankards>)% more.
endif
~
Walk through it. Seed, increment, exactly as in pattern 5. Then the branch: at three or more, ring the bell and, first thing in the branch, put the counter back to 0 so the next cycle needs three fresh guests. Below the threshold, the else does something quietly excellent for atmosphere: it computes how many more are needed, with math doing a subtraction this time, and says it out loud. Countdown lines like that turn an invisible counter into a little public game; players will fetch their friends to ring the bell.
Reset inside the milestone branch, not after the if. Reset outside and every firing zeroes the count, so the bell never rings. And use greater-or-equal rather than exactly-equals for the threshold: if anything ever nudges the counter past three without landing on it, an exactly-equals bell stays silent forever, while at-or-above catches up on the very next guest.
Variations. Keep a second, never-reset counter alongside for a lifetime total. Move both notes to $n for a personal punch-card, buy nine get the tenth free. Or make the milestone rarer as time goes on by raising the threshold each ring: file the threshold itself in a note and compare against $<$i threshold> instead of a written 3.
Pattern 7: The Chalked Crate
The problem: two scripts that need to share a value. Script notes are private to the object they are stored on, and there is no global a script can read back, so the idiom is a dead drop: store the note on some object both scripts can see, and let each read it with the angle form. Any named mob, item, or the room itself can be the crate.
GREET_PROG 100
mpoloadroom /obj/container
mpsetvar container pat_word ember
mpecho A dockhand chalks a single word on the crate: $<container pat_word>.
mpjunk $b
~
Walk through it. The script loads a stock crate onto the floor so the example is self-contained; in real building the crate is whatever fixture already stands in your scene. The mpsetvar line shows the key move: the first word after the command names WHERE the note goes, and a plain name works there just as well as a dollar code, so container finds the crate by name in the room and files pat_word on it. The mpecho line reads it back the same way, $<container pat_word>, by name. Then mpjunk $b tidies the demonstration away, junking the last thing the script loaded.
Now imagine that mpsetvar line in the harbormaster's script and the angle form in the night watchman's script, two mobs who are never awake at the same time. The crate carries the word between them. This is how a lever in one room opens a grate in another, how a killed boss changes a questgiver's greeting, how any two hosts cooperate: they agree on a visible object and a note name, and one writes what the other reads. When the two hosts stand in the SAME room, the simplest crate is often the room itself: attach a script to the room, store on $i there, and every mob in the room can read it by the room's short name or a fixture's name.
The honest limits. Name resolution looks around the script's own room, so the crate must be where the reader stands; cross-room sharing needs the note on the PLAYER who travels between them, which is what the door password, pattern 22, does. And a crate that resets with the area drops its chalk, so anything that must survive belongs on a player or in a real quest flag.
Part Three: Clocks And Cooldowns
Scripts run in an instant, but the world runs in hours and days. These five patterns give scripts a sense of time.
Pattern 8: The Cooldown Gate
The problem: a performance that must not repeat until it has had time to breathe. A juggler who juggles on every request becomes a machine; you want one show, then a rest, enforced. The idiom is a busy flag plus an appointment that clears it.
SPEECH_PROG all
if var($i resting) == 1
say Catch my breath first, friend.
else
mpsetvar $i resting 1
mpalarm 20 mpsetvar self resting
say One grand feat, coming up!
emote juggles three daggers in a glittering wheel.
endif
~
Walk through it. Any speech wakes him. If the resting note reads 1 he begs off in one line, and that cheap branch is important: a gated mob should still ACKNOWLEDGE, or players think the script is broken. On the free path, the very first thing the performance branch does is raise the flag, then it books the clearing: mpalarm 20 schedules one single command to run twenty seconds later, and that command, mpsetvar self resting with no value, erases the note. Then the show. For twenty seconds every request lands on the busy line; then the alarm fires, the flag vanishes, and he is bookable again.
The word self in the alarm line matters more than it looks. An alarm line has its dollar codes filled in at the moment it is BOOKED, so writing $i there would freeze the mob's display name into the command and force a name lookup later, which is fragile. The plain word self is not a dollar code, so it survives to run time and always means the scripted object. Inside alarms, address the host as self, always.
The mistake: raising the flag but forgetting the alarm, which leaves the mob resting until the area resets him. If a gated mob ever seems permanently tired, look for a lost or misspelled clearing line first. While testing, twenty seconds is a long time to stand around; drop the alarm to 3, watch the cycle twice, then set it back.
Variations. Gate a whole room by putting the flag on a fixture, the chalked crate way, so three street performers share one cooldown. Or make the busy line itself vary with the random-line picker, pattern 13, so even refusals have life.
Pattern 9: The Hour Bar
The problem: a punishment or privilege that expires on its own, even if the player logs out, walks away, or the mob respawns meanwhile. An alarm cannot help there: alarms live on the schedule of a loaded mob and need the person present to be found again. The idiom is a stamp: store the CURRENT mud hour on the player, and treat the note as live only while the hour still matches.
GREET_PROG 100
if var($n pat_barred) == $%datetime(hour)%
say Out. The same hour that saw you thrown out still stands.
else
mpsetvar $n pat_barred
say In you come. Mind the furniture this time.
endif
~
SPEECH_PROG all
mpsetvar $n pat_barred $%datetime(hour)%
say That does it. Out, and stay out until the hour turns.
~
Walk through it. The speech block is the offense: it stamps the player's pat_barred note with the mud clock's current hour, a number from 0 to 23 that the datetime function reads. The greet block is the door: it compares the stored stamp against the CURRENT hour. While they match, the ban holds. The moment the mud clock rolls to the next hour, the comparison fails all by itself, no alarm, no cleanup crew, and the else runs: it erases the stale stamp and lets them in. The ban expired simply by the world moving on.
Notice what this survives that an alarm would not. The player can log out for a week; the stamp is saved with them and is long stale on return. The bouncer can die and respawn; the fresh copy reads the same player note. Nothing needs to be loaded or remembered anywhere except on the player, which is the most durable place there is.
The honest edge: a stamp written at the last minute of an hour expires almost immediately, and one written at the first minute lasts nearly the full hour. For tavern justice that unevenness IS the charm. When you need a longer sentence, stamp the day with datetime(day) instead, which is the next pattern wearing a scowl, or store both day and hour in two notes and require both to match.
Pattern 10: The Daily Stamp
The problem: once per day, per player. A daily dole, a daily riddle, a daily discount. The idiom is the hour bar turned kindly: stamp the mud DAY on the player when they claim, and refuse while the stamp still matches today.
SPEECH_PROG all
if var($n pat_alms_day) == $%datetime(day)%
say The alms ledger shows your mark for today. Come back tomorrow.
else
mpsetvar $n pat_alms_day $%datetime(day)%
mpmoney $n 1
say One copper, and your mark goes in the alms ledger until tomorrow.
endif
~
Walk through it. First request of the day lands in the else: the almoner stamps today's day number onto the player, hands over one copper with mpmoney, and says so. Every further request today matches the stamp and gets the ledger line. At the turn of the mud day the comparison quietly fails for everyone at once and the dole reopens. No reset script, no midnight sweep; each player's next visit checks itself.
Order inside the else, one more time: stamp first, then pay. If the payout ever grows into a longer performance, the stamp being already written keeps a quick second request from double-dipping. Grants are always latch-then-give, this whole chapter long.
The honest edge: day numbers restart each mud month, so a player who claims on day 12 and next visits exactly a month later on day 12 gets refused once, wrongly. For flavor handouts nobody will ever notice; for anything valuable, store datetime(month) in a second note and require both to differ. That two-stamp form is also how you build once per SEASON or once per year, from ismonth, isseason, and their datetime cousins in the functions chapter.
Pattern 11: The Appointment
The problem: one thing, a little later. The kettle whistles in a few seconds; the echo dies after the bell; the guard remembers a forgotten detail a beat after you leave. Where mpsleep pauses a whole performance in the middle, the idiom for a single delayed line is mpalarm: book one command, keep going, and let it land on schedule.
GREET_PROG 100
say The kettle needs a moment. Do not wander far.
mpalarm 3 say The kettle is ready. Come and get it.
~
Walk through it. The greeting delivers its first line at once, and the block ends immediately; nothing waits. Three seconds later the booked command runs on the host as if it were a fresh one-line script, and the kettle announcement arrives. The shape is: everything after the number, on that one line, is the whole appointment. One line, one command. Chains of timed beats belong to mpsleep and the paced beat, pattern 18; the appointment is for a single afterthought.
Two rules of the appointment worth engraving. First, dollar codes in the booked line are filled in when the alarm is BOOKED, not when it lands. Write mpalarm 10 say Farewell, $N and the name is frozen in at booking time, which for text is exactly what you want. But a frozen $n used as a TARGET, in an mpechoat or mpsetvar, becomes a mere name to be looked up later, and if that player has left the room by then, the lookup finds no one and the command quietly does nothing. Aim late effects at self, which never goes stale, and treat late effects aimed at players as best-effort flavor. Second, appointments die with their host: a mob that is killed before the alarm lands takes its appointments with it, which is why DEATH_PROG bodies must say everything immediately.
Variations. Book the clearing of a flag, the cooldown gate. Book an mpcallfunc to run a whole named routine later, which is how the sphinx in the cookbook judges answers. Book an mpecho for a sound that outlives a scene by a breath; the last echo of the bell in the cookbook's watchman is exactly this.
Pattern 12: The Midnight Cameo
The problem: theater on a schedule. The city should feel like it keeps time whether or not anyone is watching, and the cheapest way to do that is one scripted moment at a fixed mud hour. The idiom is a TIME_PROG whose header lists the hour, holding a scene rather than a mechanic.
TIME_PROG 0
mpecho Midnight. Somewhere above, the great bell swallows its own echo.
~
Walk through it. The header is not a percent: for TIME_PROG it is a list of mud hours, 0 through 23, and the block fires once as the clock turns to a listed hour. Here that is midnight. The body is one room-wide line with no speaker, which is the right voice for the world doing something on its own. List several hours in one header, TIME_PROG 0 6 20, and the same block fires at each; the hour that fired rides in $g if the scene should vary, and a switch on $g, the state machine's tool, gives dawn and dusk their own lines.
The honest limits. The clock check rides the mob's own heartbeat, so this is a mob trigger, and the mob must actually be loaded, which happens when someone has visited its area. An empty, never-visited district does not toll its bells, and that is fine: theater is for audiences. For a scene that must ALSO reach the neighbors, pair the mpecho with mpasound, which pushes a line into every adjacent room, the town crier's tool from pattern 29.
Variations. A midnight cameo plus an hour-stamped note is a curfew: the cameo writes a note on a fixture, the chalked crate way, and door scripts read it to turn surly after dark; isnight() often does the same job with less machinery. And a cameo that fires mpcallfunc can kick off any routine in this chapter on a schedule, patrols included.
Part Four: Dice And Variety
Repetition kills atmosphere. Three patterns for making the same trigger produce different moments.
Pattern 13: The Random-Line Picker
The problem: an idle mob with one idle line becomes wallpaper in five minutes. You want a small repertoire, one picked at random each time. The idiom is a switch on a random number.
RAND_PROG 100
switch $%randnum(3)%
case 1
emote leans on his spade and squints at the weather.
case 2
emote taps the spade twice against a headstone, out of habit.
case 3
emote wipes the spade's blade with an old rag, unhurried.
endswitch
~
Walk through it. The randnum function rolls a die: randnum(3) answers 1, 2, or 3, each equally likely, and the $%...% wrapping pastes the roll where the switch can read it. The switch compares the roll against each case and runs exactly the one that matches. Add a fourth line by adding case 4 AND raising the die to randnum(4); the die and the case count must move together, and a die larger than the case list makes silent turns, rolls that match nothing, which reads as the mob simply not acting that beat. Sometimes you even want that: a die of 6 over three cases acts half the time.
The header is 100 here so you can watch it fire while testing. Live, an idle repertoire wants RAND_PROG 8 or so, which with a heartbeat every couple of seconds means an action every half minute or so. Torture-test the repertoire at 100, then dial it down before you walk away; forgetting is the classic way to ship a babbling mob.
The subtle rule: each case is its own little body, and the switch runs at most ONE of them per firing. If two lines should sometimes happen together, they belong in the same case, or in a cascade, which is the next pattern. And keep the repertoire in one voice; a picker that mixes a comic line into a grim set reads as a glitch, not as depth.
Pattern 14: The Chance Cascade
The problem: not all moments are equal. The forge should mostly shimmer, sometimes settle, and once in a while spit a spark players tell each other about. Equal dice cannot do rare-but-precious; the idiom is a ladder of chances tested rarest first.
RAND_PROG 100
if rand(10)
mpecho A white-hot spark leaps from the forge and dies in the dark.
else
if rand(30)
mpecho The forge coals settle with a rusty sigh.
else
mpecho Heat shimmers quietly above the forge.
endif
endif
~
Walk through it. The rand function is a percent roll: rand(10) is yes ten times in a hundred. The cascade asks its rarest question first. One firing in ten gets the spark. The other nine fall to the else, where a fresh rand(30) hands about a third of THOSE the settling coals, and everything left, roughly six firings in ten, lands on the quiet shimmer at the bottom. The bottom line is the default, needing no roll at all, so the cascade always produces exactly one line.
Rarest first is the whole discipline. Test the common thing first and it wins almost every time, starving the branches below; the numbers you wrote stop meaning what they say. When you reorder a cascade, redo the arithmetic: each rung's true share is its roll times whatever chance remains after the rungs above.
Remember the shape rule from the flow chapter: else stands alone on its line, and the next if starts fresh on the line below, each with its own endif. The stair-step indentation above is the honest shape of a cascade; fight the urge to flatten it into an else if, which the engine does not read.
Variations. Put a picker, pattern 13, on the bottom rung, so even the common case has variety. Gate the top rung on isnight() for a forge that only spits sparks after dark. Or spend the rare rung on substance instead of flavor: an mpoloadroom of a real find, once in a great while, makes a room worth loitering in.
Pattern 15: The Sometimes Extra
The problem: a mob whose routine should be steady but not mechanical. The sweeper always sweeps; only sometimes does he look up and share the thought that makes players love him. The idiom is an unconditional base action with a small chance rider.
RAND_PROG 100
emote sweeps the same square of floor he has swept all day.
if rand(25)
say The dust wins every war, you know. I just slow it down.
endif
~
Walk through it. The emote runs on every firing: that is the routine, the thing that makes him part of the furniture. Then rand(25) gives one firing in four the extra: he pauses and speaks. The pairing is what sells it. The routine alone is wallpaper; the line alone, fired at random, feels disconnected; the line ON TOP of the routine reads as a man interrupting his own work, which is character. No else is needed, because doing nothing extra IS the common case.
This pattern is the gentle cousin of the cascade: one rung, riding on a certainty. Use it wherever a mob already has a base behavior, a patrol step, a served drink, a forged nail, and you want occasional seasoning without touching the base. It also stacks kindly with the picker: make the extra a switch on randnum and the sweeper owns three thoughts instead of one, still delivered at the same easy rate.
The number to watch is the product of the two chances. Live, this block would run under RAND_PROG 10 or so, and 10 percent times 25 percent means the line lands about once every couple of minutes. Tune the rider, not the base: the base rate sets how alive the mob is, the rider sets how talkative, and they should be tunable separately, which is exactly what this shape gives you.
Part Five: States And Sequences
Behavior that unfolds over time: machines with positions, bosses with phases, stories with beats, mobs that go places.
Pattern 16: The State Machine
The problem: an object that is always in exactly one of several conditions, and moves between them in a fixed order. A kettle is cold, then heating, then boiling. A ritual is unlit, then chanted, then sealed. The idiom is one note holding the current state, and a switch that, for each state, performs the moment and writes the NEXT state.
SPEECH_PROG all
switch $<$i state>
case heating
mpsetvar $i state boiling
mpecho The kettle climbs from a murmur to a full rolling boil.
case boiling
mpsetvar $i state
mpecho The kettle is lifted off the flame and falls silent.
default
mpsetvar $i state heating
mpecho The kettle is set on the flame and begins to murmur.
endswitch
~
Walk through it. The switch reads the state note off $i with the angle form. On a fresh object the note is empty, no case matches empty, and default runs: default IS the starting state, which is a tidy trick, because it means you never need a separate setup block to seed the machine. Each case does its two jobs in the now-familiar order: write the next state FIRST, then perform the transition out loud. The boiling case writes no value at all, erasing the note, which drops the machine back to its start; the cycle is cold to heating to boiling to cold, one step per speech.
Why a switch and not stacked ifs: with ifs, the first branch would change the state and the SECOND branch would then see the new state and fire too, walking the machine through every state in one firing. A switch runs at most one case per firing, which is exactly the one step you want. When a state machine misbehaves, echo the raw note, mpecho state is $<$i state>, and watch it move; nine bugs in ten are a misspelled state word, because heating and heatng are different states and the machine falls to default, restarting mysteriously.
Variations. Trigger transitions from different events, one block per event, all reading the same note: a GIVE_PROG feeds the fire only in state heating. Report state without changing it from a LOOK_PROG, as the great lever exercise does below. Add states freely; the pattern does not care how many, only that each case names the next.
Pattern 17: The Phase Gate
The problem: HITPRCNT_PROG, the boss-health trigger, fires its block EVERY round the health is at or below the header number. At 20 percent health, both a 50 block and a 25 block match every single round. Left raw, a boss speech becomes a chant. The idiom is a phase note that each threshold advances exactly once, in order.
HITPRCNT_PROG 50
if !var($i phase)
mpsetvar $i phase 1
mpecho Cracks spider across the statue as something inside it wakes.
endif
~
HITPRCNT_PROG 25
if var($i phase) == 1
mpsetvar $i phase 2
mpecho Stone sloughs away. What remains is faster and angrier.
endif
~
Walk through it. The 50 block is a one-shot flag, pattern 2, keyed to never-set: the first qualifying round writes phase 1 and performs the awakening, and every later qualifying round finds the note set and does nothing. The 25 block gates on phase EQUALS 1, not on !var, and that exact-match is the ordering guarantee: even when one huge hit drops the statue from healthy to nearly dead, so that both thresholds qualify in the same round, blocks run in the order written, the 50 block sets phase 1, and the 25 block, running a moment later in that same round, finds its condition true and fires too. The phases happen in sequence, never skipped, never doubled.
The mistake is gating every phase on !var, which lets whichever threshold happens to run first claim the note and lock the others out, or writing the blocks out of order in the script, which works until the day a big hit crosses two thresholds at once. Keep thresholds sorted high to low on the page, gate phase N on phase N minus 1, and the machine is unbreakable.
This is the state machine, pattern 16, wearing armor: the note is the state, the thresholds are the events. Hang real teeth on each phase, an mpcast, a summon through mpmload, a self-buff through mpcondition, exactly as the pit warden does in the cookbook; the gate does not care how heavy the body is.
Pattern 18: The Paced Beat
The problem: a story told in timed beats reads as performance; the same lines in one burst read as a wall. But a performance takes seconds of real time, and the world does not stop: the trigger can fire AGAIN mid-story. The idiom is mpsleep for the pacing plus a busy latch, raised before the first beat and lowered after the last.
SPEECH_PROG all
if var($i telling) == 1
return
endif
mpsetvar $i telling 1
say The bell? Aye, I remember the bell.
mpsleep 2
say It rang once for the flood and once for the fire.
mpsleep 2
say And a third time for nothing at all. That was the worst one.
mpsetvar $i telling
~
Walk through it. The guard, pattern 1, bounces any speech that arrives while telling reads 1; the storyteller does not restart his tale because someone coughed. Past the guard, the latch goes up FIRST, before a single word, so the protection covers the whole performance including its pauses. Then the beats: each mpsleep 2 suspends this block for two seconds while the mob otherwise lives normally, and the remaining lines resume on schedule. After the last line the latch comes down, the empty mpsetvar erasing it, and he can be asked again.
The latch lives on $i, not $n, and that choice is the difference between a cooldown and a theater rule: on $i, ONE telling happens at a time for the whole room, which is right for a performance everyone hears; on $n it would merely stop one player relaunching it while letting a second player interleave a second telling over the first, two stories shouting over each other. Room-audible sequences take their latch on $i, always.
The honest edge: if the mob dies mid-story, the remaining beats die with it, latch and all, which is harmless since a fresh copy starts clean. But a very long performance with the latch on a ROOM or fixture needs a safety: pair the latch with an appointment, mpalarm 30 mpsetvar self telling, booked right after the latch goes up, so even an interrupted story frees the stage within half a minute.
Pattern 19: The Named Routine
The problem: the same six lines wanted from three different doors. The house rules should be recited on arrival, on request, and after an incident, and pasting the speech into three blocks means fixing it in three places forever. The idiom is FUNCTION_PROG, a block that never fires on its own and runs only when called by name with mpcallfunc.
GREET_PROG 100
mpcallfunc rules
~
SPEECH_PROG all
mpcallfunc rules
~
FUNCTION_PROG rules
say House rules, same as ever. No blades, no shouting, no credit.
~
Walk through it. The FUNCTION_PROG header's argument is its NAME, here rules, and the engine files it away without ever firing it for any game event. The two real triggers each contain a single line, the call. When either fires, mpcallfunc rules runs the named block right there, with the same $n and the rest of the moment's details carried through, so the routine can still say $N and mean the right person. Change the speech once, and every door that recites it is current.
This is the pattern that keeps big scripts maintainable. The moment you catch yourself copying three or more lines between blocks, cut them, wrap them in a FUNCTION_PROG, and call it from both places. Routines can call other routines, and a routine can take a note of context first, mpargset 1 something, then read $1 inside, which is how one greeting routine serves two moods.
The honest limits. A routine shares the moment it was called from, so a routine written for triggers that have a $n will shrug when called from one that does not, printing someone where a name should be; keep routines honest about what they assume. And the name match is on the whole argument, so FUNCTION_PROG rules and mpcallfunc rule, singular, never meet; when a routine seems dead, compare the two names letter by letter, the same diagnosis as a misspelled trigger.
Pattern 20: The Patrol Loop
The problem: a guard who walks a beat. The wander system can move a mob at random, but a PATROL is scripted: specific legs, in order, with theater at the turns, repeating forever. The idiom is a leg counter driving a switch, with mpwalkto taking the steps and the last case resetting the counter.
RAND_PROG 100
if !var($i leg)
mpsetvar $i leg 0
endif
mpsetvar $i leg $%math($<$i leg> + 1)%
switch $<$i leg>
case 1
emote sets off on his rounds, boots ringing on the stone.
mpwalkto north
case 2
mpwalkto south
emote returns from his rounds and plants his spear.
mpsetvar $i leg 0
default
mpsetvar $i leg 0
endswitch
~
Walk through it. Seed and increment, the counter from pattern 5, ticking once per firing. The switch turns the count into a leg of the route: leg 1 is the departure, an emote for the room he leaves and then the step north, taken with mpwalkto, which walks him through a real exit exactly as if he typed go north. Leg 2 brings him back and, crucially, resets the counter to 0 so the next firing is leg 1 again: the loop. The default case is the seatbelt: if the counter ever holds a value with no case, from an old note or a route you shortened, it resets instead of counting upward into nothing forever. Every counter-driven switch deserves that default.
The script rides ON the mob, so it travels with him; each firing happens wherever he stands, which is why each leg's steps must depart from where the previous leg left him. Walk the route yourself and write the legs from life. A longer beat is more cases, each one or two mpwalkto steps with theater between; a line like mpwalkto north north east takes three steps in one leg, briskly.
Live, the header wants RAND_PROG 10 or so, making a leisurely leg every half minute; at 100 he marches like a wind-up toy, which is only for testing. And if a fight can catch him mid-beat, add a guard: if isfight($i) return, pattern 1, at the top, so the patrol politely waits for the brawl to finish.
Pattern 21: The Escort Tick
The problem: a guide who leads players somewhere, and WAITS when they lag. A patrol walks its beat regardless; an escort's clock only ticks while someone is with him. The idiom is the patrol loop with one guard at the top: no company, no tick.
RAND_PROG 100
if numpcsroom() < 1
return
endif
if !var($i stage)
mpsetvar $i stage 0
endif
mpsetvar $i stage $%math($<$i stage> + 1)%
switch $<$i stage>
case 1
say Stay close. The next stretch is no place to dawdle.
case 2
emote beckons and slips ahead through the gloom.
mpwalkto east
default
mpsetvar $i stage 0
endswitch
~
Walk through it. The guard is the whole idea: numpcsroom() counts the players standing in the guide's room, and when it is zero the block ends before the counter moves. The journey is frozen exactly where it stands, mid-route, until a player catches up, and then the next firing resumes from the same stage. Everything after the guard is pattern 20: seed, tick, switch on the stage, theater and steps, default reset. Stage 1 warns, stage 2 moves; a real escort route just keeps adding stages, a step or two each, with the reset in the final stage instead of the default.
Notice what the wait does to pacing for free. A slow party gets a guide who visibly waits for them; a fast party still cannot rush him past his tick rate, so the gloom stays ahead of the torches. Tune with the header number: live, RAND_PROG 12 or so makes an unhurried guide, and higher means brisker.
Variations. Escort one PERSON, not just anyone: file the ward's name when the escort begins, mpsetvar $i ward $N in the block that starts the journey, and sharpen the guard to the head count ladder, pattern 27, checking roompc names against $<$i ward> so only the ward's presence advances the trip. Announce a stalled journey with a sometimes extra, pattern 15, in the guard path before the return, so the waiting guide mutters about daylight. And end the route by clearing the stage note and saying so; an escort who finishes and silently starts over reads as haunted, which is only sometimes what you want.
Part Six: Gates And Guards
Patterns that decide who may do what: passwords, tolls, vetoes, and watchful eyes.
Pattern 22: The Door Password
The problem: a way onward that opens for a spoken phrase. The idiom has three parts on the page: a hint that can be found, a listener keyed to the exact phrase that brands the speaker, and a check elsewhere that honors the brand.
LOOK_PROG 100
mpecho Under the arch, worn letters urge travelers to speak the tidekeeper's oath.
~
SPEECH_PROG p the tide keeps faith
mpsetvar $n pat_arch_word 1
mpecho The arch grinds softly, as if satisfied.
~
GREET_PROG 100
if var($n pat_arch_word) == 1
mpecho The arch's shadow parts around you like a drawn curtain.
else
mpecho The arch's shadow lies heavy and unmoving across the way.
endif
~
Walk through it. The look block is the fair-play clause: examine the arch and you learn there IS an oath, sending you off to find it in a book, a ghost's ramble, another area entirely. The listener uses the phrase form of the speech header, the leading p meaning the whole phrase the tide keeps faith must appear in the spoken line, not just any one word; without the p, the header would be a keyword list and the single word tide anywhere in conversation would spring the door, which makes passwords embarrassing. On a match, the brand: a note on the speaker, pattern 3's per-player memory. The greet block, here on the same host for a self-contained example but in real building on the room beyond or a warden inside it, honors the brand.
The brand-then-honor split is what makes passwords portable. The speaker carries the note, so the honoring check can live anywhere the player can walk: a different room, a different mob, a veto block like the toll gate in the cookbook, which enforces with CNCLMSG_PROG ENTER instead of merely narrating. Swap the greet body for that veto and the shadow stops being a metaphor.
Variations. Spend the brand on use, pattern 4, for a password that works once per utterance. Rotate the oath by season with isseason guards on two listener blocks. Or brand with a word instead of a 1, mpsetvar $n pat_arch_word $%datetime(day)%, and honor only today's utterance, the daily stamp wearing a hood.
Pattern 23: The Item Toll
The problem: passage, or favor, priced in goods instead of coin. Hand the ferryman a torch and the tunnels are yours. The idiom hangs on GIVE_PROG and two habits: filter INSIDE the block, and send back everything you did not ask for.
GIVE_PROG all
if isname($o torch)
mpjunk $o
mpsetvar $n pat_lit_way 1
say A light for the tunnels. Fair trade. Pass whenever you like.
else
mpput $o $n
say The way below costs a torch, friend. Nothing else will do.
endif
~
Walk through it. GIVE_PROG fires when someone hands the mob an item, which arrives as $o. The header must be all: this trigger carries no text to match keywords against, so the filtering is the isname($o torch) call inside, asking whether the given object answers to the name torch. On the yes path, the toll is taken in the usual strict order: destroy the payment with mpjunk, brand the payer, pattern 3 again, and only then announce. On the no path, the single most important line of the pattern: mpput $o $n places the refused item straight back in the giver's inventory before the refusal is spoken. Without it, the ferryman silently KEEPS whatever he was handed, and players discover they can lose their sword to a mob that only wanted torches. Every give-taker you ever script gets that return line in its else.
Variations. Price in quantity with hasnum($n torch 3) checked before accepting, taking them one give at a time and counting with pattern 5. Accept several currencies with a cascade of isname tests, each with its own thanks. Pay CHANGE for overpayment in flavor, a sometimes extra compliment. And when the toll should gate a doorway with real force, the brand this block writes is exactly what a toll-gate veto reads; this pattern is the clerk's half of the cookbook's toll gate, priced in goods.
Pattern 24: The Safe Veto
The problem: cancel blocks are the sharpest tool in the box. A CNCLMSG_PROG block does not react to an action; it REPLACES it, cancelling the real thing every time it matches, and it is in scope for the whole room. Written carelessly, one veto stops every open in the room, or traps wandering NPCs in a doorway forever. The idiom is a wrapper of two disciplines: mask the header to your own object's name, and guard the body for bystanders.
LOOK_PROG 100
mpecho Salt crusts the strongbox, and its lid sits fast as a gravestone.
~
CNCLMSG_PROG OPEN strongbox
if isnpc($n)
return
endif
mpechoat $n The lid holds fast. Whatever it keeps, it keeps.
mpecho Salt dust sifts from the strongbox as someone tries the lid.
~
Walk through it. The veto header names the action code, OPEN, and then the mask, strongbox, which is matched against the name of the thing being opened. That mask is the first discipline: with ALL in its place, this strongbox would cancel every open attempted anywhere in its room, including other players' pouches, because cancel blocks watch the whole room, not just their own object. Masked to its own name, it vetoes only itself. The guard is the second discipline: an NPC that tries the lid is bounced with a silent return, pattern 1, so scripted scavengers and pets do not trigger human-facing theater. Then the replacement behavior, split for audiences: a private line for the person whose fingers are on the lid, a public line for the room, pattern 28's move.
Remember the cancel rule from the cookbook, because it governs everything here: whether the veto fires at all is decided by the HEADER, and once the block runs, the action is cancelled, no matter what the ifs inside decide. You cannot allow the open from inside the block. The two honest outs are to imitate the action on the player's behalf or to remove the script so the next attempt sails through, and the cookbook's trapped chest and toll gate walk both, including the ordering trap that makes imitate-then-unload loop forever if reversed.
Variations. Mask other codes the same way: GET for an immovable prop, SELL and BUY for a picky vendor, ENTER and LEAVE for doorways. Add a second guard for staff, isimmort($n), whose return lets builders work unbothered. And keep vetoes on their own object: a veto script that also carries the object's flavor blocks dies with them if you ever unload it to open the way.
Pattern 25: The Floor Warden
The problem: reacting when someone takes, or drops, something in the room, without stopping them. Vetoes forbid; a warden merely NOTICES, which is often better theater and never traps anyone. The idiom rides on the event fan-out: when an item is picked up, GET_PROG fires not only on the item but on the room and on every scripted mob standing there. Put the block on a witness.
GET_PROG 100
if isnpc($n)
return
endif
say Everything on this floor is inventory, friend. Paws off.
~
Walk through it. The shopkeeper carries this block, and it fires whenever anyone picks anything up in his room; $n is the taker and $o the item, had we wanted to name it. The NPC guard keeps him from scolding the shop cat. And that is the whole pattern: one witness, one reaction, nothing prevented. The taking still happens, which is the point; a warden who objects but does not stop you creates exactly the social pressure a shop wants, and if the taking truly must be stopped, that is the safe veto's job, pattern 24, on the item itself.
The same fan-out serves DROP_PROG for litter, PUT_PROG on a container watching what goes into it, and OPEN_PROG on a door whose hinges gossip. Check the triggers chapter for which events fan out to witnesses; the ones that do are the warden's whole hunting ground.
Variations. Name the item in the reaction with $o for a sharper line. Escalate with the seeded counter: three takings in one visit and the warden calls the watch, pattern 6's milestone with teeth. Whitelist the owner by branding staff players, pattern 3, and guarding on the brand. Or invert it into a compliment: a curator who thanks anyone who PUTS something on display.
Part Seven: Audience And Broadcast
Who sees what. The same event can whisper to one person, read the room, or shout across a district.
Pattern 26: The Crowd Reader
The problem: a room with three people in it is a different place than the same room with a mob at the bar, and NPCs who notice that feel alive. The idiom is a branch on numpcsroom(), the head count.
GREET_PROG 100
if numpcsroom() >= 3
say A full room tonight. The good bottles stay under the bar.
else
say A quiet room tonight. The good bottles might just surface.
endif
~
Walk through it. Each arrival triggers a fresh count, and the count includes the person who just walked in. At three or more players the keeper plays it cautious; below that, expansive. One threshold, two moods, and the room suddenly has weather. The counting functions come as a family, and they aim differently: numpcsroom() counts players, nummobsroom() counts creatures, numpcsarea() counts players across the whole area, and each suits a different anxiety. A guard captain might bark orders only when nummobsroom() runs high; a hermit might flee when numpcsarea() says the valley is crowded.
Thresholds want a little hysteresis in longer scripts: if a mob switches moods at exactly three, a party dancing on the doorstep flips him back and forth comically. Give the moods a gap, calm at two or fewer, wary at four or more, shrug in between, and he reads as composed instead of twitchy.
Variations. Stack thresholds as a cascade, pattern 14, biggest first: five or more gets a speech, three or more a remark, else the quiet line. Combine with isnight() so crowds only worry him after dark. Or let the count set stakes elsewhere: a boss whose ONCE_PROG checks numpcsroom() and summons one extra minion per player is this pattern doing combat balance.
Pattern 27: The Head Count Ladder
The problem: not how many, but WHO. The lookout should name names. The functions roompc(0), roompc(1), roompc(2) answer with the name of the first, second, third player in the room, counting from zero, and empty text past the end. One honest limitation shapes the idiom: the number inside roompc must be written as a plain digit in the script, because the engine reads it before dollar codes are filled in, so you cannot feed it a loop counter. Instead of a loop, you write a ladder: a fixed run of numbered checks, each guarded against running past the end.
GREET_PROG 100
mpecho The lookout counts $%numpcsroom()% adventurers from his perch.
if roompc(1) != ""
mpecho His eye lingers on $%roompc(1)%, second through the door.
endif
if roompc(2) != ""
mpecho And on $%roompc(2)%, third and trying not to be noticed.
endif
~
Walk through it. The first line is the crowd reader, splicing the raw count into narration. Then the ladder: each rung asks whether a player exists at that position, the != "" test reading is there a name here at all, and only then names them. With one player present, both rungs skip and only the count line shows; with three, the whole ladder speaks. Rung zero is omitted here only because the count line already covers the first arrival; a full ladder starts at 0.
Three rungs cover nearly every real room, and that is the honest scope of the ladder: it is for a lookout naming a HANDFUL, not a census. Write as many rungs as you care to acknowledge and let the rest be crowd. The same ladder shape serves roommob(0) and onward for creatures, and roomitem for floor clutter, and it is how the escort of pattern 21 checks whether a SPECIFIC ward is present: compare each rung against a stored name, roompc(0) == $<$i ward>, then roompc(1), and act when any rung matches.
The mistake: forgetting the guard and narrating about an empty name, his eye lingers on , which reads as broken. Past-the-end is empty text, and empty text spliced into a sentence is a hole; the guard is not optional politeness, it is the pattern.
Pattern 28: The Split Whisper
The problem: the same moment looks different to its subject than to its witnesses, and playing both angles at once is the cheapest drama in scripting. The idiom is the paired aim of mpechoat, one person only, and mpechoaround, everyone but that person.
GREET_PROG 100
mpechoat $n A folded note finds its way into your hand alone.
mpechoaround $n A beggar brushes past the newcomer, nothing more.
~
Walk through it. The player who walks in reads a private line addressed to you; everyone else in the room reads a public line about the newcomer. Nobody sees both, and the gap between the two sentences is where the story lives: the room saw a beggar, you felt the note. Write the pair as two halves of ONE moment, same instant, two cameras, and resist explaining either side to the other; the room genuinely does not know about the note, and that is the fun.
The aim token after each command names the pivot person, and $n is the pivot nine times in ten. It can be anyone resolvable: aim mpechoat at a name and the whisper finds that person, which is how a mob warns its master across the room.
This pattern is the atom of secrets: pickpocket warnings, cult recognition signs, the tap on the shoulder only you felt. It stacks with everything. A guest book, pattern 3, whose first-time branch whispers privately while the room sees a plain nod turns a greeting into a recruitment. A safe veto, pattern 24, already splits its refusal exactly this way. The only mistake worth naming is spilling the secret into the public line; read each half alone and ask whether it stands without the other.
Pattern 29: The Town Crier
The problem: news that must carry beyond the room. A script's ordinary voice stops at the walls; the crier's does not. The idiom pairs the local line with mpasound, which pushes a sentence into every room adjacent to this one.
SPEECH_PROG all
say So the word is given. Let the street have it.
mpasound A crier's voice rings out: fresh word at the fountain!
~
Walk through it. The say serves the room he stands in; the mpasound line then sounds in each neighboring room, the ones a step away through any exit, without him moving. Write the two in different voices on purpose: the local line is a man speaking, the carried line is what a voice sounds like THROUGH walls, from a street away, so it names its source, a crier's voice, because the neighbors cannot see him. That little redundancy is craft, not waste. For truly mud-wide news there is also mpchannel, which speaks onto a chat channel; use it for events of server-wide weight, sparingly, because channel spam from scripts wears a whole community, not just a room.
Now the warning this pattern exists to deliver: relays LOOP. A scripted listener that repeats what it hears, a CHANNEL_PROG that re-announces channel traffic onto the same channel, a REGMASK mob that reacts to a word by saying that word, two criers in adjacent rooms whose mpasound lines contain each other's trigger phrases, all of these are echo chambers, each output becoming the next input. The engine's step budget will stop the worst of it and write the mob's name in the runaway log, but the room still drowned meanwhile. The rule: a relay's OUTPUT must never match its own or its neighbors' INPUT. Reword what you repeat, keep trigger words out of reaction lines, and never relay onto the channel you listen to.
Variations. A cameo, pattern 12, plus mpasound is a bell heard across a district. A DEATH_PROG mpasound lets a boss's fall echo down the corridors, free dread for the next room. And an alarm plus mpasound, booked in a veto, makes tripping the vault ring in the guardhouse next door.
Pattern 30: The Keyword Menu
The problem: an NPC who can discuss several subjects needs to TEACH players what to ask, and answer each ask cleanly. The idiom is a hub that advertises the topics, plus one keyword block per topic, each answer pointing to the next.
GREET_PROG 100
say Ask me of the tide, the toll, or the tower. Those three I know.
~
SPEECH_PROG tide
say The tide turns twice a day and forgives nothing. Ask of the toll.
~
SPEECH_PROG toll
say The toll is one torch, paid below. Ask of the tower.
~
SPEECH_PROG tower
say The tower light died the night the bell rang third. Ask no more.
~
Walk through it. The hub is the greeting: it names the menu out loud, which is the whole difference between a conversation and a guessing game. Each topic block keys on one word, and each answer ends by advertising the next topic, a breadcrumb chain that walks players through the lore in order without ever forcing it. The chain is optional but it is the difference between three facts and a story.
Two disciplines keep menus clean. First, pick DISTINCTIVE keywords, because matching is by substring: a topic keyed on the word or would fire on every sword and north in earshot. Tide, toll, tower are safe; short common syllables are not. Second, resist the catch-all. A SPEECH_PROG all block added as a default answer fires on EVERY spoken line, including the ones that matched a topic, so the mob answers twice at once. If you must have an I do not know that line, one honest form is a catch-all that checks the known words are absent, if !strin(tide $g) and so on, and stays quiet otherwise; clunky, which is why most menus simply let unmatched speech pass in silence and lean on the hub to teach the vocabulary.
Variations. Gate a topic behind a brand, pattern 3, so the tower is only discussed with friends. Let a topic block hand over an item or start a quest, which turns the menu into a quest hub, the baker's shape in the cookbook. And for long lore, make each answer a paced beat, pattern 18, so asking of the tower buys you a told story, not a paragraph brick.
Part Eight: The Toolbench
Two patterns about operating your scripts: switching them off gracefully, and finding out what they are actually doing.
Pattern 31: The Master Switch
The problem: silencing a noisy mob for a scene, a test, or an event, without clearing his script and losing it. The dependable idiom is a mute note that every noisy block checks first, plus a toggle that flips it. There are commands named mpdisable and mpenable that record per-trigger flags, but the current engine does not consult those flags when firing, so the variable gate is the form that actually holds the door.
RAND_PROG 100
if var($i hushed) == 1
return
endif
emote tunes a lute that refuses to stay tuned.
~
SPEECH_PROG all
if var($i hushed) == 1
mpsetvar $i hushed
say And now the music returns, by popular demand.
else
mpsetvar $i hushed 1
say Very well, silence it is. Wave when you miss me.
endif
~
Walk through it. The idle block opens with the guard, pattern 1: while hushed reads 1, every firing ends before the first note. The speech block is the toggle, and its shape is worth copying exactly: it reads the current state and branches to the OPPOSITE, announcing each flip so there is never doubt which position the switch is in. One spoken word mutes him; another unmutes. In live use you would key the toggle to a phrase only staff would say, SPEECH_PROG p mind the stage, rather than all.
The discipline is that EVERY noisy block gets the guard, the idle chatter, the greeting, the crier lines, all of them, or the mute is a colander. When a muted mob still leaks a line, find the block that fired it and add the missing guard; the leak names its own culprit. Keep the toggle block itself unguarded, obviously, or you can never switch back.
Variations. Mute a whole SCENE by putting the note on a fixture, the chalked crate, and guarding every performer in the room against the same note; one word silences the street. Auto-unmute with an appointment, mpalarm 300 mpsetvar self hushed, booked in the muting branch, so a forgotten switch releases itself. And a mute that players can trigger is a prank waiting to happen; gate the toggle on isimmort($n) if the phrase might be guessed.
Pattern 32: The Breadcrumb Trail
The problem: a script that misbehaves in silence. You cannot step through a script, but you can make it narrate its own progress, and the idiom is three kinds of breadcrumb, from loudest to most permanent.
GREET_PROG 100
mplog greet fired for $N
mpsetvar $i last_greeted $N
mpgset pat_last_greeter $N
say Welcome, welcome. Every guest is entered in the ledger.
~
Walk through it. Three breadcrumbs drop before the visible line. The mplog writes a timestamped line into the file /log/mudprog, invisible in the game, which is where a script narrates freely without disturbing players; read the log after the fact and you have a diary of every firing, with $-codes resolved, which alone answers did it fire and who set it off. The mpsetvar files the last visitor's name on the mob itself, a note you can read back later from anywhere in the room, mpecho last was $<name of the mob last_greeted> in a scratch test, giving you state you can inspect long after the scrollback is gone. The mpgset writes a mud-wide global into the script daemon itself, which survives even the mob's death and a reboot; be aware it is a one-way breadcrumb, since no dollar code reads a global back into a script, so it is a flight recorder for a senior admin to inspect inside the daemon, not working memory.
The loudest breadcrumb is not in the script above because you have already met it everywhere: a temporary mpecho DEBUG reached the else, dropped into whichever branch you suspect, answers in one firing which path ran. The troubleshooting chapter builds a whole method on it. The habits that make breadcrumbs pay: log the trigger name and the source in every crumb, because a diary of yes happened entries dates badly; sweep DEBUG echoes out before players arrive, since nothing breaks immersion like a mob announcing its own internals; and leave the mplog lines in anything complicated, forever, because the script that never misbehaves again has not been written.
Exercises
Patterns become yours the first time you combine two of them without looking. Each exercise below names its ingredients; try it cold on a practice mob first, then read the worked solution.
Exercise one, the counting doorman. Build a doorman who counts every entry in a house total, pattern 5, AND remembers per player whether he has met them, pattern 3, greeting new faces and regulars differently while always announcing the running total. Both memories, two different hosts for two different notes, in one block.
Exercise two, the fortune teller. Build a seer who, when spoken to, deals one of three random fortunes, pattern 13, but only one dealing per quarter minute, pattern 8, with a polite line while the cards rest. Mind the order: the gate goes before the deal.
Exercise three, the great lever. Build a lever with two positions, pattern 16, thrown by speech, whose LOOK description truthfully reports its current position without changing it. Two blocks reading one state note.
Worked Solution: The Counting Doorman
GREET_PROG 100
if !var($i door_count)
mpsetvar $i door_count 0
endif
mpsetvar $i door_count $%math($<$i door_count> + 1)%
if !var($n pat_known_face)
mpsetvar $n pat_known_face 1
say A new face, and guest number $<$i door_count> besides. Twice welcome.
else
say Guest number $<$i door_count>, and a face I know. Welcome back.
endif
~
The counter runs first, unconditionally, because every entry counts whether or not the face is known: seed, increment, exactly pattern 5, on $i where the house total belongs. Then the guest book, pattern 3, on $n where personal memory belongs, latching inside its first-time branch. Both branches splice the live total into speech with the angle form. The one design decision worth noticing is the ORDER: counter before greeting, so the number he speaks includes the guest he is speaking to. Swap them and every guest is told the total as of the previous visitor, which players will eventually notice and never forgive.
Worked Solution: The Fortune Teller
SPEECH_PROG all
if var($i shuffling) == 1
say The cards are still settling. Give them a moment.
return
endif
mpsetvar $i shuffling 1
mpalarm 15 mpsetvar self shuffling
switch $%randnum(3)%
case 1
say The cards show a road and a lantern. Travel, but travel lit.
case 2
say The cards show a coin under water. Wealth, but wet boots first.
case 3
say The cards show a crow facing backward. Old business returns.
endswitch
~
The gate comes first, pattern 8 to the letter: busy check with a polite refusal and a return, then latch, then the appointment that clears the latch, mpalarm 15 mpsetvar self shuffling, with self doing the safe self-address inside an alarm. Only past all that does the deal happen, pattern 13's switch on randnum(3), three fortunes in one voice, each beginning the same way so the table patter stays recognizable. If the deal came before the gate, two quick questions would deal two fortunes and the cooldown would protect nothing; gates guard the door, not the exit.
Worked Solution: The Great Lever
LOOK_PROG 100
if var($i braced) == 1
mpecho The great lever stands locked in its upper notch.
else
mpecho The great lever rests in its lower notch, sticky with tar.
endif
~
SPEECH_PROG all
if var($i braced) == 1
mpsetvar $i braced
mpecho The lever drops with a boom that rattles every window.
else
mpsetvar $i braced 1
mpecho The lever rises notch by notch until it locks overhead.
endif
~
One note, braced, is the whole machine: set means up, absent means down, and with only two states the state machine of pattern 16 relaxes from a switch into a single if per block. The look block is the part worth copying: it READS the state and narrates it without ever writing, which is what makes the lever feel like a real object, inspectable between throws and always truthful. The speech block is the toggle from pattern 31, each branch writing the opposite state first and then performing the transition. Wire the note to consequences elsewhere, a warden who reads $<lever braced> before allowing passage, the chalked crate of pattern 7, and the lever stops being furniture.
The Pattern Habit
Thirty-two shapes, and you will notice how few IDEAS they rest on: a note that latches, a note that counts, a note that stamps the clock, a die, a switch, a guard, a mask, and a sense of who is watching. Everything else in this chapter is those few ideas choosing a host, $i or $n or a crate, and an occasion, a trigger. That is the pattern habit: when a build request lands, hear it as ingredients. Once per player is a guest book. Every third time is a milestone bell. Only while someone is here is an escort tick. Not while resting is a cooldown gate. Say it in pattern names first, and the script mostly writes itself.
When a pattern misbehaves, the diagnosis order is always the same: mudprog <target> to confirm the triggers parsed, a breadcrumb to confirm the block fired, an echoed note to confirm the state, and the troubleshooting chapter when those three disagree. And when you invent a shape this chapter lacks, and you will, give it a name and show a colleague; that is how every pattern here was born.
This chapter is one hundred questions, asked the way builders actually ask them, answered the way a patient friend would answer. It assumes nothing. If you have never scripted, never programmed, never so much as renamed a file, start at question one and read straight down; the questions are arranged so that each section leans only on the ones before it. If you have a specific problem, skim the section titles and jump. Every answer that needs a script shows a complete one, ready to paste onto a practice mob with the mudprog command exactly as printed, tilde and all. Where an answer says "see the such-and-such chapter", that chapter exists under the same help prefix: help mudprog-basics, help mudprog-triggers, and so on.
Starting From Nothing
Question 1: Do I need to know how to program?
No. A MUDProg script is closer to stage directions than to programming. Most of its lines are commands you already type every day as a player: say, emote, smile, give. You write down what the character should do, mark WHEN it should happen with a trigger name at the top, and the engine does the rest. There are no files to edit, nothing to compile, no reboot to wait for. The handful of genuinely new ideas, triggers, dollar codes, the if line, are each explained from zero in the basics chapter, and every single one was designed to be learned by a person whose only tool is plain written English. If you can write a note to a forgetful actor, you can script.
Question 2: What happens if I make a mistake?
One of two things, and neither is dangerous. Either the script does less than you hoped, usually nothing at all, or it does too much, such as a mob repeating an emote every two seconds because a chance number is too high. Both are fixed by editing the script or, in the worst case, wiping it with mudprog <target> clear. A mistake never damages the mob, the room, the player, or the mud. The engine was built on the assumption that builders would experiment freely, and it protects you at every layer; question 3 lists exactly how. So the honest answer is: nothing happens that one clear command cannot undo. Experiment.
Question 3: Can my script crash or lag the game?
No, and it is worth knowing why, so the fear never slows you down. A command given bad arguments quietly does nothing and the script moves on. A word the engine does not recognize is handed to the mob as an ordinary game command, and if the game does not know it either, the mob fails it in private, exactly as you would if you typed gibberish. A script that runs away, say a loop that never ends, is stopped by the engine itself: every trigger run has a budget of a few thousand steps and every loop is capped at two thousand passes, after which the run halts and a note is written to the script_runaway log so you can find and fix it. And a broken script never blocks the event that fired it: the player still enters the room, still gets the item, still lands the blow. Objects with no script cost the game essentially nothing, so scripting also does not slow anything down. See question 97 for the performance details.
Question 4: What is the smallest script that actually does something?
Three lines. A trigger header, one command, and the closing tilde:
GREET_PROG 100
say Welcome to my corner of the world, $N!
~
GREET_PROG means "a player just walked into my room". The 100 means "act every time". The say line is performed by the mob as if it had typed it, with $N replaced by the arriving player's name. The tilde ends the block. Attach it to any mob, walk out of the room and back in, and the mob greets you by name. Every script you will ever write is this shape with more lines in the middle.
Question 5: What does the tilde actually do, and what happens if I forget it?
The tilde is the full stop of MUDProg: a line containing only ~ tells the engine "this block is finished". Forget it between two blocks and the engine cannot know where one ends, so it reads both as ONE block. The symptoms are distinctive: the first trigger performs the lines of both blocks back to back, and the second trigger never fires at all, because its header line was swallowed into the first block's body as just another command. The five second diagnosis is mudprog <target>, which prints a Triggers line listing every trigger the engine found; if one you wrote is missing from that list, hunt for a missing tilde directly above it. Here is the correct two block form:
GREET_PROG 100
say Hello there, traveler!
~
SPEECH_PROG hello
say Hello to you too, $N!
~
Each block sealed with its own tilde, each trigger independent.
Question 6: What does the number after the trigger name mean?
For most triggers it is a percent chance, rolled fresh each time the event happens: 100 means always, 25 means one time in four, and blank or the word all also mean always. The roll is the header's job as gatekeeper; the body only runs if the roll passes. A few triggers give the slot a different meaning: speech style triggers take keywords there instead, HITPRCNT_PROG takes a health threshold, TIME_PROG takes a list of hours. The place the percent matters most is RAND_PROG, which rolls every couple of seconds forever:
RAND_PROG 100
emote paces a slow circuit of the room, restless.
~
At 100, as printed, that fires constantly, which is useful for testing and torture for players. On a live mob use 5 to 10, which produces an idle action every half minute or so. Raise numbers to test, lower them to ship.
Question 7: Do capital letters matter?
Not to the engine. GREET_PROG, greet_prog, and Greet_Prog are the same trigger; MPECHO and mpecho are the same command; keyword matching in speech headers ignores case too. The _PROG suffix is even optional, so GREET alone works. The only place case matters is inside your message text, which is delivered exactly as you typed it, and in the two dollar codes $g and $G, where the lower case one hands you the spoken text lowercased and the capital one preserves it. The convention throughout the guide, worth copying for readability, is trigger names in capitals and commands in lower case.
Question 8: Do I have to indent the lines inside if and for?
No. The engine trims leading spaces from every line before reading it, so indentation is purely for human eyes. But keep the habit anyway: pushing the lines inside an if or a loop in by four spaces makes the shape of the script visible at a glance, and the shape is where most mistakes hide. Every example in this guide indents that way for exactly that reason.
Question 9: Can I write notes to myself inside a script?
Yes. Any line beginning with the # character or the * character is a comment: the engine skips it completely. Future you, and every builder who inherits your work, will be grateful for a line or two explaining what a block is for:
# Bram the innkeeper. Greeting written for the harvest festival.
GREET_PROG 100
* The festival line below should come out after the season turns.
say Harvest luck to you, $N! The good ale is on the left tap.
~
Both comment styles work inside a block or between blocks. Blank lines are also ignored, so you can space blocks apart freely.
Question 10: What kinds of things can carry a script?
Mobs, items, and rooms, and even online players if you have reason. The mechanics are identical everywhere; only the useful triggers differ. Mobs get the social and combat triggers: greeting, speech, fighting, dying, receiving gifts. Items get the handling triggers: being picked up, dropped, worn, opened, eaten. Rooms get arrival triggers and make the best hosts for area wide vetoes, since everything that happens inside them is in scope. The triggers chapter marks every trigger with the kind of object it belongs on. When you attach a script, the target word after mudprog can be a mob or item name in the room, the word here for the room itself, the word self, or a player's name.
Question 11: How do I actually attach a script?
With the mudprog builder command, three ways. For anything longer than a couple of lines, open the editor with mudprog <target> edit, type the script lines exactly as in the examples, and finish with a single period on its own line to save, or @abort to throw the typing away. For quick one liners, mudprog <target> set <lines> replaces the whole script from one command line, with each semicolon standing in for a line break. And mudprog <target> append <lines> adds to the end of what is already there, which is handy for tacking one more block onto a working script. However you do it, the script is live the instant it saves; the engine reparses automatically whenever the text changes, and there is no reload step of any kind.
Question 12: How do I remove a script?
mudprog <target> clear removes it entirely, and the object goes back to costing the game nothing. A script can also remove itself from inside with the mpunloadscript command, which is how a one act character deletes its own strings after the scene plays out. And remember that a script attached in game lives on that one copy of the object: when the mob dies and respawns, or the area resets and replaces it, the fresh copy arrives scriptless. Question 23 covers what to do about that.
Testing And Debugging
Question 13: What is the fastest way to test a script?
mudprog <target> test <TRIGGER> fires the named trigger on the target immediately, no walking in and out, no staged fights. You stand in as the source of the pretend event, so $n and $N are your name, and the event's text rider is the single word test. If no block for that trigger exists on the target, the command says so, which is itself a useful diagnostic, as question 17 shows. Admins additionally have scripttest, which runs raw script lines on yourself without attaching anything, fires triggers on a named target with its fire form, and runs a script body from a file with runfile. The truest test is still the real event, so before you call anything finished, walk in, say the keyword, start the fight, hand over the item.
Question 14: Why does the test command show my own name for both $N and $t?
Because a tested trigger is a pretend event with only one participant: you. The engine fills the source slot and the target slot with the same person, so any code that reads either shows your name. In live play those slots hold whoever actually caused and received the event. Nothing is wrong; it is simply the difference between a rehearsal with one actor reading every part and the real performance.
Question 15: Why does my keyword speech block stay silent when I use the test command?
Because the pretend event's spoken text is the word test, and your header keywords are matched against exactly that. A block headed SPEECH_PROG stew cannot match, since stew does not appear in the word test; that is the header doing its job, not a bug. To exercise a keyword block, say the keyword out loud in the room, or temporarily change the header to all, watch the body run, and change it back. The same applies to the bus triggers with keyword masks: only their ALL headed forms fire under the test command.
Question 16: Why does my speech trigger not fire at all?
The most asked question in scripting, and the causes are a short list. Check them in order. First, a missing tilde above the block: if the Triggers line from mudprog <target> does not show SPEECH_PROG, the header was swallowed into the previous block; see question 5. Second, the keyword really is not in the sentence: matching is by substring, so a header keyword of stew needs those four letters somewhere in the spoken line, and a phrase header like p open sesame needs the whole phrase. Third, the mob cannot hear its own voice: a mob never triggers its own SPEECH_PROG, so testing by forcing the mob itself to say the keyword proves nothing. Have a different person say it. Fourth, a misspelled trigger name parses fine and never fires; compare against SPEECH_PROG letter by letter. Fifth, you are testing with the test command, which cannot match keywords, as question 15 explains. When in doubt, attach this diagnostic block, which repeats anything it hears:
SPEECH_PROG all
say I hear you. You said: $g
~
If that fires when you speak and your keyword block does not, the problem is the keywords. If even that stays silent, the problem is the script never attached or the header never parsed; go back to the Triggers line.
Question 17: My script does nothing at all. How do I find out why?
Climb this ladder, and you will find the rung that breaks. One: view the script with mudprog <target> and confirm it is actually attached to this copy of the object; a respawned mob arrives without the script you gave its predecessor. Two: read the Triggers line and confirm every trigger you wrote appears; a missing one means a missing tilde or a typo in the header. Three: fire the trigger with mudprog <target> test <TRIGGER>; if the command reports no block found, the name you are firing and the name you wrote differ. Four: if the test fires but live play does not, the header argument is filtering the real event out: keywords that do not match, a percent that rolled low, a zapper mask the player fails. Five: if everything above passes and a specific line still does nothing, that line's command has bad arguments and is quietly no-opping, which is the engine's promise; check the spelling of paths and names on that line.
Question 18: Why does my mob greet people twice?
Two innocent causes. First, you may genuinely have two GREET_PROG blocks; when an event fires, EVERY block written for that trigger runs, in the order written, each rolling its own header chance. Builders forget the block they wrote last week and add another. View the script and count. Second, a missing tilde merged your greeting block with the block after it, so one entrance performs both bodies; the Triggers line diagnosis from question 5 settles it. The both-run rule is a feature the rest of the time: it lets you keep a plain greeting in one block and a rare extra flourish in a second block with a low percent.
Question 19: How do I see what the engine thinks my script says?
mudprog <target> with no other words prints the stored script and, above it, a Triggers line listing every trigger the parser found. That list is the engine's honest opinion of your script, and comparing it against what you meant to write is the single most useful debugging habit in MUDProg. Trigger missing: missing tilde or typo. Trigger present but silent: header argument filtering, or the event simply not happening where the object stands.
Question 20: What is the script_runaway log?
The engine's incident book. Every trigger run gets a budget of a few thousand execution steps, and every single loop is capped at two thousand passes. A script that exceeds the budget is halted mid run, the mob carries on unharmed, and one line naming the object and the trigger is appended to the script_runaway log so a builder can find the loop that never ends or the recursion that feeds itself. If a complex script seems to stop partway through consistently, check the log; if your name is in it, look for a while whose condition never becomes false, or a pair of triggers that set each other off.
Question 21: How do I print debugging output while I work?
Two tools. The mplog command writes its line, dollar codes substituted, into the mudprog log file without showing players anything, which makes it the polite way to trace a live script. And nothing stops you dropping a temporary mpecho marker into a body to prove execution reached a spot:
GREET_PROG 100
mplog greet fired, source was $N
mpecho Debug marker one reached.
~
Fire the trigger, see the marker, check the log, then take the marker back out before players meet the mob. Markers between the branches of an if tell you which path ran; a marker after a loop tells you the loop finished.
Question 22: Why do my dollar codes break when I type a script directly into the command line?
Some telnet setups and client scripting layers quietly eat or double the dollar sign before the mud ever sees it, so the script that arrives is not the script you typed. If a dollar code misbehaves when typed inline but the script reads correctly when you view it, suspect your client first. The reliable paths are the mudprog editor, which most clients pass through untouched, and for admins scripttest runfile <path>, which reads the body from a file and bypasses the typing problem entirely.
Question 23: My mob's script vanished. Where did it go?
Nowhere; the mob went. A script attached in game lives on that one copy of the object. When the mob dies and respawns, or the area resets and sweeps it, the replacement is a fresh clone born from its file, and the file knows nothing about your script. This is perfect while iterating, and wrong for finished work. When a script is ready to keep forever, a coder bakes the text into the NPC's file so every copy is born with it; the example file /domains/examples/npc/mudprog_greeter.c shows the pattern, and any senior builder can do it in a minute. Until then, keep a copy of any long script in a file of your own, so a surprise reset costs you nothing but a paste.
Question 24: How do I test a block that only fires five percent of the time?
Raise the number, test, put it back. A percent header is honest under the test command too: firing a RAND_PROG 5 block succeeds five times in a hundred, which makes for a long afternoon. Set the header to 100 while you are proving the body works, then restore the real rate before you walk away. The same trick in reverse applies to keyword headers, which you can temporarily widen to all, and to zapper masks, which you can temporarily delete. Test the body first, then test the gate.
Dollar Codes And Talking
Question 25: What is the difference between $n and $N?
On this engine, nothing: both produce the proper name of whoever set the trigger off, capitalized. The two spellings exist because the language was designed to run scripts written for CoffeeMUD, where the pair once meant slightly different renderings of the same person. Keep a simple habit and the question never matters: use $N when the name appears in speech and $n when a command needs to point at the person, as in mpechoat $n. Both work in both places.
Question 26: How do I print an actual dollar sign?
Double it. The engine reads $$ as one literal dollar sign and moves on without treating what follows as a code:
GREET_PROG 100
say To put $$100 on a sign, type two dollar signs before the number.
~
The room hears "To put $100 on a sign", with a single dollar sign, which is exactly what you wanted. Anywhere a price or a flourish needs the symbol, write it twice.
Question 27: What is the difference between $i and $I?
Both refer to the scripted object itself, the host running the script. $i gives its proper name, the thing you would call it in a sentence; $I gives its short description, the line you see in a room. For a mob named Bram whose short is a weathered innkeeper, $i says Bram and $I says a weathered innkeeper. In command positions, such as mpechoaround $i, either token points at the host object itself; the name versus short distinction only matters when the code is being printed inside text.
Question 28: What exactly does $g hold?
Whatever text rode along with the event, and each trigger loads it differently: for speech it is the spoken line, for CASTING_PROG the skill name, for LEVEL_PROG the new level, for CHANNEL_PROG the channel name plus the message, for CMDFAIL_PROG the exact command the player fumbled. $g hands you the text lowercased, which makes comparisons painless, and $G preserves the original case for display. When a trigger's entry in the triggers chapter says nothing about a text rider, treat $g as empty. A demonstration you can fire right now, since the tester's pretend event carries the word test:
CMDFAIL_PROG 100
say There is no art called $g known here, $N. Perhaps you misspoke.
~
Attach that, type a nonsense command like frobnicate in the room, and the mob quotes your gibberish back at you.
Question 29: How do I mention the room or area in speech?
Three codes read the host's surroundings at the moment the line runs: $d is the room's short name, $D its full description, and $a the area name when the room declares one. Because they are read live, one script works everywhere the mob wanders:
GREET_PROG 100
say Welcome to $d. Rough country, this corner of the $a lands.
~
A wandering guide with that block names each room correctly as it moves. If the room has no area set, $a quietly produces nothing, so phrase around it in areas you do not control.
Question 30: How do I involve a random bystander?
$r names a random PLAYER in the room; $c names a random living thing, player or mob, other than the host. They are rerolled each time the line runs, which is what makes crowd work feel alive:
RAND_PROG 100
emote eyes $r for a long moment, then loses interest.
~
At a sane percent that mob unsettles a different patron every few minutes. Mind the empty room: with no players present $r produces nothing, and the emote reads oddly, so either keep such lines harmless when the name is blank or guard the block with a condition like numpcsroom() from the functions chapter.
Question 31: How do I say he or she or their correctly?
Pronoun codes, filled in from the person's actual gender at the moment the line runs. For the source: $e is he or she, $s is him or her, $m is his or her. The same three letters capitalized give you the target's pronouns, and $j, $h, $k give the host's own. So one line serves every visitor:
GREET_PROG 100
say Here comes $N. I hope $e brought $m coin pouch.
~
For a woman that reads "I hope she brought her coin pouch", for a man "I hope he brought his". Write the sentence once, and the codes agree with whoever walks in. There is also $y, which produces sir or madam for the source, made for deferential service characters.
Question 32: Why did my mob speak the code itself instead of a value?
Because the engine did not recognize the code, and its policy for unknown codes is to leave them visible rather than silently eat your text; a mob saying $z out loud is a signpost pointing at a typo. Check the letter against the table in the variables chapter, and remember the multi character forms have their own punctuation: variables are $<object name> with angle brackets, function substitutions are wrapped in percent signs. If the code is definitely right and still comes out mangled, suspect your client eating dollar signs, question 22.
Question 33: How do I show text to just one person, or to everyone except one person?
The pair mpechoat and mpechoaround. Each takes a person first, almost always $n, then the text. The at form delivers to that person alone; the around form to everyone in the room except them. Together they let one moment read differently from two perspectives:
GREET_PROG 100
mpechoat $n A pickpocket eyes your belt pouch as you pass.
mpechoaround $n A pickpocket drifts toward the newest arrival.
~
The arriving player reads a private warning; the room reads the public version. This pair is how you script secrets, whispered asides, and any scene where knowing and watching are different experiences. Plain mpecho is the third sibling: everyone, no exceptions, no name attached.
Question 34: Can my mob be heard from the next room?
Yes: mpasound prints its text in every room adjacent to the host's room, through each exit, without the host's name attached. It is the sound that carries, not the speaker:
RAND_PROG 100
mpasound A deep bell tolls somewhere close by.
say That bell never brings good news.
~
Players one room away hear the toll; players standing with the mob hear the spoken line as well. Lower the percent for live use, and use the pairing deliberately: distant sound plus local comment is a cheap and wonderful way to make an area feel connected.
Question 35: Can a script listen to or speak on chat channels?
Both. The mpchannel command sends a line onto a named channel. Listening is the CHANNEL_PROG trigger, which is world wide: it fires on every scripted object that defines it, wherever that object stands, whenever any channel traffic flows. $n is the speaker and $g holds the channel name followed by the message, and because header keywords match against that whole line, putting a channel name in the header is how you listen to one channel only:
CHANNEL_PROG
emote cups an ear toward the distant chatter of the realm.
~
A blank header hears everything, which is a lot; a header naming one channel, such as gossip, keeps the reaction on topic. Channel scripts are world wide, so use them sparingly and quietly.
Question 36: Can I put a calculation or a die roll inside a sentence?
Yes, with function substitution: wrap any function from the functions chapter in $% and % and its result lands in the text at that spot:
GREET_PROG 100
say I count $%numitemsroom()% things cluttering this room right now.
~
The count is computed at the moment the line runs. The same form carries randnum for dice, math for arithmetic, level for numbers about people, and every other function in the catalogue; anywhere text can go, a computed value can go. The heavy use of this appears in the variables chapter's counter examples, and question 54 below builds one.
Asking Questions And Making Decisions
Question 37: How do I make one script behave differently for different visitors?
With the if line, which asks a question, runs the lines under it when the answer is yes, and runs the lines under the optional else when the answer is no. Every if must be closed by endif, the way every block is closed by a tilde. The questions are asked with functions, small named tools that inspect the world; ispc asks "is this a player character":
GREET_PROG 100
if ispc($n)
say Flesh and blood, and a paying customer at that.
else
emote sniffs the air and turns away from the creature.
endif
emote consults a battered ledger of names.
~
A player gets the welcome, a wandering mob gets the sniff, and everyone gets the ledger line, because it stands after the endif, outside the question. That placement trick, shared lines after the branch, keeps scripts short.
Question 38: How do I make an NPC react only to mages?
Two ways, and knowing both makes you flexible. The first is a condition in the body, using the class function:
GREET_PROG 100
if class($n) == mage
say Ah, a fellow student of the arcane, $N.
else
say Welcome, traveler, whatever your trade.
endif
emote marks a tally in the visitors book.
~
The second is a zapper mask in the header, which filters WHO may set the trigger off before the body ever runs. A dash word names the attribute, the values after it say who qualifies:
GREET_PROG -class mage
say Only the arcane-touched hear this greeting at all.
~
Use the body form when both kinds of visitor should get SOMETHING, and the header form when non matching visitors should get nothing at all. Masks can also chain: a header of -class mage -level 20 requires both. The full mask vocabulary, class, race, sex, name, deity, level, player, npc, good, evil, is in the triggers chapter.
Question 39: How do I check a player's level?
The level function, compared against a number. House style is to write the comparison inside the parentheses:
GREET_PROG 100
if level($n >= 10)
say You carry yourself like a veteran, $N.
else
say New boots, fresh face. Welcome all the same, $N.
endif
emote sizes up the newcomer with one long look.
~
The symbols are the usual family: >= at least, <= at most, > and < strictly more or less, == equals, != not equals. Writing the comparison after the parentheses, as in level($n) >= 10, also works; question 43 explains why the guide standardizes on the inside form anyway. For a header-level version there is the -level zapper mask, which takes a minimum or a range like 30-40.
Question 40: How do I check race, gender, or faith?
The functions race, sex, and deity, each answering with a word you compare. Race names and class names compare case-insensitively, so human and Human both match:
GREET_PROG 100
if race($n) == human
say Another human. The city is full of your kind lately.
else
say We do not see many of your people this far in, $N.
endif
emote pours two measures of watered wine.
~
sex($n) answers male, female, or neuter; deity($n) answers the name of the god the player follows, or nothing for the faithless. Each also has a zapper mask twin for header filtering: -race, -sex, -deity.
Question 41: How do I ask two questions at once?
Join them with AND when both must be true, or with OR when either suffices:
GREET_PROG 100
if ispc($n) AND level($n > 5)
say A seasoned adventurer walks my floor. Good.
else
say Everyone starts somewhere, $N.
endif
emote chalks a small mark on the doorframe.
~
The joins read left to right, and you can chain more than two. Keep compound questions short enough to say aloud; when a condition grows past three joins, the flow chapter's advice is to split the logic into nested ifs, which read more honestly than one long incantation.
Question 42: How do I say NOT in a condition?
Put an exclamation mark directly in front of the function, which flips its answer:
GREET_PROG 100
if !isfight($i)
say My blade rests, as all blades should.
else
say Not now! My hands are rather full!
endif
~
There are also the connector words NOT, ANDNOT, and ORNOT for joining a negated question onto a chain, so A ANDNOT B reads "A and not B". The exclamation mark form covers nearly everything a builder needs and is easier to read at a glance.
Question 43: Should I write level($n >= 20) or level($n) >= 20?
Today, both work and mean the same thing. The guide standardizes on the first form, comparison inside the parentheses, for two reasons. It is the canonical shape from CoffeeMUD, so scripts you paste from its documentation match your own habits. And it is proof against a classic family of parser accidents in which trailing text after a closing parenthesis gets ignored, turning a comparison into a bare always-true call; this engine handles the outside form correctly, but the inside form never even offers the temptation. One caution either way: write ONE comparison per function call. Something like level($n > 5 < 50) is not a range check; use two calls joined by AND.
Question 44: How do I compare words instead of numbers, and what about many possible answers?
Word comparisons use the same == and != and ignore case entirely:
GREET_PROG 100
mpsetvar $i password moonlight
if var($i password == MOONLIGHT)
say Case never matters when words are compared.
endif
~
When one value has MANY interesting answers, chain of ifs gets clumsy, and the switch block says it cleanly: name the value once, list a case for each answer, close with endswitch. The optional default catches everything unlisted:
GREET_PROG 100
mpsetvar $i order cider
switch $<$i order>
case ale
say One ale then, plain and honest.
break
case cider
say Cider it is, cool from the cellar.
break
default
say We only have rainwater today.
endswitch
~
The switch line works out its value once, here reading the stored order, and runs the first case that matches, case-insensitively. The flow chapter walks through switch at length.
Question 45: How do I check whether a player is carrying something?
The has function, given the person and the item name as the game knows it:
GREET_PROG 100
if has($n torch)
say I see you came prepared for the dark, $N.
else
say No torch? The lower halls will eat you alive.
endif
emote taps the unlit lantern by the door.
~
Its cousin hasnum($n torch 3) asks for at least a count of them, worn checks whether a named piece is actually equipped, and goldamt reads carried coin. Together they cover gatekeepers, toll takers, and every "you shall not pass without" in the trade.
Question 46: How do I check whether a player has finished a quest?
The questwinner function, given the player and the quest id:
GREET_PROG 100
if questwinner($n relic_run)
say The relic runner returns! Drinks are on the house.
else
say Rumor says the museum pays well for honest legwork.
endif
emote polishes a commemorative plaque.
~
Quest ids come from the quest system; ask the quest's author or check the quest tools for the exact string. For quests in progress, the qvar function reads fields of the player's active quest entry, and the quest commands mpstartquest and mpquestwin from the commands chapter let a script hand out and complete quests itself.
Question 47: Can my script behave differently at night, or by season or weather?
Yes. The functions isnight, istime, season, isseason, weather, and isweather read the mud's clock and sky at the moment they are asked:
GREET_PROG 100
if isnight()
say Dark roads bring dark business.
else
say Daylight is for honest trade.
endif
say The season is $%season()% and the sky does as it pleases.
~
For behavior that should fire AT a certain hour rather than merely differ when asked, use the TIME_PROG trigger from the triggers chapter, which fires as the clock reaches hours you list; question 91 shows a town crier built on it.
Question 48: How do I add randomness inside a body, not just in the header?
The rand function is a percent die: rand(30) answers yes thirty times in a hundred. The randnum function rolls a die of any size: randnum(6) is one to six.
GREET_PROG 100
if rand(50)
say Heads, the coin says. Lucky you.
else
say Tails. The house wins again.
endif
emote flips the worn copper coin high.
~
Header percents choose WHETHER a block runs; body randomness chooses WHAT it does when it runs. A greeter with four flavor lines behind rand splits feels twice as alive as one with a single line, at the cost of four more lines of script.
Memory And Variables
Question 49: How can a mob remember a player between visits?
Store a note on the PLAYER with mpsetvar $n and read it back with the var function. Notes stored on players are written into their saved character, so the memory survives logouts, reboots, and even the death and respawn of the mob that wrote it:
GREET_PROG 100
if var($n bram_met)
say Back again, $N? The stew pot remembers you fondly.
else
mpsetvar $n bram_met yes
say A new face! Sit, sit. First visit is always free.
endif
emote wipes the counter out of habit.
~
First visit, the note is empty, the else runs: warm welcome, and the note is written. Every visit after, forever, the familiar greeting. The bram_ prefix on the name is deliberate; question 56 explains it.
Question 50: What is the difference between $1 and a variable?
Lifespan. The numbered slots $0 through $9 are a scratch workbench that exists only for the current run of the current block: the moment the trigger finishes, they are gone. Variables written with mpsetvar are the filing cabinet: they live on an object and persist between runs, between triggers, and, on players, between sessions. Typical scripts do their arithmetic in slots and file the result in a variable, as the counter in question 54 shows. Loops also count into a slot, which is one more reason to think of slots as temporary work space.
Question 51: How long does a variable last?
It depends entirely on where you put it. On a mob or item, the note lasts as long as that particular copy: death, a reset sweep, or a reboot clears it. On a room, it lasts until the room reloads at a reboot or update, which makes rooms good for scene state such as a lever pulled. On a player, it lasts indefinitely, saved with the character. Choose the shelf to match the memory: a once-per-fight flag belongs on the mob, a toll receipt belongs on the player.
Question 52: Can two scripts share data?
Yes, and the mechanism is simpler than you might fear: notes live on objects, and ANY script can read or write notes on any object it can see. There is no private memory. So two scripts share by agreeing on a place: a guard's script can write a note onto the door with mpsetvar door alarm yes, and the door's script can test var(door alarm) or, from its own point of view, var($i alarm). A demonstration on a single host, reading its own note back into speech with the angle form:
GREET_PROG 100
mpsetvar $i gate_state sealed
say Current gate order: $<$i gate_state>. Any script here could read that.
~
For scripts in the SAME room, storing on a named object both can see is the whole answer. For scripts in different rooms, give the note to something that travels, usually the player: a questgiver in one town writes mpsetvar $n stage two, and a contact three zones away tests var($n stage == two). The player becomes the courier of the state.
Question 53: What is mpgset, and why can I not read a global back?
mpgset <name> <value> stores a value in the script engine itself, shared by the whole mud and saved across reboots. It exists for CoffeeMUD compatibility, and here is the honest limitation: in the current engine there is no dollar code or function that reads a global back into a script. Server code and admins can inspect them, but your script text cannot. So when two scripts need to share, do not reach for a global; use the object notes of question 52, which are readable from everywhere that matters. If a future engine update adds a reader, the variables chapter will say so.
Question 54: How do I count things, like visitors or attempts?
Combine the three memory tools: read the old count, add one with the math function, file the new count. The pattern is worth memorizing because every counter in scripting is this exact shape:
GREET_PROG 100
if var($i visits == '')
mpsetvar $i visits 0
endif
mpargset 1 $%MATH($<$i visits> + 1)%
mpsetvar $i visits $1
say Welcome! You are visitor number $1 by my count.
~
Line by line: if the note has never been written, start it at zero. Put old count plus one onto the workbench slot $1. File $1 back into the note. Speak it. The two quote marks in the first condition mean "empty", which is what a never-written note reads as.
Question 55: How do I make something happen only once?
Guard it with a variable: test the flag, stop if it is set, set it, then act. The classic customer is the boss speech that should happen once per fight rather than every round below a health threshold:
HITPRCNT_PROG 50
if var($i enraged)
return
endif
mpsetvar $i enraged yes
say Enough games! Now you see my true strength!
~
HITPRCNT_PROG fires EVERY qualifying round, so without the guard the boss bellows that line every two seconds until it dies. The return command ends the block on the spot. For something that should happen once when the mob LOADS, there is a dedicated trigger, ONCE_PROG, which runs a single time about a second after the mob enters the world and needs no guard at all.
Question 56: How do I make something happen only once per player?
Move the guard variable from the mob to the player: the script in question 49 is exactly this pattern, with mpsetvar $n writing the flag into the player's own saved notes. Since every script shares one pocket of notes per object, name the flag defensively: bram_met, not met, because some other builder's mob may also be storing met on the same players, and the two scripts would trample each other. Your mob's name as a prefix is the simple convention that prevents it.
Question 57: How do I erase a variable?
Store nothing into it: mpsetvar $n toll_paid with no value leaves the note empty, and empty counts as not-set everywhere that matters, including the bare var($n toll_paid) test and the == '' comparison. There is no separate delete command, and you do not need one; empty is the language's idea of gone.
Question 58: Why is my variable empty when another script reads it?
Four usual suspects, in order of likelihood. The two scripts are talking to different objects: one wrote on $i, meaning ITS host, and the other read var($i name), meaning its OWN host, which is a different creature; name the shared object explicitly. The variable names differ by a letter or a case of pluralization; copy and paste the name between the scripts. The reader ran before the writer ever fired; walk the sequence in game and check with a temporary say $<object name> marker. Or the writing object respawned since, taking its notes with it, which is question 51's lifespan table in action; move the note to a longer-lived shelf.
Loops, Timing, And Subroutines
Question 59: How do I repeat a line several times without pasting it?
A for loop counts from one number to another, holding the current count in a numbered slot, and next closes it the way endif closes an if:
GREET_PROG 100
say Watch closely, $N.
for $1 = 1 to 3
emote tosses ball number $1 in a lazy arc.
next
say And that is how you juggle badly.
~
The room sees three tosses, numbered 1, 2, 3, then the punchline. Counting downward works too: for $1 = 3 to 1 runs 3, 2, 1, ready for countdowns. Always count into a NUMBERED slot; a letter there would collide with the dollar codes.
Question 60: How do I make a script pause between lines?
The mpsleep command, with a number of seconds. The script stops there, waits, and then continues with its remaining lines as if nothing had happened:
GREET_PROG 100
say Give me a moment to think...
mpsleep 2
say Yes. Yes, I remember you now.
~
The visitor hears the first line, two real seconds of silence, then the second. The minimum pause is one second, and pauses can be sprinkled as often as you like. This single command is what turns a wall of text into a performance; almost every long speech in the cookbook chapter uses it.
Question 61: Why do the lines after my mpsleep sometimes never happen?
Because the continuation belongs to the host: if the scripted object is destroyed during the nap, dies and is swept, or the script is cleared, the sleeping remainder is quietly dropped. That is the designed behavior, not a fault; a dead mob finishing its speech would be far stranger. The practical consequences: keep DEATH_PROG bodies immediate, since delayed lines scheduled by a dying mob have no one to run them, and keep long scenes on durable hosts such as rooms when the speaker might plausibly be killed mid performance.
Question 62: How do I schedule something to happen later, without stopping the script?
The mpalarm command takes a delay in seconds and one command line, and sets it aside to run after the delay, while the script carries on immediately:
GREET_PROG 100
mpalarm 2 say The kettle I put on when you arrived is boiling now.
say Sit. Tea takes exactly as long as it takes.
~
The spoken line comes first, and two seconds later the kettle line arrives on its own. The difference from mpsleep is the shape: mpsleep pauses the whole remaining script, mpalarm plants one delayed line and moves on. The alarm dies with the host, same as question 61.
Question 63: What stops a loop from running forever?
The engine's caps: any single loop stops after two thousand passes, and the whole trigger run stops at a few thousand steps, with a line in the script_runaway log naming the culprit, as question 20 described. A while loop repeats as long as its condition stays true, so the well formed ones change something each pass:
GREET_PROG 100
mpargset 1 3
while $1 > 0
say Countdown check: $1 remains.
mpargset 1 $%MATH($1 - 1)%
endwhile
say All checks complete.
~
That counts 3, 2, 1 and finishes. Delete the line that subtracts one and the condition never changes; the cap would halt it and log it, the mob unharmed. If you only need "do this N times", prefer for, which cannot forget to count.
Question 64: Can one script, or one block, call another?
Yes, with named routines. A FUNCTION_PROG block never fires on its own; its header is a name, and any other block on the same object invokes it with mpcallfunc, runs it to completion, then continues:
GREET_PROG 100
mpcallfunc doorbell
say That chime announces every guest, $N.
~
FUNCTION_PROG doorbell
mpecho A small silver chime rings twice by the door.
return done
~
Write a flourish once, call it from the greeting, the speech block, and the gift block, and change it in one place forever after. For calling ACROSS objects, there is no direct command; share through notes on a common object as in question 52, or have one script force the event that fires the other's trigger.
Question 65: What does the return command actually do?
It ends the current block run on the spot; nothing below it executes. Inside an ordinary trigger that makes it the early exit for guard patterns, as in question 55. Inside a FUNCTION_PROG it additionally carries a result: any text after the word return becomes the routine's answer, which the callfunc function hands back when the routine is invoked from a condition. Most builders use bare return for years and never need the value; know it exists for the day a routine should report success or failure to its caller.
Question 66: How do I build a multi-step scene with proper pacing?
Chain speech and gesture with short sleeps between, and keep each beat small:
GREET_PROG 100
emote lowers his voice and leans close.
mpsleep 2
say What I am about to tell you does not leave this room.
mpsleep 2
say The cellar door was open again this morning.
~
Two second beats read as thought; five second beats read as dread; zero second beats read as a machine gun. If the mob also has idle RAND_PROG chatter, gate it off during the scene so it cannot interrupt, which is question 96's pattern. And remember the audience can walk away mid scene; write beats so that hearing only half still makes sense.
Items, Creation, And Protection
Question 67: How do I create an item in the room?
The mpoloadroom command clones a fresh copy of an item file into the host's room:
GREET_PROG 100
say You look half starved, $N. Here, eat.
mpoloadroom /obj/meal
emote sets a steaming plate down within reach.
~
The path is the item's file path; /obj/meal, /obj/torch, /obj/armor, and /obj/container are safe practice items that exist on this mud. A wrong path quietly loads nothing and the script continues, so if the item never appears, check the path spelling first. Ask a senior builder for the real item paths in your own area before shipping.
Question 68: How do I hand an item directly to a player instead of dropping it?
Load it into the host's OWN inventory with mpoload, then have the host give it away with the ordinary give command, which plays all the normal messages:
GREET_PROG 100
mpoload /obj/torch
give torch to $N
say Take this torch. The lower halls are darker than they look.
~
The give line is not an mp command at all; it is the same verb a player types, performed by the mob, which is why the room sees a natural hand-over. This pairing, mpoload then give, is the standard reward pattern in quest turn-ins, as question 93 shows in full.
Question 69: What is $b?
The most recently loaded thing. After any mpoload, mpoloadroom, or mpmload in the same run, $b names the object that just arrived and $B gives its short description, which lets the script talk about its own handiwork without hardcoding a name:
GREET_PROG 100
mpoloadroom /obj/torch
say Fresh from the storeroom: $B, still smelling of pitch.
~
$b also works as a target for commands, so mpjunk $b destroys the thing just created, and mpsetvar $b owner $N writes a note onto it. Before anything has been loaded in the run, $b is empty.
Question 70: How do I destroy an item a script created?
mpjunk destroys a named item, and mppurge destroys a named thing including mobs; neither will touch a player. Combined with $b you can create and uncreate within one breath, which is how brief props work:
GREET_PROG 100
mpoloadroom /obj/torch
mppurge $b
say Stock comes and stock goes. Mostly it goes.
~
For mobs your script summoned with mpmload, note they are marked to despawn when the area resets anyway, so cleanup is only your problem within the scene itself.
Question 71: How do I stop players stealing my quest item?
Put a veto on the item itself, so the protection travels with it everywhere. The CNCLMSG_PROG trigger runs BEFORE an action commits, and when it matches, your block runs INSTEAD of the action; the code GET is the moment of picking up:
GREET_PROG 100
say Admire the reliquary freely, $N. Touching it is another matter.
~
CNCLMSG_PROG GET ALL
mpechoat $n The reliquary grows searing hot and squirms from your grip.
mpechoaround $n The reliquary flares and refuses $N's hand.
~
Attach both blocks to the item: the greeting fires when it happens to be carried by a mob or scripted room scene, and the veto refuses every take. Because your block REPLACES the normal behavior, always narrate the refusal, or the player just sees silence and files a bug report. To protect one item by name from a guard or a room instead, use the mask form, a header of CNCLMSG_PROG GET followed by the item's key name; the bus chapter's relic example walks through it.
Question 72: How do I make a cursed item that cannot be taken off?
Veto REMOVE on the item, and for full effect add flavor at the moment it goes on:
WEAR_PROG 100
mpechoat $n The armor tightens with a sound like a satisfied sigh.
~
CNCLMSG_PROG REMOVE ALL
mpechoat $n The straps writhe out of your fingers. It is not done with you.
~
WEAR_PROG is the ordinary after-the-fact trigger for being worn, good for atmosphere; the REMOVE veto is what enforces the curse, refusing the take-off before it happens. Give cursed gear an escape route somewhere, a priest, a quest, a fee, because a permanent no with no story attached is not a curse, it is a support ticket.
Question 73: How do I seal a room so players cannot leave, or cannot enter?
Movement vetoes go on rooms. The code LEAVE fires as someone tries to walk out of the room carrying the script; ENTER fires as someone tries to walk INTO it. Both carry no message text, so keep the header bare:
GREET_PROG 100
mpecho The air here is thick, and reluctant to let anything go.
~
CNCLMSG_PROG LEAVE
mpechoat $n The doorway stretches away from you. You stay exactly here.
~
Attach to the room with mudprog here edit. The greeting sets the mood on arrival; the veto turns every exit attempt into your message instead of a move. Sealed rooms need a release, an if in the body checking a key, a variable, a spoken password via a companion SPEECH_PROG that clears the ward, or the whole room becomes a trap with no story. Note that a follower being dragged along by a group leader is not re-checked; the leader was.
Question 74: How do I make a sanctuary where no one can start a fight?
Veto ATTACK on the room, or on a guardian object standing in it:
GREET_PROG 100
say Peace holds within these walls, $N. That is not a request.
~
CNCLMSG_PROG ATTACK ALL
mpechoat $n Your anger drains away the moment you move to strike.
~
ATTACK is the moment a fight STARTS, and it fires for aggressive creatures as well as players, so monsters that wander in are becalmed by the same block. It does not stop a fight that began outside and spilled in, since those blows are exchanges of an existing fight, not new initiations; design your shrine's geography with that in mind. The aliases FIGHT and KILL both mean ATTACK in the header, whichever reads best to you.
Question 75: Can I make food that refuses to be eaten?
Yes; the code CONSUME covers both eating and drinking in one header:
GREET_PROG 100
mpoloadroom /obj/meal
mpecho A suspiciously perfect meal sits waiting on the table.
~
CNCLMSG_PROG CONSUME
mpechoat $n Your jaw simply refuses to close on it. Some hungers warn.
~
Attach to the meal itself, or to the room hosting the scene. The separate codes EAT and DRINK also exist when you want to refuse one but not the other, a fountain that may be drunk but a feast that may not be touched.
Question 76: How do I watch everything that happens in a room?
The observer side of the bus: EXECMSG_PROG runs AS actions complete, and a blank header observes every code. Unlike a veto it cannot stop anything; it sees, and that is the point:
EXECMSG_PROG
mpecho Behind the lattice, a clerk notes down what just happened.
~
Attach to the room or to any scripted object in it, including an inanimate item on the floor, which is the one way a mere object gets to witness events happening to OTHER things. Narrow the watch with a code and mask, such as a header of EXECMSG_PROG GET to see only pickups, and do your bookkeeping with mpsetvar counters rather than echoes once the novelty wears off; a clerk who narrates every drop grows old in minutes.
How The Engine Thinks
Question 77: How do vetoes interact with the game's normal checks?
The order is: the game's own checks first, then your veto, then the action. When a player types get relic, the game first applies everything it always applies, whether the item is visible, takeable, not guarded, not too heavy. Only if the game itself is satisfied does the cancel pass run, immediately before the action commits; your CNCLMSG_PROG is the last voice, not the first. Three consequences follow. A veto never sees attempts the game already refused, so your searing-hot relic message does not fire for a player too burdened to lift it; the game's weight message already ended that attempt. A veto can only say no; it cannot bless an action past a check the game would fail, so no script can let a player take the untakeable, and question 80 expands on that. And because your block replaces only the commit, a cancelled action costs the player nothing but the attempt; nothing has moved, nothing is half done. One special case worth knowing: movement of a grouped follower is not re-checked, because the leader's move was; vetoes gate the leader.
Question 78: What is the execution order when several blocks match one event?
On a single object, every block written for the fired trigger runs, in the order the blocks appear in the script, each checking its own header argument independently. Prove it to yourself with this pair:
GREET_PROG 100
say First block, spoken first.
~
GREET_PROG 100
say Second block, always second.
~
The order never varies; it is the order you wrote. When an event fans out across OBJECTS, mob and room both scripted for the same arrival for instance, each object fires independently, host by host. The bus cancel pass adds one twist: objects are consulted in scope order, and the FIRST OBJECT owning a matching block wins, running all its own matching blocks top to bottom while later objects are never consulted; question 79 gives that order. Within any one object, then, think "top to bottom, all of them"; across objects on the cancel pass, think "first owner wins".
Question 79: Who gets asked first on the cancel pass?
In this order: the primary object of the event, that is the item being taken, the door being opened, the vendor being traded with, the room being entered, the victim being attacked; then the room the actor stands in; then every scripted object in that room, creature or item alike; then the actor themselves if scripted. The first of those owning a matching CNCLMSG_PROG cancels the action, its blocks run, and the rest are never consulted. Practical reading: protection ON the thing beats protection near the thing, so curse the item itself when you can, and use room or guard level vetoes for policies about a place rather than a possession.
Question 80: Can a veto script make an action succeed that would normally fail?
No. The bus runs in one direction: scripts may cancel what the game was about to allow, never the reverse, and a veto block that runs no lines still cancels. What a script CAN do is perform a different action itself: the members-only door in the bus chapter refuses everyone, then in the allowed branch moves the member through by force with mptransfer, which travels outside the bus and so does not re-trigger the veto. That is the general pattern for "no, unless": cancel all, then script the exception by hand. It is powerful and it is also a scalpel; when a locked door with a key tells the same story, prefer the door.
Question 81: Do vetoes apply to builders and admins too?
Yes. The cancel pass does not check rank; a CNCLMSG_PROG LEAVE on a room holds the builder who wrote it as firmly as any player, which surprises almost everyone once. There is no allow command inside a veto body, so you cannot write yourself an exemption that lets the normal action proceed; if the block runs, the action is cancelled, full stop. Your escape hatches are real ones: mudprog here clear removes the script, admin movement commands travel outside the bus entirely, and the mptransfer pattern from question 80 can wave named people through by force. Test sealed rooms with that list in mind before you seal yourself in with style.
Question 82: What is the difference between GIVE and GIVING?
Tense, and therefore pass. GIVE is the cancel-pass code, the moment before a hand-over commits, so a header of CNCLMSG_PROG GIVE refuses gifts. GIVING is the observe-pass announcement of a COMPLETED hand-over, so watchers write EXECMSG_PROG GIVING. The asymmetry is honest: an observer header written GIVE will not match the completed event; use GIVING, or ALL, on the observer side. The same tense split explains CONSUME, the completed eat-or-drink, and CASTING, the completed skill use, though those two are also accepted as friendly aliases on the cancel side. The bus chapter's alias table settles any doubt.
Question 83: Why does my mask on an ENTER or LEAVE veto never match?
Because those two codes carry no message text at all; there is nothing for the mask words to match against, so a header like CNCLMSG_PROG ENTER north can never fire. Write movement vetoes with a bare code and do any narrowing inside the body with conditions, level($n), race($n), var notes, whatever the door cares about. This is the one place the header mask machinery genuinely cannot help you.
Question 84: Can an item inside my inventory veto things happening in the room?
No. The bus consults the primary object, the room, the objects lying IN the room, and the actor; it does not open bags or pockets, and a carried trinket is not in scope for someone else's actions. A carried or worn item IS the primary object of events happening to itself, so its own GET, WEAR, REMOVE, and GIVE vetoes always work from anywhere. If you want a talisman that protects its bearer from other events, script the bearer or the room, or accept the honest limitation.
Question 85: What happens if my veto script has an error in it?
The action is allowed. Every bus script runs inside error protection, and any failure counts as permission, by design: a broken script must never weld a door shut or make an item permanently untouchable. The same forgiveness runs through the whole engine, but the direction matters here: veto mistakes fail OPEN. So after editing a veto, test that it still refuses; a typo that breaks the block does not announce itself, it just quietly stops cancelling.
Question 86: How do I temporarily switch a behavior off and on?
The dependable pattern is a variable gate: every block you might want silenced starts by checking a note and leaving early when it is set.
RAND_PROG 100
if var($i quiet)
return
endif
emote hums while restacking crates.
~
SPEECH_PROG hush
mpsetvar $i quiet yes
say Fine, fine. Not another note.
~
Say hush and the humming stops; clear the note, from any script or with another speech block, and it resumes. The engine also offers mpdisable <trigger> and mpenable, which record a trigger type as switched off in the host's bookkeeping; the variable gate remains the form this guide recommends, because the gate is visible in the script itself, survives your own reading of it six months later, and can be as fine grained as one block rather than a whole trigger type.
Question 87: What are the text-mask triggers for, and how do I avoid feedback loops?
IMASK_PROG and REGMASK_PROG watch raw lines of text, colors stripped, and fire when a line matches their header, a plain substring for IMASK against the host's OWN action text, a regular expression for REGMASK against ANYTHING the host sees. They are the escape hatch when no named trigger covers your event. The danger is self reaction: if the body produces text its own header matches, the script sets itself off again, forever, until the step budget halts it. The cure is built in:
IMASK_PROG
mpechoaround $i A tiny bell chimes each time the curator so much as moves.
~
mpechoaround $i shows the room everything and the host nothing, so the host never sees its own chime and the loop cannot close. Make that your reflex in every mask body.
Question 88: Can a script react to emotes and socials?
Live, yes, through REGMASK_PROG, which sees every line printed in front of the host, emotes included:
REGMASK_PROG bows|curtseys
emote returns the courtesy with a precise nod.
~
The bar character in the pattern means or, so that answers either gesture. Test it by actually emoting in the room; the test command cannot match a keyword header, as question 15 explained. A dedicated SOCIAL_PROG trigger exists in the engine for exactly this purpose, but the live emote system does not yet call its hook, so today it fires only under the test command; the triggers chapter tracks its status. Until then, REGMASK is the working answer, with question 87's loop cure applied.
Characters With Craft
Question 89: How do I make a boss change tactics as its health drops?
Stack HITPRCNT_PROG blocks, one per phase, each with a one-shot guard. The header is a health threshold, not a chance: the block fires every combat round the boss's health sits at or below that percent, which is why the guard matters:
HITPRCNT_PROG 60
if var($i phase2)
return
endif
mpsetvar $i phase2 yes
say You want a real fight? Granted.
~
HITPRCNT_PROG 20
if var($i phase3)
return
endif
mpsetvar $i phase3 yes
say Enough! I yield, I yield! Call it off!
~
At 55 percent health only the first block qualifies; at 15 percent both thresholds are met, but the guards ensure each speech happened exactly once on the way down. Real bosses swap the speeches for mpcast attacks, summons via mpmload, or an mpflee. Remember the notes die with the mob, so every respawn starts fresh at phase one, exactly as it should.
Question 90: How do I make a mob flee or cry for help when losing?
The mpflee command makes the host bolt through an exit, and pairing it with a threshold trigger gives a survival instinct. For calling help, shout so the neighbors hear:
HITPRCNT_PROG 30
yell Guards! To me!
mpecho His cry rattles the shutters up and down the street.
~
Since HITPRCNT fires every qualifying round, an unguarded yell repeats each round below thirty percent, which for a panicking victim is honest behavior; give it a question 55 guard if once is enough. Actual reinforcements arriving is a second script on the guardhouse, listening with a REGMASK_PROG for the cry, or simply an mpmload in this block, loading the rescue where the fight is.
Question 91: How do I make a town crier who announces the hours?
The TIME_PROG trigger, whose header is the required list of hours to speak at, on the mud's own clock from 0 to 23:
TIME_PROG 0 8 20
yell Hear ye! The watch changes and the gates turn with it!
mpecho The crier's bell clangs three measured times.
~
That crier performs at midnight, morning, and evening. The mob checks the clock on its own heartbeat, so it must be loaded, which means somebody has visited its area since the last reboot; town squares keep criers naturally loaded. For patrol routes between announcements, the wander system on the NPC itself is the right tool, with the script riding along for the performances.
Question 92: How do I make a guard attack monsters on sight but greet citizens?
Two blocks on the same trigger, filtered by opposite zapper masks:
GREET_PROG -player
say Stay behind me, citizen. This ward is protected.
~
GREET_PROG -npc
mpkill $n
~
Every arrival fires GREET_PROG; the masks sort them. A player passes the first mask and hears the reassurance; a wandering monster passes the second and gets mpkill, which sets the guard on it. Refine the second mask if the area has friendly NPCs, for example -npc -name rat goblin to attack only the vermin by name. Pointing mpkill at players from a greeting is technically possible and almost always regretted; aggressive-to-players belongs to the NPC aggression systems, where it obeys the usual level and safety rules.
Question 93: How do I run a fetch quest turn-in, where the player hands me the item?
GIVE_PROG fires when a player hands the host anything, with the item in $o. Check it is the RIGHT thing, consume it, reward:
GIVE_PROG 100
if isname($o torch)
say The very torch I lost! You have my thanks, $N.
mpjunk $o
mpoload /obj/meal
give meal to $N
else
say Kind of you, but this is not what I asked for.
endif
emote straightens the ledger of favors owed.
~
The isname function matches the item's name words, mpjunk removes the turned-in item so it cannot be recycled, and the reward is the load-and- give pairing from question 68. Add a question 56 per-player flag if the reward should be once per person, and mpexp or mpmoney from the commands chapter when the prize is experience or coin rather than goods. Note the wrong-item branch RETURNS the nothing: the mob keeps whatever else it is handed unless you script a hand-back.
Question 94: Can my vendor react to what people buy and sell?
Yes: BUY_PROG fires on a scripted vendor when a player buys, with the purchase in $o, and SELL_PROG when a player sells TO the vendor:
BUY_PROG 100
say A fine purchase, $N. No refunds, mind you, but fine all the same.
~
These are reaction triggers; the trade has already happened when they speak. To actually REFUSE trade, the vendor carries a cancel block, a header of CNCLMSG_PROG BUY or CNCLMSG_PROG SELL, and the body explains the refusal, snobbery about cursed goods being the classic. The dedicated shopkeepers chapter builds a full personality vendor from these parts.
Question 95: How do I make a door or chest that talks when used?
Put the script on the container or door itself; the four handling triggers fire as it is used, with $n the person doing it:
OPEN_PROG 100
mpecho The lid rises with a theatrical groan of hinges.
~
CLOSE_PROG 100
mpecho The lid drops shut, muttering dust.
~
LOCK_PROG and UNLOCK_PROG complete the set. Attach with mudprog chest edit while standing with it. On these four the scripted thing IS the door, so $o is empty; $i is the door itself. To REFUSE opening rather than merely comment on it, that is the bus again: a CNCLMSG_PROG OPEN block on the same object, with the refusal narrated in the body.
Question 96: How do I stop idle actions from interrupting a scripted scene?
Gate the idle block on a busy note that the scene sets on its way in and clears on its way out:
GREET_PROG 100
mpsetvar $i midscene yes
say Quiet now. Watch the doorway with me.
mpsleep 2
say There. Did you see the shadow cross it?
mpsetvar $i midscene
~
RAND_PROG 100
if var($i midscene)
return
endif
emote drums his fingers on the sill.
~
While the note holds a value, the RAND block exits before its emote; the scene plays uninterrupted, and the final line, storing nothing, empties the note and hands the character back to its idle life. This is question 86's switch pattern applied to pacing, and it belongs in every mob that has both a performance and a fidget.
Limits, Speed, And What Comes Next
Question 97: Do scripts slow the mud down?
Not in any way you can cause by accident. An object with no script costs the engine a single property glance, essentially nothing, so the ten thousand unscripted things in the world stay free. A scripted object is parsed once and cached, reparsed only when its text changes, and each trigger run is bounded by the budgets from question 20. World wide triggers such as LOGIN_PROG use a registry of exactly the objects that define them rather than sweeping the world. The one performance lever you personally hold is frequency: a RAND_PROG at 100 or a blank-headed CHANNEL_PROG on a popular channel fires constantly, which annoys players long before it troubles the server. Write rare, purposeful reactions and both audiences stay happy.
Question 98: How big can a script get?
Text size is effectively unlimited; what is bounded is each RUN, by the step and loop budgets from question 20, and those budgets are generous: a run that legitimately needs thousands of steps is rare outside of runaway accidents. A script can hold as many blocks as you like, so characters with dozens of triggers are routine. When one grows unwieldy, organize rather than trim: share repeated behavior through FUNCTION_PROG routines, question 64, comment the sections, question 9, and keep a master copy in a file, question 23. If a single block wants hundreds of lines, it usually wants to be several blocks and a variable or two instead.
Question 99: Can I paste in scripts from CoffeeMUD documentation?
Mostly yes; the language is a deliberate CoffeeMUD parity port. The trigger names, the mp command family, the function library, zapper masks, code-spec prefixes on bus headers, and the alias spellings are all accepted, so the bulk of published CoffeeMUD scripting drops in unchanged. The honest differences: here $n and $N are the same proper name; mpsleep pauses natively, which CoffeeMUD needed alarm gymnastics for, so ported scripts full of MPALARM chains can often be simplified; globals written by mpgset cannot yet be read back by script, question 53; and a few triggers, DAMAGE_PROG, BRIBE_PROG, SOCIAL_PROG, and the mob-side movement trio, are parsed and testable but not yet wired to live events, with the triggers chapter naming the working substitutes for each. When a pasted script misbehaves, check its triggers against that chapter before debugging its logic.
Question 100: This FAQ did not answer my question. Where do I go next?
To the shelf it lives on. The basics chapter is the gentle full introduction; triggers catalogues every WHEN; commands every mp WHAT; functions everything an if can ask; variables all of memory and the dollar codes; flow the deep dive on if, switch, and loops; bus the veto and observer system; cookbook and the workbooks are finished characters to steal from; shopkeepers builds vendors; troubleshooting is the long form of this FAQ's debugging section; and reference is the terse everything-on-one-page card. All under help mudprog-<name>, with help mudprog as the hub. Beyond the shelf, ask a senior builder, and bring the script: in this language the script IS the question, and a second pair of eyes on the Triggers line solves most mysteries in a minute. Now go give something a soul.
This is the last chapter of the MUDProg guide, and it is the one you will open most often once the others are read: the dictionary and the map. The first half is a glossary, every word of scripting jargon the other chapters use, in alphabetical order, each explained in plain language as if you had never heard it before, with a tiny working example wherever an example teaches faster than a paragraph. The second half is the master index: every trigger, every command, and every function in the entire engine, one line apiece, with a pointer to the chapter that covers it in depth. The chapter closes with the recommended reading order through the whole twenty-four chapter guide, so that a brand new builder always knows what to read next.
Nothing in this chapter is new material. If a term here surprises you, that is the glossary doing its job: read the entry, then follow the pointer to the chapter that teaches the idea properly. And if you have never scripted at all, do not start here; start with help mudprog-basics, which builds everything up from zero, and come back when the vocabulary starts to pile up.
How To Use This Chapter
Three ways, depending on what you walked in needing:
- You met a word you do not know. Scroll the glossary below; the terms are alphabetical, from Argument to Zapper Mask. Each entry says what the word means, where you will meet it, and which chapter teaches it fully. Entries lean on each other, so "see Chance" means there is a Chance entry a little further up or down the page.
- You remember that a trigger, command, or function exists but not what it does, or you know what you want to do but not what it is called. Jump to the master index in the second half. It is grouped the same way the engine is: triggers first, then commands, then functions, with one honest line for each.
- You are new and want a curriculum. The final section lays out the reading order for all twenty-four chapters, in stages, with a word on why each chapter sits where it does.
Every script example in the glossary is complete and attachable exactly as printed: walk up to a practice mob, type mudprog <target> edit, enter the lines, finish with a period, and fire the block with mudprog <target> test followed by the trigger name. The examples were all machine-tested against the live engine before this page shipped.
The Glossary
A note before the alphabet starts. Scripting has less jargon than it first appears; most of these sixty-odd entries are ordinary words that scripting bends only slightly, like body and header, and a handful are genuine terms of art, like zapper mask and ok-pass, inherited from the CoffeeMUD engine this system is modelled on. When two words mean the same thing, both are listed and one points at the other, so you can look up whichever one you actually met.
Argument
The extra information a name needs to do its job. The word appears in three places, and it means the same thing in all three.
A trigger's argument is everything after the trigger name on the header line. In GREET_PROG 100 the argument is 100, a percent chance. In SPEECH_PROG stew food the argument is the keyword list stew food. A few triggers give their argument a special meaning of their own, such as the health threshold of HITPRCNT_PROG; the master index below notes each one.
A command's arguments are the words after the command. In mpechoat $n Well done! the first argument names the receiver and the rest is the text to send.
A function's arguments sit inside its parentheses. In level($n) the argument is $n, and the function answers with that person's level. Some functions take two or more arguments separated by spaces, such as var($i met), which reads the note named met from the host.
When a chapter says "the argument is optional", it means the name works with nothing after it: RAND_PROG with no number behaves as always-fire, and hastitle($n) with no title argument asks whether $n has any title at all.
Block
The unit a script is built from; also called a PROG block or just a prog. A block is a header line naming a trigger, then a body of command lines, then a line holding only a tilde. A script may contain any number of blocks, and when an event fires, every block registered for that trigger runs in the order written. This three-line script is one complete block:
GREET_PROG 100
say Welcome, $N, to the glossary of all things.
~
The shape never changes. Everything else in scripting is about what goes in the header and the body. See Header, Body, and Tilde; the anatomy is taught from zero in the basics chapter.
Body
The middle of a block: the lines between the header and the tilde. The body is the WHAT of a script, the instructions to perform when the trigger fires, run top to bottom, one line at a time. A body line is either a command (see Command) or control flow (see Control Flow). A body can be one line or a hundred; blank lines inside it are ignored, and lines starting with # are comments (see Comment).
Break
The command that leaves a loop early. When a running script meets the word break inside a for or while loop, the loop ends immediately and the script continues after the loop's closing line. Inside a switch, break ends the current case, though in this engine a case ends on its own anyway, so you rarely need it there. Here the loop is told to count to ten but is broken out after two passes:
GREET_PROG 100
for $1 = 1 to 10
if number($1) > 2
break
endif
say Ring number $1 of the bell.
next
say The bell stopped at two rings, as intended.
~
The room hears ring one, ring two, and then the closing line; rings three through ten never happen. The flow chapter covers break alongside its cousin return, which stops the whole script rather than one loop.
Bus
Short for the message bus: the machinery inside the game engine that turns physical actions into little messages, shows those messages around BEFORE the action commits, and lets scripts either cancel the action or watch it happen. Picking up an item, opening a door, starting a fight, walking out of a room: each travels the bus as a message with a message code (see Message Code), passes the ok-pass where a CNCLMSG_PROG block can veto it (see Veto and Ok-Pass), and then, if allowed, executes while EXECMSG_PROG observers watch (see Observer and Exec Pass). The whole model has its own chapter, help mudprog-bus, and it is the most powerful tool in the engine, so read that chapter before you use it in anger.
Case
Two unrelated meanings, both common.
First, a case is one arm of a switch block: the line case merchant starts the lines that run when the switched value is merchant. See Switch and Default.
Second, "case" as in upper and lower case letters. Script text is case-insensitive almost everywhere: GREET_PROG, greet_prog, and Greet_Prog are the same trigger, mpecho and MPECHO are the same command, and string comparisons with == ignore case too, so class($n) == Mage matches a mage. The convention throughout the guide is trigger names in capitals and everything else in lower case, purely for readability. The one place case genuinely matters is text you print: players see your capitalization exactly as typed.
Chance
The most common kind of trigger argument: a whole number from 1 to 100 that sets the percent probability the block fires each time its event happens. GREET_PROG 100 fires on every entrance; GREET_PROG 25 greets roughly one visitor in four; RAND_PROG 5 fires on about five of every hundred heartbeats. A chance of 100, the word all, or nothing at all each mean "always". A chance of 0 or below never fires, which makes lowering a header to 0 a handy way to switch a block off without deleting it. This block demonstrates a certain chance:
RAND_PROG 100
emote flips a coin and smiles at the result.
~
While testing, set chances to 100 so you see the block work, then dial them down before you walk away; the basics chapter tells the cautionary tale. Also called percent (see Percent, which has a second meaning worth knowing).
Clause
One piece of a zapper mask. A mask like -class mage -level 30 has two clauses: the -class clause with its allowed value mage, and the -level clause with its threshold 30. Every clause must pass for the trigger to fire; there is no "or" between clauses. See Zapper Mask for the full picture and the reference chapter for every clause type.
Code
A slippery word, because the guide uses it in three senses and only context tells them apart.
Game code is the LPC programming the mud itself is written in. You never touch it; the whole point of MUDProg is scripting without game code.
A dollar code is a two-character token like $n that is swapped for a live value when a line runs. See Dollar Code.
A message code is the short uppercase name of an action on the message bus, like GET or OPEN. See Message Code.
When another chapter says "the code" with no qualifier, it nearly always means one of the last two, and the sentence around it will say which.
Code-Spec
The first word after CNCLMSG_PROG or EXECMSG_PROG in a header: it specifies WHICH message codes the block should intercept. It can be one plain code (CNCLMSG_PROG GET), the wildcard ALL, or a CoffeeMUD-style decorated form such as <GET or T=GET, which this engine accepts and treats as the plain code. A few codes have aliases that match each other, such as CONSUME for EAT and DRINK. The bus chapter walks through every form; the reference chapter tabulates them.
Command
A body line that does something. Commands come in two families. Plain game commands are exactly what a player could type, performed by the host: say, emote, yell, socials, wield, wear, cast, and the rest. Mp commands are the script-only family, each starting with the letters mp, that reach past what a player can do: mpecho narrates to the room, mpoloadroom creates an item, mpdamage hurts someone, mptransfer teleports someone. Both kinds sit side by side in a body:
GREET_PROG 100
emote bows with the exaggerated care of a stage actor.
say Both of those lines were commands, $N.
~
If the engine does not recognize a line as an mp command or control flow, it hands the line to the host to perform as a game command; if the game does not know it either, the line quietly does nothing (see No-Op). Every mp command is catalogued in help mudprog-commands and listed one-per-line in the master index below.
Comment
A note to yourself inside a script. Any line whose first character is # (or *) is skipped completely by the engine; it exists only for the next person reading the script, who is usually you, three months later, wondering what you meant. Comments cost nothing, so leave them wherever a future reader might frown:
GREET_PROG 100
# this line is a note to myself and never runs
say The comment above me is invisible to you, $N.
~
One habit worth stealing: start long scripts with a comment saying what the script is for and who wrote it.
Comparator
The symbol in the middle of a comparison: the == in class($n) == mage, the >= in level($n) >= 20. The full set is == (equals), != (does not equal), > (greater than), < (less than), >= (at least), <= (at most), and .in. (appears inside, for text). The engine is forgiving about spelling: = works like ==, <> like !=, and => and =< like >= and <=, so pasted CoffeeMUD scripts behave. When both sides look like numbers the comparison is numeric; otherwise it compares text, ignoring case. A tiny demonstration, true for every player who ever lived:
GREET_PROG 100
if level($n) >= 1
say You have lived at least one level's worth, $N.
endif
~
Also called an operator (see Operator). The flow chapter teaches comparisons gently; the reference chapter tabulates them.
Condition
A question a script asks, written after if or while, answered yes or no at the moment the line runs. A condition is usually a function, with or without a comparison: ispc($n) asks "is this a player?"; level($n) >= 40 asks "is this player at least level forty?"; var($i sprung) == 1 asks "is the host's note sprung set to one?". Conditions can be chained with connectors (see Connector) and flipped with not. An if with a false condition skips its lines; a while with a false condition ends its loop. The whole subject, including every function you can ask, lives in help mudprog-flow and help mudprog-functions.
One warning that saves an evening of confusion: a condition with a misspelled function name does not error, it just answers no (see No-Op and Truthy). If a branch never fires, read the function name letter by letter first.
Connector
The words and, or, and not, which join simple conditions into compound ones. All the joined parts of an and must be true; any one part of an or suffices; not flips the answer of what follows it. The engine also accepts the CoffeeMUD spellings andnot and ornot. Evaluation runs left to right. A compound question in the wild:
GREET_PROG 100
if ispc($n) and isalive($n)
say You are alive and you are real. Excellent news, $N.
endif
~
When a chain mixes and with or, the guide's advice is to keep each header question simple and split complicated logic across nested ifs, which read better and misfire less. The flow chapter shows the patterns.
Context
Everything the engine remembers about one run of one block: who the host is, who the source and target are, which items are involved, what the message text was, plus the ten scratch slots $0 through $9. A fresh context is built each time a trigger fires and thrown away when the run finishes, which is why slots do not survive between runs (see Slot) and why the same block can serve two players at once without their answers tangling. You never touch the context directly; the dollar codes are its public face. See Dollar Code, Source, Target, Host, and Message.
Continuation
What remains of a script after an mpsleep line: the engine sets the unfinished lines aside, waits the requested seconds, and then runs them, with the context (see Context) carried over intact. The pause is invisible in the script text; you just write the lines in order:
SPEECH_PROG all
say Give me a moment to think about that.
mpsleep 2
say Yes. I have thought about it, and I agree.
~
The listener hears the first line, two seconds of silence, then the second line. Sleeps work inside ifs and loops too; whatever was left to do resumes after the delay. The shortest sleep is one second, and mpwait is the same command under another name. The commands chapter covers sleeping and its scheduled cousin mpalarm; the dialogue deep dive (help mudprog-dialogue) turns pauses into whole staged cutscenes.
Control Flow
The umbrella term for every body line that decides or repeats rather than does: if, else, endif, switch, case, default, endswitch, for, next, while, endwhile, break, and return. Control flow is what turns a list of commands into behavior with choices in it. All of it is taught in help mudprog-flow; the reference chapter has the compact syntax table. See also Nesting.
Default
The catch-all arm of a switch block: its lines run when no case matched the switched value. A switch without a default simply does nothing when nothing matches, which is sometimes what you want and sometimes a silent bug, so the guide's habit is to always write one, even if it only says something generic. See Switch for a worked example.
Dollar Code
A token starting with a dollar sign that is swapped for a live value at the moment a line runs. The everyday ones are $n and $N for the source's name, $i for the host's name, $t and $T for the target's name, $o for the first involved item, and $g for the message text; there are a few dozen in all, covering pronouns, random bystanders, the room, and more:
GREET_PROG 100
say My own name is $i, and yours is $N.
~
Beyond the two-character codes there are three longer forms: $<obj var> reads a stored note (see Variable), $%func(args)% inserts a function's answer (see Percent), and $$ prints a literal dollar sign. The codes match CoffeeMUD's exactly, so scripts written from CoffeeMUD documentation substitute identically here. The complete table, with every pronoun form, is in help mudprog-variables and again in the reference chapter.
Editor
The line editor opened by mudprog <target> edit, where you type a script in one line at a time. Finish with a single period on its own line to save, or @abort to throw the session away. Whatever you save replaces the target's previous script and is live immediately; no reload, no reboot. For one-liners, mudprog <target> set and append skip the editor, with a semicolon standing in for each line break. The basics chapter walks through all of it, and the editor is also the reliable way to enter text full of dollar signs, which some telnet setups mangle when typed on a command line.
Engine
The machinery that makes scripts go: it reads the text stored on each object, splits it into blocks, watches the game for events, decides which blocks fire, substitutes the dollar codes, evaluates the conditions, and performs the commands. You never speak to the engine directly; you write text and the engine reads it. Its three promises are worth engraving: a changed script is re-read automatically the moment it is saved; a script can never crash its host or the mud; and a bad line simply does nothing rather than erroring (see No-Op). That last promise cuts both ways, as this block shows:
GREET_PROG 100
mpechoat nobodyhere You will never read this line.
say Nothing broke just now, which is the whole point.
~
The echo is aimed at someone who does not exist, so it fails silently; the greeting still runs, and nobody is hurt. The price of that safety is that your typos fail silently too, which is why help mudprog-troubleshooting exists.
Event
Anything that happens in the game: a player enters a room, someone speaks, a fight starts, an item changes hands, a mob dies, the clock strikes an hour. Events are the raw material of scripting; a trigger is a named hook onto one kind of event (see Trigger). When the guide says an event "fires" a trigger, it means the engine noticed the event and ran the matching blocks. See Fire.
Exec Pass
The second of the message bus's two checkpoints: after an action has been allowed and has actually happened, every scripted object in scope with a matching EXECMSG_PROG block gets to run. By then the deed is done; an exec-pass script can comment, count, reward, or scheme, but not undo. Compare Ok-Pass, which comes first and can cancel. Also called the observe pass; the script that runs is called an observer (see Observer). The bus chapter is the full treatment.
Fire
What a trigger does when its moment comes: the block fires, meaning its body runs. A block fires when three gates all open: the event matched the trigger name, the header argument passed (the chance rolled true, the keyword matched, the mask allowed the source), and the host object actually carries the script. When a block you expected stays silent, one of those three gates stayed shut, and the troubleshooting chapter walks the checklist in order.
Function
A question with a name, asked in parentheses form: level($n), class($n), goldamt($n), hitprcnt($i), var($i met). Functions are used in two places. In a condition, after if or while, the answer steers the script (see Condition). Inside the percent form $%...%, the answer is pasted into text:
SPEECH_PROG all
say My health sits at $%hitprcnt($i)% percent right now.
~
Most functions take an object reference as their first argument and default to the host when you leave it out. The full catalogue, with an entry for each of the hundred-plus functions, is help mudprog-functions; the one-line version is in the master index below. Do not confuse functions with function progs, which are the next entry.
Function Prog
A named routine inside a script: a block headed FUNCTION_PROG followed by a name of your choosing. It never fires on its own; it runs only when another block calls it, with mpcallfunc or the callfunc() function. Function progs exist so a script that does the same thing from five different triggers can write the shared part once:
GREET_PROG 100
mpcallfunc fanfare
~
FUNCTION_PROG fanfare
mpecho Trumpets sound a small but heartfelt fanfare.
~
A function prog can end with return followed by a value, and the caller can read that value back through callfunc(), which turns a routine into a question-answering machine. The flow chapter covers routines and return values; the patterns chapter uses them heavily.
Global
A value stored in the script engine itself rather than on any object, written with mpgset <name> <value>. Globals survive reboots and are shared mud-wide, but there is an honest limitation in the current engine: no dollar code or function reads a global back into a script. They can be read by server-side code and inspected by admins, not substituted into script text. So when two scripts need to share a value, do not reach for a global; store a note on an object both scripts can see, usually the room, and read it with the $< > form. The variables chapter explains the workaround with a worked example.
Header
The first line of a block: the trigger name, then the argument. The header is the WHEN of the block, as the body is the WHAT. GREET_PROG 100, SPEECH_PROG stew food, HITPRCNT_PROG 25, and CNCLMSG_PROG GET sword are all headers. Two comforts: the _PROG suffix is optional (GREET means GREET_PROG), and case does not matter. One caution: the engine accepts ANY word as a header name and files the block under it faithfully, so a misspelled trigger name is not an error, just a block that never fires. The Triggers line shown by mudprog <target> is how you catch that; the basics chapter's mistakes section tells the story.
Heartbeat
The mud's pulse: roughly every two seconds, every living thing gets a moment to act. Scripts meet the heartbeat in two triggers. RAND_PROG rolls its chance once per heartbeat of the host mob, which is why RAND_PROG 5 fires about ten times a minute in total. FIGHT_PROG fires with each combat round, which rides the same pulse. When a chapter says "per heartbeat", read "every couple of seconds".
Host
The object carrying the script: the mob, room, or item the script is attached to. In script text the host is $i (its name) and $I (its short description), and commands with an optional who argument, like mprejuv, default to the host when you leave it out. One script, one host; when a guard and a gate each need behavior, each carries its own script. The word matters most in the bus chapters, where WHERE a block lives decides which actions it can see (a veto on an item guards that item; the same veto on the room guards everything in it). See also Source and Target, the other two members of the cast.
Keyword
A word a speech-like trigger listens for. The header SPEECH_PROG stew food hungry fires when a spoken line contains stew or food or hungry, anywhere in the sentence, any case; it stays silent otherwise. The word all in place of keywords means "react to any speech". Keywords are how questmasters key on quest, how the amulet dealer below keys on amulet, and how you keep NPCs from answering sentences that were never aimed at them:
GREET_PROG 100
say Ask me about the amulet and I will answer.
~
SPEECH_PROG amulet
say The amulet was lost in the Brinewarrens a century ago.
~
To match a whole phrase rather than any single word, start the argument with the letter p (see Phrase Match). GIVE_PROG and the bus triggers use keywords too, matched against the item name or message text. The triggers chapter has the details; the dialogue deep dive builds whole conversations on keywords.
Loop
A piece of control flow that repeats its lines. The for loop counts: for $1 = 1 to 5 runs its body five times with the current count in the slot $1, and next closes it. The while loop repeats as long as a condition stays true, and endwhile closes it. This one counts three bell strokes:
GREET_PROG 100
for $1 = 1 to 3
say Counting the bells: $1.
next
~
Loops nest freely inside ifs and each other, break leaves one early (see Break), and every loop is capped so a mistake cannot spin forever (see Loop Cap and Step Budget). The flow chapter teaches both loops with worked examples.
Loop Cap
The engine's hard ceiling on a single loop: two thousand passes. A for asked to count to a million, or a while whose condition never turns false, stops at the cap and the script simply moves on past the loop. You will never meet the cap in honest scripting; it exists so a typo cannot hang the mud. Its sibling is the step budget, which bounds the whole run rather than one loop (see Step Budget and Runaway Script).
Mask
An umbrella word for "a filter written in the header". Three kinds share it. A zapper mask filters WHO may set a trigger off, by class, race, level, and so on (see Zapper Mask). A keyword mask filters WHAT text sets a speech-like or bus trigger off (see Keyword). And the two text-mask triggers, IMASK_PROG and REGMASK_PROG, fire on text itself: IMASK_PROG when the host's own output contains a substring, REGMASK_PROG when any text the host sees matches a regular expression (see Regular Expression). When a chapter says "the mask", the nearest example shows which kind it means. Here a zapper mask restricts a greeting to players:
GREET_PROG -player
say Only a real person could have made my bell ring, $N.
~
Message
The text riding along with an event, available to the script as $g (lowercased) and $G (as typed). For a speech trigger it is the spoken line; for GIVE_PROG the item's name; for BRIBE_PROG the coin amount; for CHANNEL_PROG the channel traffic; for LEVEL_PROG the new level. Keyword arguments are matched against this same text, and a block can quote it back:
SPEECH_PROG all
say I heard every word of that. You said: $g
~
When you fire a block artificially with mudprog <target> test, the message is the single word test, which is why keyword blocks need a real say to test them. See Keyword; the variables chapter covers $g alongside the other codes.
Message Code
The short uppercase name of an action on the message bus. There are eighteen: GET, DROP, PUT, GIVE, WEAR, REMOVE, OPEN, CLOSE, LOCK, UNLOCK, EAT, DRINK, BUY, SELL, CAST, ENTER, LEAVE, and ATTACK. A CNCLMSG_PROG or EXECMSG_PROG header names one of them (or the wildcard ALL) as its code-spec, and the block then intercepts exactly that kind of action. A few friendly aliases exist, such as CONSUME matching both EAT and DRINK, and FIGHT matching ATTACK. The full table with aliases is in the bus chapter and the reference chapter. See Bus, Code-Spec, Veto, and Observer.
Nesting
Putting one structure inside another: an if inside an if, a loop inside an if inside a loop. Nesting is legal to any sensible depth, and every opener must find its closer: each if its endif, each for its next, each while its endwhile, each switch its endswitch. Indenting each level a few extra spaces costs nothing (the engine trims it) and is the single best habit for keeping nested scripts readable. When a nested script misbehaves, count openers and closers first; a missing endif makes the engine swallow lines you meant to run unconditionally.
No-Op
Short for "no operation": a line that does nothing, quietly. This is the engine's answer to every mistake: a command with a bad argument, a load of a file that does not exist, an echo aimed at someone who is not there, a misspelled function in a condition. No error message, no crash, just silence and on to the next line. The design keeps builders safe and the mud stable; the cost is that diagnosing a silent script means checking your spelling and your targets rather than reading an error. The troubleshooting chapter is built around exactly that kind of detective work. See Engine for a demonstration block.
Object
Anything that exists in the game world: a mob, a room, an item, a container, a door. Scripting cares because ANY object can carry a script, and because dollar codes and command arguments all name objects. When the guide says "object reference", it means any way of pointing at one: a dollar code like $n, a name like guard, or the words self and here. Mobs, rooms, and items each favor different triggers, noted per trigger in the master index below.
Observer
A script that watches an action happen without interfering: an EXECMSG_PROG block, running on the exec pass of the message bus. The customs clerk who logs every trade in his room is an observer; so is a shrine that counts offerings. An observer runs after the action is done and cannot stop it; for that, see Veto.
EXECMSG_PROG ALL
mpecho The clerk scribbles a careful note about what just happened.
~
Scope matters: an observer sees actions near its host, so the same block on an item, a mob, or the room watches different slices of the world. The bus chapter draws the map.
Ok-Pass
The first of the message bus's two checkpoints. Before a bus action commits, the engine asks every scripted object in scope: do you have a CNCLMSG_PROG block matching this message? The first that does wins: its block runs as the replacement behavior and the action is cancelled. That question-asking round is the ok-pass, named for CoffeeMUD's okMessage cycle. If no one objects, the action proceeds to the exec pass (see Exec Pass). Two facts to keep: the ok-pass never errors (a broken script counts as permission, so a veto can never wedge shut), and in a room with no scripted objects it costs almost nothing. See Veto, Bus, and help mudprog-bus.
Operator
Another name for comparator: the symbols ==, !=, >, <, >=, <=, and .in. that sit in the middle of a comparison. See Comparator for the list and the forgiving alternate spellings. The .in. operator deserves one extra sentence because it is the odd one out: a .in. b asks whether the text a appears anywhere inside the text b, ignoring case, which makes it the tool for questions like "does the spoken line contain my name".
Percent
Two meanings, both everywhere.
First, the percent chance in a trigger header; see Chance.
Second, the percent form: $%func(args)% pastes a function's answer into text, as in say You carry $%goldamt($n)% gold. The form starts with a dollar sign and a percent sign and ends with a percent sign; everything between is the function call. One honest limit: percent forms do not nest inside each other, so compute an inner value into a slot with mpargset first when you need layers. The variables chapter teaches the form; the functions chapter lists what you can call.
Phrase Match
The keyword argument's whole-phrase variant: start the argument with the letter p and a space, and the rest is matched as one connected phrase rather than a list of separate words. SPEECH_PROG p open sesame fires only when a spoken line contains open sesame in that order; SPEECH_PROG open sesame would fire on either word alone. The p form works anywhere keywords do, including the bus triggers' keyword masks.
Prog
The word "prog" is inherited from MOBPROG, the scripting system of the old Diku muds, by way of CoffeeMUD; it is short for program. On this mud it survives in two places: the trigger names all end in _PROG, and a block is often called a prog block or just a prog ("add a greet prog to the innkeeper"). Wherever you meet it, it just means script. See Block.
Property
A named pocket of data that game code keeps on an object. Scripts brush against properties in three places. The script itself lives in a property called mudprog, written for you by the mudprog command. Your script notes live in a property called script_vars, written by mpsetvar (see Variable). And the mpset command plus the hastag() function let advanced scripts write and read other properties directly. Unless you are doing that last thing deliberately, you can forget the word; the commands handle the pockets for you.
Regular Expression
A pattern language for matching text, used by exactly one trigger: REGMASK_PROG fires when any text the host sees matches the pattern in its header. Simple patterns are just the text itself (REGMASK_PROG dragon fires on any line containing dragon); the full language, with its dots and stars and anchors, is a standard one and far beyond this glossary. If you do not already know regular expressions, you do not need them: keywords cover nearly every real case (see Keyword), and the triggers chapter shows the few places REGMASK_PROG earns its keep, such as reacting to emotes.
Return
The command that stops a script on the spot. Lines after a return never run:
SPEECH_PROG all
say You will hear this line.
return
say You will never hear this line at all.
~
Return has two everyday uses. In an ordinary block it is the early exit: guard clauses like if isnpc($n) followed by return keep the rest of a script from running on the wrong audience. In a function prog, return with a value after it hands that value back to the caller (see Function Prog). The flow chapter covers both.
Runaway Script
A script that would run forever or do far too much work in one go, usually by accident: a while whose condition can never turn false, or loops nested into an avalanche. The engine stops runaways automatically: each loop stops at the loop cap and the whole run stops at the step budget (see Loop Cap and Step Budget). When the budget trips, the run is halted, the host shrugs and carries on, and a note is written to the log file /log/script_runaway naming the host and trigger so a builder can find and fix the script. A runaway cannot hang the mud; the worst it does is embarrass its author, gently, in a log file.
Script
The whole text attached to one object: one or more blocks, in order, stored in the object's mudprog property, read and performed by the engine. "The guard's script" means everything you would see by typing mudprog guard. A script belongs to one object; give two objects behavior and you have written two scripts, even if the text is identical. See Block, Host, and Engine.
Slot
One of the ten temporary pockets $0 through $9: the script's scratch paper. A for loop counts into a slot; mpargset writes one by hand; mploadvar copies a stored note into one; and any line can read one with its dollar code. Slots are wiped when the run ends, which is the point: they are the workbench, not the filing cabinet. Anything that must outlive the current run goes into a note instead (see Variable). The variables chapter draws the workbench-versus-cabinet picture in full.
Source
Whoever set the trigger off; the actor of the event. The player who walked in, spoke, attacked, gave the item, opened the box. In script text the source is $n and $N, and its pronouns are $e, $s, and $m. The source is the answer to "who did this?", and nine scripts in ten spend their time talking to it. When you fire a block artificially with mudprog <target> test, YOU are the source (and the target too; see Target). One of the three members of the cast, with Host and Target.
Step Budget
The engine's ceiling on how much work one trigger run may do: a few thousand steps, where each executed line and loop pass costs one. An honest script never gets near it; a runaway hits it and is stopped (see Runaway Script). The budget resets for every run, so a script stopped today runs again tomorrow, still broken but still safe. A loop that finishes naturally never notices the budget exists:
SPEECH_PROG all
mpargset 0 0
while number($0) < 3
mpargset 0 $%math($0 + 1)%
say Safe loop pass number $0.
endwhile
say The loop ended on its own, well before any safety cap.
~
Substitution
The moment the engine sweeps a line for dollar codes and swaps each for its live value, just before the line runs. Substitution is left to right in a single pass, which has two practical consequences: a value substituted in is never re-scanned for more codes (so player-typed text in $g cannot inject codes into your script), and the longer forms $<, $%, and the quest brackets each consume everything to their closing character. Write $$ when you need a real dollar sign printed. See Dollar Code and Percent; the variables chapter is the full story.
Switch
The many-ways fork of control flow: it takes one value and matches it against a series of cases, running the lines of the first case that matches, or the default lines when none do. Where a ladder of ifs asks many separate questions, a switch asks one question with many answers:
SPEECH_PROG all
switch $%sex($n)%
case male
say A gentleman graces my shop.
case female
say A lady graces my shop.
default
say A customer graces my shop, whatever else they may be.
endswitch
~
Case matching ignores letter case, one case runs per switch, and endswitch closes the whole structure. See Case and Default; the flow chapter has the deeper treatment, including switching on function answers as shown here.
Target
The second party of an event, when there is one: the recipient of the gift, the victim of the spell, the mount being ridden. In script text the target is $t and $T, with pronouns $E, $S, and $M. Many events have no separate target, and then the target is simply the same as the source, which is also true when you fire a block with mudprog <target> test:
GREET_PROG 100
say The source of this event is $N and the target is $T.
~
Fired by test, both blanks fill with your own name, and that is expected. The triggers chapter notes, per trigger, who lands in the target seat. One of the three members of the cast, with Host and Source.
Tilde
The ~ character, alone on a line: the full stop of MUDProg, ending a block. Everything between a header and its tilde is one block; forget the tilde and the next block's header is swallowed into the body above it, producing the classic double-greeting bug that the basics chapter dissects. When any trigger is mysteriously missing from the Triggers line of mudprog <target>, hunt for a missing tilde directly above where it should be. The last block in a script is forgiven a missing tilde, but write it anyway; the habit is cheaper than the bug.
Trigger
The WHEN of a script: a named hook onto one kind of game event, written as the first word of a block's header. GREET_PROG hooks "a player entered my room"; SPEECH_PROG hooks "someone spoke near me"; DEATH_PROG hooks "I am dying". There are about sixty, each catalogued in help mudprog-triggers and indexed one-per-line in the master index below. A script may carry many blocks for many triggers, and several blocks for the same trigger, which run in written order. See Fire, Event, Header, and Argument.
Truthy
The engine's notion of yes when a condition has no comparator. A bare function or value counts as true when it is anything other than: the number zero, empty text, or the words false and no. So if var($i met) is true once the note met holds anything real, and if goldamt($n) is true for anyone carrying a single coin. When you care about a specific value, say so with a comparator instead; bare truthiness is best kept for functions that answer plain yes-or-no, like ispc($n), where it reads exactly as intended.
Variable
A named, stored value: the script's memory. Written onto any object with mpsetvar <obj> <name> <value>, read back in conditions with var(obj name), and pasted into text with the angle form $<obj name>. The variables chapter calls them notes, which is the friendliest way to think of them: a note stuck to an object, readable by any script that can see the object:
SPEECH_PROG all
mpsetvar $i mood cheerful
say Today my mood is $<$i mood>, thank you for asking.
~
Notes on a mob last as long as that copy of the mob; notes on a player save with the character. That difference, per-player memory, counters, cooldowns, and the rest of the craft of remembering live in help mudprog-variables. See also Slot for the temporary cousins and Global for the mud-wide one.
Veto
A CNCLMSG_PROG block, doing its job on the ok-pass: it matches an action before the action happens, cancels it, and runs INSTEAD of it. The action is not merely forbidden, it is replaced; whatever the block prints is all anyone sees, because the normal messages belong to an action that no longer happens. The first rule of vetoes follows directly: always narrate the refusal, or the player experiences dead silence and calls it a bug:
CNCLMSG_PROG ALL
mpecho A ripple of light stops the action before it starts.
~
That block, on an object in a room, refuses every bus action taken nearby; a real veto narrows its header to one message code and usually a keyword mask (CNCLMSG_PROG GET sword). The bus chapter builds vetoes from the ground up, including the sacred rule about broken vetoes failing open. See Ok-Pass, Bus, and Message Code.
Zapper Mask
A header argument that filters WHO may set the trigger off, written as clauses starting with a dash: GREET_PROG -class mage -level 30 fires only for mages of level thirty or higher. Clause types include -class, -race, -level (a bare number means at-or-above, 30-40 a range), -sex, -name, -deity, -player, and -npc; every clause must pass (see Clause). The name comes from CoffeeMUD, where the same syntax "zaps" whole categories of characters out of eligibility, and CoffeeMUD-written masks drop straight in. A mask replaces the chance in a header; it cannot share the line with one. The reference chapter tabulates every clause; the guards deep dive (help mudprog-guards) uses masks constantly, because doormen are what they are for.
Test Yourself
A glossary settles in faster when you make it answer questions. Cover the answers, try each one, then check.
Question one: in the header SPEECH_PROG stew food, what are the two words after the trigger name called, and what do they do? Answer: they are keywords, the trigger's argument; the block fires only when a spoken line contains at least one of them.
Question two: what is the difference between $n and $i? Answer: $n is the source, whoever set the trigger off; $i is the host, the object carrying the script.
Question three: a block ends and the next one begins. What single character must sit between them, and what happens if it is missing? Answer: the tilde; without it, the second block's header is swallowed into the first block's body and its trigger is never registered.
Question four: what is the difference between a veto and an observer? Answer: both live on the message bus, but a veto (CNCLMSG_PROG) runs before the action and replaces it, while an observer (EXECMSG_PROG) runs after the action and cannot stop it.
Question five: your loop is correct but you worry about typos hanging the mud. What two safety nets has the engine already stretched under you? Answer: the loop cap, two thousand passes per loop, and the step budget, a few thousand steps per run; a script that trips either is stopped and logged, and the mud sails on.
Now three writing exercises. Each has a worked solution; write yours before reading it.
Exercise one. A doorkeeper should greet veterans of level five and up differently from newcomers, but everyone should get the same closing welcome. Use if, else, endif, and one line after the branch. One solution:
GREET_PROG 100
if level($n) >= 5
say A seasoned traveler stands at my counter.
else
say Fresh boots and bright eyes stand at my counter.
endif
say Either way, you are welcome here, $N.
~
The closing line sits after the endif, so it runs on both paths. Any level threshold works; the shape is what matters.
Exercise two. A porter should keep a running count of remarks he has overheard, surviving between speeches, and announce the tally each time. You will need a note for the memory, a slot for the arithmetic, and the math function. One solution:
SPEECH_PROG all
if var($i heard) == ''
mpsetvar $i heard 0
endif
mploadvar $i heard 0
mpargset 0 $%math($0 + 1)%
mpsetvar $i heard $0
say My tally of overheard remarks now stands at $0.
~
Walk it through: the first if seeds the note with zero the very first time, because reading a note that was never written yields empty text. Then mploadvar copies the note onto the workbench, math adds one, mpsetvar files the new total back, and the say announces it. Every piece of the variables chapter in seven lines.
Exercise three. Seal a chest so that nothing done near it succeeds, with a private line for the person refused and a public flourish for the room. One solution:
CNCLMSG_PROG ALL
mpechoat $n The chest refuses you utterly. Nothing here will move.
mpecho Runes flare along the seam of the old chest.
~
In live use you would narrow ALL to OPEN, so only opening is sealed and the room's other business proceeds; the wildcard version is the easiest to test. Note the veto rule observed: both audiences are told what happened, so the cancelled action never reads as a bug.
The Master Index
Everything the engine knows, one line apiece. Three tables: triggers, commands, functions. Angle brackets mean "replace with your value"; square brackets mean "optional"; the bracketed note after a trigger description names the kind of object the script usually lives on.
Every trigger is covered in full in help mudprog-triggers, every command in help mudprog-commands, and every function in help mudprog-functions; those are the chapters to open when a one-liner here is not enough. Where a line ends with a name in parentheses, that deep-dive chapter gives the entry a whole treatment of its own; add the mudprog- prefix when asking for help, so (dialogue) means help mudprog-dialogue.
Master Index: Triggers
Arrival and presence:
GREET_PROG <pct|mask> - A player enters the room [mob, room].
(dialogue, dungeons)
ALL_GREET_PROG <pct|mask> - As greet, sneakers included [mob, room].
GROUP_GREET_PROG <pct> - As greet, once per entering group [mob].
ENTRY_PROG <pct> - On a room: a player enters. On a mob:
the mob itself enters a room. (dungeons)
ARRIVE_PROG <pct> - The scripted mob arrives in a room [mob].
EXIT_PROG <pct> - The scripted mob leaves a room [mob].
LOGIN_PROG - A player enters the game; world-wide.
(events)
LOGOFF_PROG - A player leaves the game; world-wide.
(events)
Speech and sound:
SPEECH_PROG <keywords> - Someone speaks a matching line nearby
[mob]. (dialogue)
SPEAK_PROG <keywords> - The scripted mob itself speaks [mob].
(dialogue)
ACT_PROG <keywords> - CoffeeMUD name for heard speech [mob].
MASK_PROG <keywords> - Another CoffeeMUD heard-speech name [mob].
SOCIAL_PROG <pct|keywords> - A social or emote performed nearby [mob].
CHANNEL_PROG [chan words] - Traffic on a chat channel; world-wide.
(events)
CMDFAIL_PROG <pct> - A player's command failed nearby; the
failed line is $g [mob, room].
Idle life and the clock:
ONCE_PROG - Once, when the mob first loads [mob].
RAND_PROG <pct> - Rolls each of the mob's heartbeats [mob].
DELAY_PROG - Fires from the idle pulse after a stored
countdown; mostly for ports [mob].
TIME_PROG <hours> - The mud clock reaches a listed hour.
(events)
DAY_PROG <days> - A new mud day begins; day number in $g.
(events)
AGE_PROG - A player crosses a played-hour mark;
world-wide. (events)
QUEST_TIME_PROG <q> <mins> - A timed quest reaches the listed minutes
remaining. (questcraft)
Combat and dying:
FIGHT_PROG <pct> - Each round while the mob fights [mob].
(combatai)
HITPRCNT_PROG <pct> - A round at or below the header health
percent [mob]. (combatai)
DAMAGE_PROG <pct> - The mob takes damage [mob]. (combatai)
DEATH_PROG - The mob is about to die [mob]. (combatai)
KILL_PROG - The mob has just killed its target [mob].
(combatai)
Items changing hands:
GIVE_PROG <keywords> - The mob is handed an item; item is $o
[mob]. (questcraft)
GIVING_PROG <pct> - Fires on the item being given away [item].
BRIBE_PROG <amount> - The mob is handed coins; amount in $g
[mob].
GET_PROG <pct> - The item is picked up [item, room, mob].
GETTING_PROG <pct> - Fires on the living doing the taking.
DROP_PROG <pct> - The item is dropped [item, room, mob].
DROPPING_PROG <pct> - Fires on the living doing the dropping.
PUT_PROG <pct> - An item goes into a container [item,
container].
PUTTING_PROG <pct> - Fires on the person doing the putting.
WEAR_PROG <pct> - The item is worn or wielded [item].
WEARING_PROG <pct> - Fires on the wearer; the item is $o.
REMOVE_PROG <pct> - The item is taken off or unwielded [item].
CONSUME_PROG <pct> - The food is eaten or drink drunk [item].
Doors, containers, and shops:
OPEN_PROG <pct> - The container or door is opened
[container]. (dungeons)
CLOSE_PROG <pct> - The container or door is closed
[container]. (dungeons)
LOCK_PROG <pct> - It is locked [container]. (dungeons)
UNLOCK_PROG <pct> - It is unlocked [container]. (dungeons)
BUY_PROG <pct> - A player buys from the vendor; item in $o
[vendor]. (shopkeepers)
SELL_PROG <pct> - A player sells to the vendor; item in $o
[vendor]. (shopkeepers)
Magic, following, riding, looking:
CAST_PROG <keywords> - A spell resolves on the scripted mob;
spell name in $g. (combatai)
CASTING_PROG <keywords> - Magic is used nearby; skill name in $g.
FOLLOW_PROG <pct> - Someone starts following the mob [mob].
(companions)
UNFOLLOW_PROG <pct> - Someone stops following the mob [mob].
(companions)
RIDE_PROG <pct> - The scripted mount is mounted; rider is
$n [mount]. (companions)
RIDING_PROG <pct> - Fires on the rider; the mount is $t.
(companions)
LOOK_PROG <pct> - A player looks at the host [mob, item].
LLOOK_PROG <pct> - CoffeeMUD alias of LOOK_PROG [mob, item].
LEVEL_PROG - A player gains a level; new level in $g;
world-wide. (events)
The message bus and text masks:
CNCLMSG_PROG <code> [mask] - The veto: cancels a matching action and
runs instead of it [any object]. (bus)
EXECMSG_PROG [code] [mask] - The observer: runs after a matching
action [any object]. (bus)
IMASK_PROG [text] - The host's own output contains the text;
blank matches everything [mob, item].
(bus)
REGMASK_PROG <regexp> - Text the host sees matches the regular
expression [mob, item]. (bus)
Routines:
FUNCTION_PROG <name> - A named routine; runs only when called
with mpcallfunc or callfunc(). (flow)
Master Index: Commands
Messaging:
mpecho <text> - Narrate to everyone in the room.
mpechoat <who> <text> - Narrate to one person only.
mpechoaround <who> <text> - Narrate to everyone except one person.
mpasound <text> - Narrate into the adjacent rooms.
mpchannel <chan> <text> - Speak onto a chat channel. (events)
mpspeak <text> - The host says the text aloud.
mpllm <text> - Whisper to online staff only.
mplog <text> - Write a line to the server log.
mpprompt <text> - Ask the player a question; the reply
lands in their prompt_answer note.
(dialogue)
mpchoose <text> - Alias of mpprompt. (dialogue)
mpconfirm <text> - Ask yes or no; the reply lands in
confirm_answer as yes or no. (dialogue)
mpaccuse <who> - Publicly accuse; costs the accused
syndicate reputation.
Movement and combat:
mpgoto <room> - Move the host to a room path or "here".
mpat <room> <command> - Run one command as if standing in another
room, then return.
mptransfer <who> [room] - Teleport a target to a room, or to the
host's room when no room is given.
mpwalkto <dir...> - Step the host through listed directions.
mptrackto <dir...> - Alias of mpwalkto.
mpkill <who> - Start a fight with the target. (combatai)
mphit <who> - Land a single attack. (combatai)
mpdamage <who> <amt> [type]- Deal direct damage; types blunt, cutting,
thrusting, pierce, heat, cold, shock,
magic. (combatai)
mpheal <who> <amt> - Restore health. (combatai)
mpcast <spell> [target] - Cast a spell by name. (combatai)
mpcastext <spell> [target] - Alias of mpcast.
mpslay <who> - Kill the target outright. (combatai)
mprejuv [who] - Restore a living to full; defaults to the
host.
mpreset [who] - Rejuv a living, or reset a room.
mpstop [who] - End combat; defaults to the host.
mpflee - Make the host flee the fight. (combatai)
mpforce <who> <command> - Force a target to run a command.
mpbeacon <secs> <command> - Run a script line after a delay. (events)
mpalarm <secs> <command> - Alias of mpbeacon. (events)
mpsleep <seconds> - Pause here; the rest of the script
resumes after the delay. (dialogue)
mpwait <seconds> - Alias of mpsleep. (dialogue)
mppossess <player> <mob> - Let a player take control of a mob.
mpbehave <flag> - Turn a behavior on, such as aggressive.
mpunbehave <flag> - Turn a behavior off.
Loading and the world:
mpmload <path> - Clone an NPC into the room; it becomes $b.
mpoload <path> - Clone an item into the host's inventory;
it becomes $b.
mpoloadroom <path> - Clone an item into the room; it becomes
$b.
mploadquestobj <path> - Clone an item straight to the source.
(questcraft)
mprload <room> - Reset a room by path.
mpjunk <item> - Destroy an item.
mppurge <who> - Destroy a mob (never a player).
mpput <item> <container> - Move an item into a container.
mphide [who] - Turn invisible; defaults to the host.
mpunhide [who] - Become visible again.
mplink <dir> <room> - Add an exit to the host's room.
(dungeons)
mpunlink <dir> - Remove an exit. (dungeons)
mpopen <thing> - The host opens a door or container.
mpclose <thing> - The host closes it.
mplock <thing> - The host locks it.
mpunlock <thing> - The host unlocks it.
mpoloadshop <path> - Stock the vendor's storeroom with an
item. (shopkeepers)
mpmloadshop <path> - Alias of mpoloadshop. (shopkeepers)
mpm2i2m - CoffeeMUD morph command; accepted and
ignored here.
Character and progression:
mpset <who> <key> <value> - Set a property, stat, level, name, or
description.
mpsetinternal ... - Alias of mpset.
mpexp <who> <amount> - Grant or remove experience. (questcraft)
mprpexp <who> <amount> - Alias of mpexp.
mpmoney <who> [type] <amt> - Give or take currency; type defaults to
gold. (shopkeepers)
mptitle <who> <title> - Award a title. (questcraft)
mpfaction <who> <fac> <n> - Shift faction reputation. (questcraft)
mptrains <who> <skill> [n] - Credit skill progress.
mppracs <who> <skill> [n] - Alias of mptrains.
mpaffect <who> <id> [secs] - Apply a condition such as rooted or
poisoned. (combatai)
mpcondition <who> <id> <type> <secs> [mag] [pct]
- Apply a full condition with magnitude
and percent. (combatai)
mpunaffect <who> <id> - Remove a condition. (combatai)
mptattoo <who> <text> - Give a visible tattoo.
mpacctattoo <who> <text> - Alias of mptattoo.
mpachieve <who> <id> - Flag an achievement, with fanfare.
mpplayerclass <who> <cls> - Change a character's class.
mpsetclan <who> <clan> - Set clan membership.
mpsetclandata <who> <k> <v>- Store a clan-related note.
Quests:
mpstartquest <who> <quest> - Begin a quest; the host is the giver.
(questcraft)
mpendquest <who> <quest> - Turn a quest in, or drop it if
unfinished. (questcraft)
mpquestwin <who> <quest> - Complete a quest outright. (questcraft)
mpstepquest <who> <event> - Advance a quest by firing a kill, visit,
or talk event. (questcraft)
mpqset <who> <q> <k> <v> - Write a field on an active quest.
(questcraft)
mpquestpoints <who> <amt> - Award quest points. (questcraft)
Variables and script state:
mpsetvar <obj> <name> <v> - Store a note on any object.
mpsavevar <obj> <name> <v> - Alias of mpsetvar; player notes persist
either way.
mpgset <name> <value> - Set a mud-wide global (write-only to
scripts; see the Global entry).
mploadvar <obj> <name> <slot>
- Copy a note into a scratch slot 0-9.
mpargset <slot> <value> - Write a scratch slot 0-9 by hand.
mpcallfunc <name> [args] - Run a FUNCTION_PROG by name. (flow)
mpscript <line> - Run one script line immediately.
mpunloadscript - The host sheds its own script.
mpnotrigger - Suppress the host's next trigger fire.
mpdisable <TRIGGER> - Switch one of the host's triggers off.
mpenable <TRIGGER> - Switch it back on.
Any line that is none of the above is run by the host as an ordinary game command: say, emote, yell, whisper, socials, wield, wear, remove, cast, and everything else a player could type.
Master Index: Functions
Where an argument reads (who), any object reference works and the host is the default. Functions marked "compat" are accepted for CoffeeMUD compatibility and always answer zero or empty on this mud; they exist so pasted scripts parse cleanly rather than misfire.
Chance, numbers, and text:
rand(pct) - True pct percent of the time.
randnum(n) - A random number from 1 to n.
rand0num(n) - A random number from 0 to n-1.
number(text) - The text as a number.
isodd(n) - True when n is odd.
math(expr) - Simple arithmetic: + - * / and remainder,
left to right.
eval(condition) - True when the condition text passes.
strin(needle haystack) - True when needle appears in haystack.
strcontains(haystack needle) - The same test, arguments reversed.
islike(text mask) - True when text matches a * wildcard mask.
callfunc(name [args]) - Run a FUNCTION_PROG and return its
value. (flow)
Who and what someone is:
isnpc(who) - True for a mob.
ispc(who) - True for a player.
isalive(who) - True for a living, breathing target.
isfight(who) - True while they fight. (combatai)
isimmort(who) - True for staff.
ischarmed(who) - True under a charm effect.
isfollow(who) - True while following someone.
isservant(who) - True for a companion. (companions)
isgroup(who) - True while grouped.
ispkill(who) - True in a player-kill area.
isgood(who) / isevil(who) - Alignment checks, where set.
isneutral(who) - True when no alignment is set.
iscontent(who) - True when not fighting.
isspeaking() - True when the trigger carried spoken
text.
isbirthday(who) - compat; always 0.
isrecall(who) - compat; always 0.
sex(who) - Their gender word.
position(who) - standing, sitting, lying, kneeling,
flying, or swimming.
level(who) - Their level.
class(who) / baseclass(who) - Their class name.
race(who) / racecat(who) - Their race name.
name(who) - Their name.
deity(who) - The deity they worship, if any.
mood(who) - Their mood property, if set.
hitprcnt(who) - Health as a percent of maximum.
(combatai)
exp(who) - Experience points.
questpoints(who) - Quest points. (questcraft)
goldamt(who) - Gold carried (for items, their value).
currency(who) - The currency name; always gold here.
value(item) - An item's worth. (shopkeepers)
stat(who name) - A stat: str, agi, con, int, wis, cha.
gstat(who name) - A stat or, failing that, a property.
trains(who) / pracs(who) - compat; always 0.
ipaddress(who) - A player's connection address.
isable(who skill) - True when they know the skill at all.
expertise(who skill) - Their skill level as a number.
cansee(who whom) - False when the second party is
invisible.
canhear(who) - False while deafened.
affected(who [id]) - True under the named condition; with no
id, the first active condition's name.
(combatai)
isable2(...) - compat; always 0.
Inventory and items:
has(who item) - True when they carry the named item.
hasnum(who item n) - True when they carry at least n of it.
itemcount(who) - How many things they carry.
numitemsmob(who) - Alias of itemcount.
worn(who item) - True when the named item is worn.
wornon(who slot) - True when anything is worn on the slot.
objtype(item) - armor, weapon, container, or item.
isopen(thing) - True for an open door or container.
(dungeons)
islocked(thing) - True for a locked one. (dungeons)
incontainer(item cont) - True when the item sits in the
container.
mobitem(who n) - The name of their nth carried item.
The room and the area:
nummobsroom() / nummobs() - Mobs in the host's room.
numitemsroom() - Items in the host's room.
numpcsroom() - Players in the host's room.
roommob(n) - The name of the nth mob here.
roomitem(n) - The name of the nth item here.
roompc(n) - The name of the nth player here.
numraces() - Distinct races present here.
numracesinarea() - Alias of numraces.
nummobsinarea() - Mobs nearby; approximated to the room.
numpcsarea() - Players in the whole area. (events)
areapc(n) - The name of the nth player in the area.
inroom(who path) - True when they stand in the named room.
ishere(name) - True when the named thing is in the
host's room.
inlocale(who text) - True when their room's path contains the
text.
inarea(who text) - True when they are in the named area.
The clock and the sky:
istime(hour) / ishour(hour) - True at that mud-clock hour. (events)
isday(n) - True on that mud day. (events)
ismonth(name) - True in that mud month. (events)
isyear(n) - True in that mud year. (events)
isseason(name) - True in that season. (events)
ismoon(phase) - True under that moon phase. (events)
isweather(text) - True when the local weather matches.
(events)
datetime(part) - The hour, day, month, or year, by name.
isrlhour(n) / isrlday(n) - True at that real-world hour or day.
isrlmonth(n) / isrlyear(n) - True in that real-world month or year.
weather() - The local weather condition. (events)
season() - The current season. (events)
timeofday() - dawn, day, dusk, or night. (events)
isnight() - True at night. (events)
Memory, quests, and standing:
var(obj name) - The note stored on the object, or empty.
hastag(who prop) - True when the property is set.
questwinner(who quest) - True when they have completed the quest.
(questcraft)
questscripted(who) - True when the target carries a script.
questobj(who item) - True when the item is quest-bound to
them. (questcraft)
qvar(quest key) - A field from the source's active quest.
(questcraft)
questmob(...) / questroom(...) / questarea(...) / isquestmobalive(...)
- compat; always 0.
faction(who name) - Their faction tier name. (questcraft)
factionrep(who name) - Their raw faction number. (questcraft)
hastitle(who [title]) - True when they hold the title, or any
title with no argument. (questcraft)
hastattoo(who) - True when they carry a script tattoo.
hasacctattoo(who) / hastattootime(who) - Aliases of hastattoo.
clan(who) - Their clan name, or empty.
clanrank(who) - Their rank number within it.
clandata(...) / clanqualifies(...) - compat; always empty.
isbehave(who flag) - True when the behavior flag is on.
isname(who text) - True when the text names them.
Shops:
shophas(vendor item) - True when the shop stocks the item.
(shopkeepers)
shopitem(vendor n) - The name of the shop's nth item.
(shopkeepers)
numitemsshop(vendor) - How many items the shop stocks.
(shopkeepers)
Vital signs and the group (Rogue extensions):
hp(who) / maxhp(who) - Health, current and maximum. (combatai)
sp(who) / maxsp(who) - Spell points, current and maximum.
ep(who) / maxep(who) - Stamina, current and maximum.
skill(who name) - A skill level by name.
groupsize(who) - How many stand in their group.
(companions)
explored(...) - compat; always 0.
The Recommended Reading Order
Twenty-four chapters sounds like a mountain; it is a staircase, and most builders are shipping real content by the fourth step. Here is the order the guide was designed to be read in, with a word on why each chapter sits where it does. Take the stages at your own pace, and build something of your own between each one; the guide teaches, but the practice mob does the real instruction.
Stage one, the foundation. Read these six in order:
1. mudprog-basics starts from zero and ends with a
complete tavern keeper. Everything else assumes it.
2. mudprog-triggers is the full WHEN: every trigger,
its argument, and which object it belongs on. Skim it once now,
knowing you will return with questions.
3. mudprog-variables is names and memory: the
dollar codes, the notes, the slots, and the percent form.
4. mudprog-flow is decisions: if, switch, loops,
conditions, and connectors.
5. mudprog-commands is the full WHAT: every mp
command with its arguments and habits.
6. mudprog-functions is the full ASK: every
question a condition or a percent form can pose.
Stage two, practice. Knowledge settles when it is used:
7. mudprog-workbook1 walks you through guided
beginner exercises with worked solutions.
8. mudprog-cookbook is complete working scripts to
copy, adapt, and dissect. Steal freely; that is what it is for.
Stage three, the bus. One idea, one chapter, ten new powers:
9. mudprog-bus teaches cancelling and observing
game actions: vetoes, observers, codes, and scope.
10. mudprog-workbook2 drills the intermediate
craft, bus included, with worked solutions.
Stage four, keeping it working:
11. mudprog-troubleshooting is the repair manual:
what silence means, how to read the Triggers line, and the
diagnosis checklists. Read it once so you know what is in it;
it will save you hours the first bad evening.
12. mudprog-reference is the one-page cheat sheet.
From here on, keep it open in a second window while you write.
Stage five, the deep dives. Eight chapters, each taking one kind of build all the way to professional depth. Read the ones your current project needs, in any order:
19. mudprog-combatai - fight scripts, boss phases,
and dying with style.
20. mudprog-dialogue - conversation, cutscenes,
pacing, and NPC memory.
Stage six, mastery:
21. mudprog-patterns is the idiom library: the
shapes experienced scripters reach for without thinking.
22. mudprog-workbook3 is the advanced workbook;
finish it and you are the senior builder someone else asks.
And the two companions, useful from day one and never finished:
23. mudprog-faq answers one hundred real questions,
askable in any order.
24. mudprog-glossary is this page: the dictionary,
the index, and the map you are holding.
Wherever you are on the staircase, the working habit is the same one the basics chapter taught on day one: write a small block, attach it, test it, watch it, fix it, and only then make it bigger. The engine will not let you break anything; the glossary will not let a word stop you. Go build somebody worth talking to.