THOMAS — The Hypermedia Online Multiplayer Adventure SystemPower User Guide

Everything Mudlet, TinTin++ and MUSHclient give you — triggers, aliases, timers, scripting, speedwalk, split windows, logging — built into the browser, with nothing to install and nothing to configure. Your setup follows your account, not your machine. This page is all of it, with the exact syntax and the traps worth knowing.

More about THOMAS → An illustrated tour of the platform this client is part of — the engine, the world editors and the VINE graph tools the game is built with.

One runner. Triggers, timers, aliases, state rules, speedwalk and command stacking all end at the same macro runner. That means every one of them accepts the same script language — conditionals, loops, variables, calls — and there's exactly one place a runaway loop can live, with one stop that reaches all of it.

1. Triggers

Fire commands when a line of game text matches. Type trigger alone to list what you have.

trigger [@channel] [#group] [once] [Nln] <pattern> = <commands>
trigger off <pattern>        remove that one
trigger on <pattern>         re-enable a disabled one
trigger on|off #group         disable/enable a whole group (never deletes)
trigger on|off all

Worked examples

trigger you're bleeding = bandage
trigger /^(\w+) hits you/ = flee
trigger @loot pipe = take pipe
trigger #combat you're hit = flee
trigger off #combat
trigger once /the safe clicks/ = open safe
trigger 3ln /the door opens.+inside/ = look
trigger you're bleeding = gag;say I saw that
PieceMeaning
/…/Regex. Anything not wrapped in slashes is a case-insensitive substring — the default you can't write wrongly.
$1$9Regex captures, substituted into the command text. An absent group becomes empty, never the literal $3.
@channelScope to a semantic message class — @say, @loot, @combat, @death… (~30 of them). An empty pattern plus a channel matches every line on it.
#groupA label you can switch on and off as a set. Not part of the rule's identity, so regrouping moves a rule rather than duplicating it.
onceFires, then retires itself from storage — it won't come back after a reload.
NlnMulti-line, 1–10. Joins the last N lines and matches across them; regexes compile dot-all so . spans the join. Fires once per window, not once per line.
Why @ and not colour. Other clients let you trigger on ANSI colour. THOMAS has no ANSI — it has ~30 semantic classes on every line, and colour was always a lossy proxy for exactly those. Colour triggers are deliberately never coming.
Three guards you'll eventually meet. A trigger fires a command, the command prints a line, that line fires a trigger — a loop with the server in the middle, and the first trigger most people write is on a combat message. So: lines printed during a trigger chain fire nothing; each trigger has a 400 ms cooldown; and if more than 25 fires happen inside a sliding 10 second window, every trigger is switched off and you're told so. It disables rather than throttles, because a throttled runaway still spams forever with nothing to explain it. Turn them back on with trigger on all once the pattern is fixed.
A broken regex never throws. The row is marked broken and skipped. This code runs inside the log's append path — an exception there would kill the log on every line until a reload.

2. Gagging lines

A trigger whose action is gag hides the matching line.

trigger you're bleeding = gag
trigger @say Vex = gag;say heard you

Gagging is resolved when the trigger is compiled, not while the script runs, and the line is suppressed before it's mounted rather than removed afterwards. That's what makes the find bar, the scrollback cap and Read Aloud all agree without any of them knowing gagging exists.

System messages can never be gagged — that class carries the "all triggers switched off" notice. And gagged lines are still written to .savelog.

3. State triggers — on your own vitals

The half text triggers can't reach: rules on your HP, thirst, sanity and the rest.

on                          list state rules
on hp_pct < 30 = drink stim
on thirst < 20 = drink water
on clear                    (also: on off)

The condition is a full expression (see §8) and is checked whenever your vitals update.

Edge-triggered, never level-triggered. It fires on the transition into the condition and won't fire again until the condition has gone false in between. Level-triggering would fire on every vitals update for as long as you were hurt — which in a fight means drinking your entire inventory.

4. Timers

timer                            list
timer 30s = look                 repeating
timer 500ms = look
timer 5m = say still here
timer after 90s = say I have to go   ONE-SHOT
timer off                        stop them all, and stay off
timer clear                      delete them all

Bare numbers mean seconds. The floor is 1s — anything faster is a command every tick, forever. A one-shot retires itself from storage when it fires, so it doesn't return on the next reload and go off half an hour later with nothing to explain it.

5. Aliases

alias                            list
alias hb = say hello there
alias /^k (.+)$/ = attack $1
alias off k
alias on|off all

Same grammar as triggers. An alias may expand into a ;-chained script, and into client verbs — alias f = macro fight works, because expansion feeds the macro runner.

One pass only. An alias whose output matches another alias does not re-expand. Also: your command history remembers what you typed, not what it expanded to.

Order of processing on submit: alias → speedwalk → stacking → client verb → server.

6. Variables

vars                 list all as  $name = value
vars count           read one
vars count 0         set (the value is an expression)
vars clear

Inside a script, use the segments set <name> <expr> and unset <name>. Setting a variable skips the inter-command pacing and touches no network.

Built-in values win name collisions — $hp always means hit points. An unset variable fails every comparison rather than reading as zero.

7. Macros & the script language

Press + on the smartbar to open the macro manager. A macro is a named script; segments are split on newlines or ; and run top to bottom with a 350 ms stagger between real commands.

SegmentDoes
<command>Any real command, routed exactly as if typed — including client-only verbs.
delay <ms>Pause.
echo <text>A local-only line in your log.
$valueLive player values, interpolated.
if / elseif / else / endifNestable. endif required.
while / endwhileNestable. break and continue hit the innermost loop.
macro <name> [args…]Call another macro. Args bind $1$9; $0 is all of them joined.
set / unsetVariables.
return [expr]Ends this macro; the value lands in $result for the caller.
wait for <pattern> [timeout]Park until a line matches. See §9.

Walk somewhere, wait to arrive, act

gps clone facility
delay 500
auto on
while notin clone facility
  delay 1500
endwhile
drink sink

Kill something, then loot whoever fell

attack enforcer
wait for /^(\w+) collapses/ 30s
loot $1

Available $values

$hp $hp_max $hp_pct $stamina $sta $stamina_pct $sanity $sanity_pct $hunger $thirst $radiation $rad $credits $horniness $body_temp $zone $zone_id $home $home_id

In a macro, write auto on or auto off — never bare auto. Bare auto is a toggle, so its effect depends on state you can't see from inside the script. And $home is a label, not a destination: use the home verb, since gps matches names rather than ids.

Guards

1000 steps per run · 20 levels of nesting · cycle detection on macro calls · a 1 second floor on any while pass that ran no delay of its own · and a server-side token bucket that aborts your macros rather than disconnecting you.

The editor

Check & Fix normalises the script (one segment per line, lowercase keywords, else ifelseif, =<<=, appends a missing endif, re-indents) and then validates — it must pass before Save unlocks. A while with no delay in its body gets a non-blocking amber advisory. The 📖 Guide tab lists your live values, the room's furniture actions, your items and the verb list; clicking a row inserts it.

8. Expressions

Conditions and set values are real expressions, not a fixed comparison shape.

if $hp_pct < 30 and has bandage
if $zone == bishops
if lower($1) contains enforcer
set trips $trips + 1

Precedence, loosest first: or · and · not · comparisons (< <= > >= == != contains starts ends) · + - · * / · unary minus · has lacks in notin · parentheses. Functions: lower upper trim len word num round abs min max.

A bare word is a variable if one exists, otherwise its own text. Comparison is numeric when both sides look numeric and case-insensitive string otherwise; + concatenates when either side isn't a number.

A malformed expression is false, never an error. Which is why the Check button validates well-formedness rather than "did it parse to something" — otherwise a typo'd condition would pass Check and then silently never fire.
Four older prefix forms still work and are matched first, because their arguments are unquoted and may contain spaces: has field bandage, lacks …, in <zone>, notin <zone>. Nothing already written changed meaning.

9. wait for — what makes a script a program

wait for the door opens
wait for /^(\w+) collapses/ 30s
wait for /you're dead/ 500ms

Captures bind into the rest of the script, exactly as trigger captures do. A miss leaves $result empty, so a script can branch on whether it actually landed. Parked scripts are woken before triggers are considered, and every matching waiter wakes, not just the first.

Always bounded. Default 10s, maximum 120s, and there's no unbounded form. A script parked forever is indistinguishable from a hung client, and stop cannot reach a runner that isn't on a step boundary.

Only wait for … is claimed — a bare wait still belongs to the game.

10. Stopping everything

A red ■ Stop chip pins itself to the left of the smartbar whenever anything is running. Or type:

stop

In order: cancels auto-walk, aborts macros, switches timers off and persisted, and gives up every parked wait for. If none of those were active it falls through to the game's own stop. Timers are persisted off deliberately — a stop that leaves the thing which restarts the automation running reads as broken.

11. Key bindings & the smartbar

Each macro can claim one key: F1F9 or a numpad digit. It fires while your caret is in the command box, and is suppressed while a panel owns the keyboard (flight sim, cockpit, hangar, cab, piano) or at the login screen. One key, one macro — claiming releases the previous holder, and the manager shows you who that was.

F10–F12 are deliberately not offered: the browser owns them and intercepting them doesn't reliably win.

The smartbar reads [+] [Tablet] [Inv] then your macro buttons then the room's context verbs. Long-press ~350 ms to lift a button and drag it; the order is remembered per browser.

12. Speedwalk

3n2e     north north north east east
3ne      northeast x3
2s1w

Expanded after aliases and run through the macro runner, so it paces itself room by room.

It must contain at least one digit. The obvious "only direction letters" test matches use — u, s and e are all directions, as are sew, wed and dune. So 3n walks and nnn does not, and a verb people type constantly is left alone. Cap is 40 steps; a zero count is refused.

13. Command stacking

n;n;e                                  three commands
say meet me at the bar; I'll be late   NOT split
emote waves;; then bows                NOT split; sends one ;

Free-text verbs (say, tell, whisper, emote) are never split — the alternative is eating half of somebody's chat message. ;; anywhere on the line escapes the whole line, and each ;; collapses to one ;.

14. Routes & split panes

Send matching lines to their own floating window. This is the "split windows" feature, and a route is just a trigger whose action is "put it over there" — so it reuses that grammar exactly.

route                       list
route @say = Chat           a COPY; the line stays in the log
route @say = Chat only      MOVES it out of the log
route /incoming/ = Combat
route off @say
route clear

Panes are draggable by the title bar, natively resizable, remember their position, hold 400 lines and follow their own tail.

Panes are derived, never authored. There's no "create a pane" command — a pane exists because a rule names it. Closing a pane hides it; the rule survives. Use route off <pattern> to actually stop routing.
The routed copy is written even when the line is gagged — "send chat to its own window and keep it out of here" is the commonest reason to want this at all.

The other half of split windows — an input area that stays put while output scrolls — was never a gap here. The command box doesn't scroll.

15. Highlights, find, transcript

highlight reactor core     toggle one, default colour
hl reactor core            same verb, shorter
highlight                  manager: colours and audible pings
highlight clear
find enforcer              (or Ctrl+F)
.savelog                   write the transcript to a .txt

Highlights are plain substrings, never regex — a regex box in a field with no error surface throws on every line appended. Longest rule wins, one ping per line rather than per match, glyph art is skipped, and changing a rule repaints the log already on your screen. Find marks matches in place; step them with Enter / Shift+Enter.

.savelog reads a 20,000-line session buffer, not the screen. The on-screen scrollback is trimmed as you play, so reading the document would start your transcript wherever trimming had got to — which is exactly the part you saved it to read. The saved file's own header states the limits and how many lines fell off the front. Gagged lines are included.

16. Tab completion & scroll lock

Tab completes the first token against the live verb list (sent by the server once per session from its real registries — never a stale hand-written list) and anything after it against the nouns actually in the room. It fills the common prefix first, then cycles; Shift+Tab walks back; candidates are sorted shortest-first so take is reachable before takeoff. No match does nothing at all — it never invents a word, and it never traps your Tab key.

Scroll back and the log stops following; an "N new lines ↓" chip takes you to the bottom. Entering a command also releases the lock. New lines, room changes and panels do not.

17. Voice input

accessibility voice off
accessibility voice review        fills the box, you press Enter
accessibility voice auto-send

Off by default — a player who never asks for a microphone is never asked for one. Tap the mic button, hold it to talk, or press Ctrl/Cmd+Shift+M from anywhere including inside the command box. (A chord, because the flight sim, the piano and WASD own the bare letter rows.) Firefox has no speech recognition and the button simply doesn't appear there.

The feature isn't the speech API — it's the normalizer. General speech recognition is trained on prose and this game's input is jargon, so "wield rusty pipe" comes back "field rusty pipe" and a bare n comes back "in". Three rules: aggressive on one token, cautious on many (a lone "in" becomes n; the same word inside "put the coat in the locker" is untouched); the vocabulary is live, scraped from what the room and your inventory are actually showing; and never invent — unmatched speech passes through so the server answers Unknown command, because a guess that lands on a real verb runs it.

drop, give, sell, buy, pay, attack, quit and the consumables are never auto-sent, whatever the mode says.

18. Read Aloud

accessibility read off
accessibility read natural        the browser's own voice - recommended
accessibility read in-world       the game's formant synth
accessibility speed slow|normal|brisk

Off by default, and deliberately so: the log is a live region, so a screen reader is already reading it — on by default would speak every line twice in two voices. Natural is recommended over the in-world synth, which fits the fiction but is harder work to listen to for an hour.

It rides the UI audio channel, so muting the television doesn't stop the game being read to you (the master Sound switch still does). Its queue is capped and drops the oldest — a voice minutes behind the game is describing a fight that already finished. Glyph art is never read. Entering a command interrupts it; Escape stops it dead.

19. Accessibility options

accessibility               list everything with current values
access                      alias
a11y                        alias
accessibility text large    by label
accessibility text 19       by value
accessibility font read     a unique prefix works
accessibility reset
VerbSettingOptions
textText SizeSmall / Medium / Large / X-Large … up to 200%
fontTypefaceMonospace / Sans / Readable
motionMotionOn / Off
marksStatus MarksOff / On
readRead AloudOff / Natural / In-world
speedReading SpeedSlow / Normal / Brisk …
voiceVoice InputOff / Review / Auto-send
monoMono AudioOff / On
sfxSound DetailOff / Limited / Full

The tablet's Accessibility page and this verb render from the same table; neither owns it. The verb exists as a verb on purpose: it needs no tablet and prints straight into the log, so the setting stays reachable when the interface is not.

None of these change the game's difficulty. A reaction-time option was built and reverted the same day. The rule left behind: an accessibility option may move the interface freely, but it may not move the odds on a contested outcome.

20. Display modes

displaymode visual
displaymode textgames
displaymode log

Also settable before you log in, from the "Playing with a screen reader?" panel on the auth screen. Unlike everything above, this is server-side and follows your character.

RungMinigamesInfo panelsFor
visualgraphicalgraphicalthe default
textgamescharacter-drawn, real-time, reflex intactstill graphicalwants text, keeps the games
logresolved by dice, not renderedwritten into the scrolling logscreen readers

At the log rung the area pane goes away, room descriptions reach the log, and arrivals are abbreviated — but the classic MUD contract holds: nothing is ever lost, only deferred by one keystroke. An explicit look is always the full description.

21. Your setup follows your account

Macros, triggers, aliases, timers, state rules, highlights, variables and routes are all stored server-side against your character. Log in on another machine and they're there. There's nothing to export and no config file to copy.

Two browsers open at once will clobber each other on the next edit. Conflicts resolve last-writer-wins, deliberately.

Not synced, on purpose: the smartbar drag order, and every device-level preference — volume, theme, text size, and therefore all the accessibility options. A phone and a desktop shouldn't have to agree about volume.

22. What THOMAS deliberately won't do

Colour triggers

There's no ANSI. @channel is the semantic thing colour was standing in for, and a worse duplicate would be the mistake.

Server-side automation

Nothing here reaches the server except commands you could have typed. There's no scripting privilege to grant or revoke.

Unbounded waiting

Every wait for has a timeout. A parked script is indistinguishable from a hung client.

Continuous logging to disk

.savelog writes a session buffer on demand. Streaming needs a Chromium-only API.

Trigger set sharing

No import/export of somebody else's rules.

Arbitrary window management

Panes exist because a route named one. That's the whole model.