1958 lines
47 KiB
Markdown
1958 lines
47 KiB
Markdown
# MUD Scripting Languages Reference
|
|
|
|
this document catalogs real-world MUD scripting DSLs with code examples. the goal is to study how different games let builders create interactive objects, NPCs, and behaviors -- to inform our own DSL design later.
|
|
|
|
for each language, we show real examples from existing MUDs, then implement two test scenarios:
|
|
|
|
**scenario A - talking parrot**: an NPC that listens to player speech, remembers what it hears, and randomly repeats things on a timer.
|
|
|
|
**scenario B - tv set**: an interactive object with a pull knob (on/off toggle) and spin dial (channel selector). when on, it periodically shows messages from the current channel (weather, news, etc).
|
|
|
|
---
|
|
|
|
## 1. MOO Verb Language
|
|
|
|
object-oriented, prototype-based. everything is an object with properties and verbs. `fork` schedules delayed tasks. `:tell()` sends messages. `:announce_all()` broadcasts to the room.
|
|
|
|
### real examples
|
|
|
|
wind-up toy (classic timer pattern):
|
|
```moo
|
|
@verb toy:wind this none none
|
|
@program toy:wind
|
|
if (this.wound < this.maximum)
|
|
this.wound = this.wound + 2;
|
|
player:tell("You wind up the ", this.name, ".");
|
|
player.location:announce(player.name, " winds up the ", this.name, ".");
|
|
endif
|
|
.
|
|
|
|
@verb toy:drop this none none
|
|
@program toy:drop
|
|
pass(@args);
|
|
if (this.wound)
|
|
this.location:announce_all(this.name, " ", this.startup_msg);
|
|
for x in [1..this.wound]
|
|
fork (x * 15)
|
|
this.location:announce_all(this.name, " ", this.continue_msg);
|
|
this.wound = this.wound - 1;
|
|
endfork
|
|
endfor
|
|
endif
|
|
.
|
|
```
|
|
|
|
pet following + command response:
|
|
```moo
|
|
@verb pet:follow any with this
|
|
@program pet:follow
|
|
if ((player in this.masters) || (!this.masters))
|
|
who = $string_utils:match_player(dobjstr);
|
|
if (who == $failed_match)
|
|
player:tell(this.name, " looks confused...");
|
|
else
|
|
this.tofollow = who;
|
|
this.on_track = 1;
|
|
endif
|
|
endif
|
|
.
|
|
```
|
|
|
|
interactive object (kleenex with conditional responses):
|
|
```moo
|
|
@verb kleenex:wipe any with this
|
|
@program kleenex:wipe
|
|
if (dobjstr == "nose")
|
|
player:tell("Okay. You wipe your nose with the ", this.name, ".");
|
|
player.location:announce(player.name, " has the sniffles.");
|
|
else
|
|
player:tell("Okay. You wipe ", dobjstr, " with the ", this.name, ".");
|
|
endif
|
|
.
|
|
```
|
|
|
|
random message generation:
|
|
```moo
|
|
first = {"You discover that", "To your surprise,", "Unbelievably,"}[random(3)];
|
|
last = {" the cat is alive.", " the cat is dead."}[random(2)];
|
|
player:tell(first, last);
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```moo
|
|
@create $thing named parrot
|
|
@property parrot.memories {}
|
|
@property parrot.max_memories 10
|
|
@property parrot.chatty 1
|
|
|
|
@verb parrot:setup_listener this none this
|
|
@program parrot:setup_listener
|
|
" Called when parrot is placed in a room - starts the chatter loop ";
|
|
if (this.chatty)
|
|
fork (random(60) + 30)
|
|
this:chatter();
|
|
endfork
|
|
endif
|
|
.
|
|
|
|
@verb parrot:chatter this none this
|
|
@program parrot:chatter
|
|
" Randomly repeats something it heard ";
|
|
if (this.location && length(this.memories) > 0)
|
|
phrase = this.memories[random(length(this.memories))];
|
|
this.location:announce_all(this.name, " squawks: \"", phrase, "\"");
|
|
endif
|
|
if (this.chatty)
|
|
fork (random(60) + 30)
|
|
this:chatter();
|
|
endfork
|
|
endif
|
|
.
|
|
|
|
@verb parrot:hear this none this
|
|
@program parrot:hear
|
|
" Override of standard :hear verb to capture speech ";
|
|
pass(@args);
|
|
what = args[1];
|
|
who = args[2];
|
|
if (who != this && length(what) > 0)
|
|
this.memories = {@this.memories, what};
|
|
if (length(this.memories) > this.max_memories)
|
|
this.memories = this.memories[2..$];
|
|
endif
|
|
endif
|
|
.
|
|
|
|
@verb parrot:moveto this to any
|
|
@program parrot:moveto
|
|
pass(@args);
|
|
if (valid(this.location))
|
|
this:setup_listener();
|
|
endif
|
|
.
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```moo
|
|
@create $thing named television
|
|
@property tv.is_on 0
|
|
@property tv.channel 1
|
|
@property tv.channels {{"The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."}, {"Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."}, {"Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."}}
|
|
@property tv.broadcasting 0
|
|
|
|
@verb tv:pull this none none
|
|
@program tv:pull
|
|
if (this.is_on)
|
|
this.is_on = 0;
|
|
this.broadcasting = 0;
|
|
player:tell("You pull the knob. The ", this.name, " turns off with a click.");
|
|
player.location:announce(player.name, " turns off the ", this.name, ".");
|
|
else
|
|
this.is_on = 1;
|
|
player:tell("You pull the knob. The ", this.name, " flickers to life.");
|
|
player.location:announce(player.name, " turns on the ", this.name, ".");
|
|
this:start_broadcast();
|
|
endif
|
|
.
|
|
|
|
@verb tv:turn this none none
|
|
@verb tv:spin this none none
|
|
@program tv:turn
|
|
if (!this.is_on)
|
|
player:tell("The ", this.name, " is off.");
|
|
return;
|
|
endif
|
|
this.channel = (this.channel % length(this.channels)) + 1;
|
|
player:tell("You turn the dial. Now on channel ", this.channel, ".");
|
|
player.location:announce(player.name, " changes the channel.");
|
|
.
|
|
|
|
@verb tv:start_broadcast this none this
|
|
@program tv:start_broadcast
|
|
if (!this.broadcasting && this.is_on)
|
|
this.broadcasting = 1;
|
|
fork (15)
|
|
this:broadcast();
|
|
endfork
|
|
endif
|
|
.
|
|
|
|
@verb tv:broadcast this none this
|
|
@program tv:broadcast
|
|
if (this.is_on && valid(this.location))
|
|
msgs = this.channels[this.channel];
|
|
msg = msgs[random(length(msgs))];
|
|
this.location:announce_all("The ", this.name, " shows: ", msg);
|
|
fork (15)
|
|
this:broadcast();
|
|
endfork
|
|
else
|
|
this.broadcasting = 0;
|
|
endif
|
|
.
|
|
|
|
@verb tv:moveto this to any
|
|
@program tv:moveto
|
|
this.broadcasting = 0;
|
|
pass(@args);
|
|
if (this.is_on && valid(this.location))
|
|
this:start_broadcast();
|
|
endif
|
|
.
|
|
```
|
|
|
|
---
|
|
|
|
## 2. LPC (LPMud)
|
|
|
|
C-like, file-based OOP. key hooks: `create()` (init), `init()` (add_action for commands), `heart_beat()` (timer tick). objects inherit from standard library bases.
|
|
|
|
### real examples
|
|
|
|
NPC scripting system (Lima):
|
|
```lpc
|
|
create_script("lunch", ({
|
|
step(SCRIPT_ACTION, "say Well, time for some lunch."),
|
|
step(SCRIPT_WAIT, 5),
|
|
step(SCRIPT_TRIGGER, "The elevator door opens.", "go northwest"),
|
|
step(SCRIPT_DESC, "Harry, leaning against the elevator panel."),
|
|
step(SCRIPT_ACTION, "emote stands up.@@say Guess it's time."),
|
|
}));
|
|
```
|
|
|
|
say-response NPC (Nightmare-Residuum):
|
|
```lpc
|
|
void set_say_response(string match, string response) {
|
|
__SayResponse[match] = response;
|
|
}
|
|
void handle_say_response(string message) {
|
|
foreach (string match, string response in __SayResponse) {
|
|
if (regexp(message, match)) {
|
|
this_object()->handle_command("say " + response);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
periodic random actions (Lima):
|
|
```lpc
|
|
void set_actions(int delay, string *actions) {
|
|
delay_time = delay;
|
|
my_actions = actions;
|
|
start_actions();
|
|
}
|
|
|
|
void actions() {
|
|
if (!this_object()->query_target() && sizeof(my_actions))
|
|
do_game_command(choice(my_actions));
|
|
if (my_actions && query_listeners() && find_call_out("actions") == -1)
|
|
call_out("actions", delay_time);
|
|
}
|
|
```
|
|
|
|
room chatter (Discworld):
|
|
```lpc
|
|
room_chat(({ 120, 240, ({
|
|
"A frog croaks.",
|
|
"A revolting smell drifts from the rug.",
|
|
"You hear distant thunder."
|
|
}) }));
|
|
```
|
|
|
|
parrot race definition (Discworld):
|
|
```lpc
|
|
void setup() {
|
|
set_name("parrot");
|
|
set_long("A vicious, evil-looking parrot.\n");
|
|
add_ac("feather_blow", "blunt", 10);
|
|
}
|
|
void set_unarmed_attacks(object thing) {
|
|
thing->add_attack("beak", 50, ({ number / 2, 2, number / 2 }), "pierce", "unarmed", 0);
|
|
}
|
|
```
|
|
|
|
room with interactive lever/doorbell (Dead Souls):
|
|
```lpc
|
|
void init() {
|
|
::init();
|
|
add_action("aa_ring", "ring");
|
|
}
|
|
int aa_ring(string str) {
|
|
write("DONG!\nYou ring a doorbell!\n");
|
|
this_player()->SetProperty("rung_bell", 1);
|
|
return 1;
|
|
}
|
|
int pre_north() {
|
|
if (!this_player()->GetProperty("rung_bell")) {
|
|
write("Ring the doorbell first!\n");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
```
|
|
|
|
Lima's react scripting mini-language:
|
|
```
|
|
@trigger The elevator door opens.
|
|
go northwest
|
|
@endtrigger
|
|
|
|
@doevery 30 to 60
|
|
say Anyone want to buy some fish?
|
|
@enddoevery
|
|
|
|
@if find gold here
|
|
@action $N picks up the gold
|
|
@move gold $self
|
|
@else
|
|
say No gold here...
|
|
@endif
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```lpc
|
|
// /domains/town/npc/parrot.c
|
|
#include <lib.h>
|
|
|
|
inherit LIVING;
|
|
|
|
string *memories;
|
|
int max_memories;
|
|
int next_chat;
|
|
|
|
void create() {
|
|
::create();
|
|
set_name("parrot");
|
|
set_short("a colorful parrot");
|
|
set_long("A bright tropical parrot with green and red plumage. "
|
|
"It watches you with intelligent eyes.");
|
|
set_gender("neuter");
|
|
set_race("bird");
|
|
|
|
memories = ({});
|
|
max_memories = 10;
|
|
next_chat = 0;
|
|
|
|
set_heart_beat(1);
|
|
}
|
|
|
|
void heart_beat() {
|
|
::heart_beat();
|
|
|
|
if (!environment()) return;
|
|
|
|
next_chat--;
|
|
if (next_chat <= 0 && sizeof(memories) > 0) {
|
|
string phrase = memories[random(sizeof(memories))];
|
|
do_game_command("say " + phrase);
|
|
next_chat = 30 + random(60); // 30-90 seconds
|
|
}
|
|
}
|
|
|
|
void eventReceiveEmit(string message, object who) {
|
|
string speech;
|
|
|
|
// match "Foo says: blah" or "Foo says, \"blah\""
|
|
if (sscanf(message, "%*s says: %s", speech) ||
|
|
sscanf(message, "%*s says, \"%s\"", speech)) {
|
|
if (who != this_object() && speech && strlen(speech) > 0) {
|
|
memories += ({ speech });
|
|
if (sizeof(memories) > max_memories) {
|
|
memories = memories[1..];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```lpc
|
|
// /domains/town/obj/television.c
|
|
#include <lib.h>
|
|
|
|
inherit OBJECT;
|
|
|
|
int is_on;
|
|
int channel;
|
|
mapping channels;
|
|
int next_broadcast;
|
|
|
|
void create() {
|
|
::create();
|
|
set_name("television");
|
|
set_short("a vintage television set");
|
|
set_long("An old black-and-white television with a pull knob and a dial. "
|
|
"The screen is " + (is_on ? "glowing" : "dark") + ".");
|
|
set_mass(5000);
|
|
|
|
is_on = 0;
|
|
channel = 1;
|
|
channels = ([
|
|
1: ({ "The weather is sunny.",
|
|
"Clouds rolling in.",
|
|
"Expect rain tonight." }),
|
|
2: ({ "Breaking news: mayor resigns!",
|
|
"Sports: home team wins!",
|
|
"Traffic is heavy downtown." }),
|
|
3: ({ "Soap opera: dramatic pause...",
|
|
"Commercial: buy now!",
|
|
"Sitcom: laugh track plays." })
|
|
]);
|
|
next_broadcast = 0;
|
|
|
|
set_heart_beat(1);
|
|
}
|
|
|
|
void init() {
|
|
::init();
|
|
add_action("do_pull", "pull");
|
|
add_action("do_turn", "turn");
|
|
add_action("do_turn", "spin");
|
|
}
|
|
|
|
int do_pull(string str) {
|
|
if (!str || !id(str)) return 0;
|
|
|
|
if (is_on) {
|
|
is_on = 0;
|
|
write("You pull the knob. The television turns off with a click.");
|
|
say(this_player()->query_cap_name() + " turns off the television.");
|
|
} else {
|
|
is_on = 1;
|
|
write("You pull the knob. The television flickers to life.");
|
|
say(this_player()->query_cap_name() + " turns on the television.");
|
|
next_broadcast = 15;
|
|
}
|
|
|
|
set_long("An old black-and-white television with a pull knob and a dial. "
|
|
"The screen is " + (is_on ? "glowing" : "dark") + ".");
|
|
return 1;
|
|
}
|
|
|
|
int do_turn(string str) {
|
|
if (!str || !id(str)) return 0;
|
|
|
|
if (!is_on) {
|
|
write("The television is off.");
|
|
return 1;
|
|
}
|
|
|
|
channel = (channel % sizeof(channels)) + 1;
|
|
write("You turn the dial. Now on channel " + channel + ".");
|
|
say(this_player()->query_cap_name() + " changes the channel.");
|
|
return 1;
|
|
}
|
|
|
|
void heart_beat() {
|
|
::heart_beat();
|
|
|
|
if (!is_on || !environment()) return;
|
|
|
|
next_broadcast--;
|
|
if (next_broadcast <= 0) {
|
|
string *msgs = channels[channel];
|
|
string msg = msgs[random(sizeof(msgs))];
|
|
environment()->eventPrint("The television shows: " + msg);
|
|
next_broadcast = 15;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. DG Scripts (CircleMUD/TBAmud)
|
|
|
|
event-triggered scripts with `%variable%` substitutions. trigger types: greet, speech, random, command, fight, give, bribe, etc. scripts use `if/else/end`, `wait`, `switch/case`, `%load%`, `%purge%`.
|
|
|
|
### real examples
|
|
|
|
quest NPC (greet trigger):
|
|
```
|
|
#1
|
|
Quest Offer~
|
|
0 g 100
|
|
~
|
|
if %actor.is_pc% && %direction% == south
|
|
wait 1 sec
|
|
say Can you help me, %actor.name%?
|
|
wait 1 sec
|
|
say An ogre has something of mine.
|
|
say Please, bring me the wings.
|
|
end
|
|
~
|
|
```
|
|
|
|
magic eight ball (object command trigger):
|
|
```
|
|
#6
|
|
Magic Eight Ball~
|
|
1 c 2
|
|
shake~
|
|
switch %random.20%
|
|
case 1
|
|
%send% %actor% Outlook Good
|
|
break
|
|
case 2
|
|
%send% %actor% Outlook Not So Good
|
|
break
|
|
case 3
|
|
%send% %actor% Ask Again Later
|
|
break
|
|
done
|
|
~
|
|
```
|
|
|
|
mynah bird (speech mimicry with variable storage!):
|
|
```
|
|
#1203
|
|
Mynah Bird~
|
|
0 d 100
|
|
*~
|
|
if %actor.is_pc%
|
|
if %c%<20
|
|
eval c 20
|
|
global c
|
|
end
|
|
if !%speech.contains(!)%
|
|
eval c (%c%)+1
|
|
global c
|
|
set %c% %speech%
|
|
global %c%
|
|
eval count %%random.%c%%%
|
|
eval ans %%%count%%%
|
|
wait %random.5%
|
|
say %ans%
|
|
end
|
|
end
|
|
~
|
|
```
|
|
|
|
guard bribe trigger:
|
|
```
|
|
#5
|
|
Guard Bribe~
|
|
0 m 1
|
|
~
|
|
if %amount% >= 10
|
|
if %amount% > 10
|
|
eval change %amount% - 10
|
|
give %change% coin %actor.name%
|
|
end
|
|
say Thank you.
|
|
unlock gateway
|
|
open gateway
|
|
wait 10 sec
|
|
close gateway
|
|
lock gateway
|
|
else
|
|
say only %amount% coins, I require 10.
|
|
give %amount% coin %actor.name%
|
|
end
|
|
~
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
the mynah bird example above IS essentially the parrot scenario! it uses global variables `%c%` (counter) and `%1%`, `%2%`, etc (stored phrases). here's an annotated version:
|
|
|
|
```
|
|
#2001
|
|
Talking Parrot~
|
|
0 d 100
|
|
*~
|
|
* Speech trigger - fires on any speech in room
|
|
* Uses numbered global variables to store memories
|
|
if %actor.is_pc%
|
|
* Initialize memory counter if needed
|
|
if %c%<20
|
|
eval c 20
|
|
global c
|
|
end
|
|
* Only remember non-exclamatory speech
|
|
if !%speech.contains(!)%
|
|
* Increment counter
|
|
eval c (%c%)+1
|
|
global c
|
|
* Store speech in numbered variable (%21%, %22%, etc)
|
|
set %c% %speech%
|
|
global %c%
|
|
* Pick a random memory to repeat
|
|
eval count %%random.%c%%%
|
|
eval ans %%%count%%%
|
|
* Wait random time then repeat it
|
|
wait %random.5%
|
|
say %ans%
|
|
end
|
|
end
|
|
~
|
|
```
|
|
|
|
the clever bit: it uses the counter value itself as a variable name. `set %c% %speech%` expands to something like `set 21 hello there`, which stores "hello there" in variable %21%. then `eval ans %%%count%%%` does double-expansion to retrieve it.
|
|
|
|
### tv implementation
|
|
|
|
```
|
|
#3001
|
|
TV Pull Knob~
|
|
1 c 2
|
|
pull~
|
|
* Command trigger on "pull tv"
|
|
if %cmd.mudcommand% == pull && pull /= tv
|
|
* Toggle state using context variable
|
|
if %is_on%
|
|
eval is_on 0
|
|
context is_on
|
|
%send% %actor% You pull the knob. The television turns off with a click.
|
|
%echoaround% %actor% %actor.name% turns off the television.
|
|
else
|
|
eval is_on 1
|
|
context is_on
|
|
%send% %actor% You pull the knob. The television flickers to life.
|
|
%echoaround% %actor% %actor.name% turns on the television.
|
|
end
|
|
else
|
|
return 0
|
|
halt
|
|
end
|
|
~
|
|
#3002
|
|
TV Turn Dial~
|
|
1 c 2
|
|
turn spin~
|
|
* Command trigger on "turn/spin tv"
|
|
if !%is_on%
|
|
%send% %actor% The television is off.
|
|
halt
|
|
end
|
|
* Cycle through channels 1-3
|
|
eval channel (%channel% %% 3) + 1
|
|
context channel
|
|
%send% %actor% You turn the dial. Now on channel %channel%.
|
|
%echoaround% %actor% %actor.name% changes the channel.
|
|
~
|
|
#3003
|
|
TV Broadcast~
|
|
1 f 100
|
|
~
|
|
* Random trigger - fires periodically
|
|
if !%is_on%
|
|
halt
|
|
end
|
|
* Select message based on channel
|
|
if %channel% == 1
|
|
switch %random.3%
|
|
case 1
|
|
set msg The weather is sunny.
|
|
break
|
|
case 2
|
|
set msg Clouds rolling in.
|
|
break
|
|
case 3
|
|
set msg Expect rain tonight.
|
|
break
|
|
done
|
|
else
|
|
if %channel% == 2
|
|
switch %random.3%
|
|
case 1
|
|
set msg Breaking news: mayor resigns!
|
|
break
|
|
case 2
|
|
set msg Sports: home team wins!
|
|
break
|
|
case 3
|
|
set msg Traffic is heavy downtown.
|
|
break
|
|
done
|
|
else
|
|
switch %random.3%
|
|
case 1
|
|
set msg Soap opera: dramatic pause...
|
|
break
|
|
case 2
|
|
set msg Commercial: buy now!
|
|
break
|
|
case 3
|
|
set msg Sitcom: laugh track plays.
|
|
break
|
|
done
|
|
end
|
|
end
|
|
%echo% The television shows: %msg%
|
|
~
|
|
```
|
|
|
|
---
|
|
|
|
## 4. MUSHcode / Softcode
|
|
|
|
code stored in object attributes. `$pattern:action` creates commands. `^pattern:action` creates listeners. `@listen`/`@ahear` for event reactions. `%N` = actor name, `%0`/`%1` = captures.
|
|
|
|
### real examples
|
|
|
|
basic command trigger:
|
|
```
|
|
&TURNON TV=$turn tv on:@emit %N turns on the TV.
|
|
```
|
|
|
|
echo device (monitor trigger):
|
|
```
|
|
@set EchoBox=MONITOR
|
|
&VA EchoBox=^* says, "*":@emit You hear an echo... %1 %1 %1...
|
|
```
|
|
|
|
listener reaction:
|
|
```
|
|
@listen Guard=*attacks*
|
|
@ahear Guard=@emit The guard steps in and shouts, "No fighting here!"
|
|
```
|
|
|
|
random selection:
|
|
```
|
|
&CMD-+CHECK Game=$+check *:@pemit %#=You rolled [rand(1,100)] on %0.
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```
|
|
@create Parrot
|
|
@set Parrot=MONITOR
|
|
@desc Parrot=A colorful tropical parrot. It watches you with intelligent eyes.
|
|
|
|
* Store memories as a space-separated list
|
|
&MEMORIES Parrot=
|
|
|
|
* Listen to all speech
|
|
&LISTEN Parrot=*
|
|
&AHEAR Parrot=@switch/first [strmatch(%0,*says*)]=1,{@va me=%0}
|
|
|
|
* Capture speech and store it
|
|
&VA Parrot=@switch/first [words(%q<1>)]>=[get(me/MAXMEM)]=1,{@trigger me/FORGET},{&MEMORIES me=[get(me/MEMORIES)] [after(%0,says)]};@trigger me/CHATTER
|
|
|
|
&MAXMEM Parrot=10
|
|
|
|
* Forget oldest memory when full
|
|
&FORGET Parrot=&MEMORIES me=[rest(get(me/MEMORIES))]
|
|
|
|
* Schedule random chatter
|
|
&CHATTER Parrot=@wait [rand(30,90)]=@switch [words(get(me/MEMORIES))]=0,,{@emit [name(me)] squawks: "[extract(get(me/MEMORIES),[rand(1,words(get(me/MEMORIES)))],1)]";@trigger me/CHATTER}
|
|
|
|
* Start chattering when created
|
|
@startup Parrot=@trigger me/CHATTER
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```
|
|
@create Television
|
|
@desc Television=An old black-and-white television with a pull knob and a dial. The screen is [switch(get(me/ISON),1,glowing,dark)].
|
|
|
|
&ISON Television=0
|
|
&CHANNEL Television=1
|
|
|
|
* Channel content
|
|
&CH1 Television=The weather is sunny.|Clouds rolling in.|Expect rain tonight.
|
|
&CH2 Television=Breaking news: mayor resigns!|Sports: home team wins!|Traffic is heavy downtown.
|
|
&CH3 Television=Soap opera: dramatic pause...|Commercial: buy now!|Sitcom: laugh track plays.
|
|
|
|
* Pull command
|
|
&CMD-PULL Television=$pull television:@switch [get(me/ISON)]=1,{&ISON me=0;@oemit %#=%N turns off the television.;@pemit %#=You pull the knob. The television turns off with a click.},{&ISON me=1;@oemit %#=%N turns on the television.;@pemit %#=You pull the knob. The television flickers to life.;@trigger me/BROADCAST}
|
|
|
|
* Turn/spin dial command
|
|
&CMD-TURN Television=$turn television:@switch [get(me/ISON)]=0,{@pemit %#=The television is off.},{&CHANNEL me=[mod(add(get(me/CHANNEL),1),3)];@pemit %#=You turn the dial. Now on channel [get(me/CHANNEL)].;@oemit %#=%N changes the channel.}
|
|
|
|
&CMD-SPIN Television=$spin television:@trigger me/CMD-TURN
|
|
|
|
* Broadcast loop
|
|
&BROADCAST Television=@switch [get(me/ISON)]=0,,{@emit The television shows: [extract(get(me/CH[get(me/CHANNEL)]),[rand(1,3)],1,|)];@wait 15=@trigger me/BROADCAST}
|
|
|
|
* Start broadcasting if on
|
|
@startup Television=@switch [get(me/ISON)]=1,{@trigger me/BROADCAST}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. MUF (Multi-User Forth)
|
|
|
|
stack-based language for TinyMUCK. `: word ... ;` defines words. `me @` pushes current player. `notify` sends messages.
|
|
|
|
### real examples
|
|
|
|
```forth
|
|
: hello-world
|
|
me @ "Hello, world!" notify
|
|
;
|
|
|
|
: whoami
|
|
me @
|
|
"Your name is " me @ name strcat notify
|
|
;
|
|
|
|
: roll-die
|
|
me @
|
|
"You rolled a "
|
|
random 6 % 1 +
|
|
intostr strcat notify
|
|
;
|
|
```
|
|
|
|
### parrot implementation sketch
|
|
|
|
```forth
|
|
$def MAXMEM 10
|
|
|
|
: parrot-remember ( str -- )
|
|
me @ location contents ( str loc-contents )
|
|
begin
|
|
dup while
|
|
dup "parrot" stringpfx if
|
|
dup "memories" array_getitem ( str parrot memories )
|
|
swap rot ( memories parrot str )
|
|
swap "memories" 3 pick ( memories str "memories" parrot )
|
|
3 pick array_appenditem ( memories parrot )
|
|
"memories" rot getpropstr ( memories )
|
|
array_count MAXMEM > if
|
|
1 array_delitem ( trim oldest )
|
|
then
|
|
"memories" swap setprop
|
|
then
|
|
next
|
|
repeat
|
|
pop pop
|
|
;
|
|
|
|
: parrot-chatter ( -- )
|
|
me @ "memories" array_getitem ( memories )
|
|
dup array_count if
|
|
random over array_count % 1 + ( memories idx )
|
|
array_getitem ( phrase )
|
|
me @ location
|
|
"The parrot squawks: \"" 3 pick strcat "\"" strcat
|
|
notify_except
|
|
else
|
|
pop
|
|
then
|
|
|
|
30 60 random + sleep
|
|
parrot-chatter ( recursive loop )
|
|
;
|
|
|
|
: parrot-listen
|
|
begin
|
|
read ( str )
|
|
dup "says" instr if
|
|
parrot-remember
|
|
else
|
|
pop
|
|
then
|
|
repeat
|
|
;
|
|
```
|
|
|
|
(note: this is illustrative, not production-ready. MUF's event model would need more plumbing for real speech interception.)
|
|
|
|
---
|
|
|
|
## 6. DIL (DikuMUD Intermediate Language)
|
|
|
|
C-like compiled bytecode language. `wait(SFB_*, condition)` blocks for events. `heartbeat` for timers. labels + goto for loops. `exec()` runs game commands. `self` = attached object, `activator` = triggering player.
|
|
|
|
### real examples
|
|
|
|
NPC that greets players:
|
|
```dil
|
|
dilbegin hi();
|
|
var
|
|
code
|
|
{
|
|
:start:
|
|
wait(SFB_DONE, command(CMD_AUTO_ENTER)
|
|
and activator.type == UNIT_ST_PC);
|
|
exec("say Hello, " + activator.name + "!", self);
|
|
goto start;
|
|
}
|
|
dilend
|
|
```
|
|
|
|
guard daily routine with pathfinding:
|
|
```dil
|
|
dilbegin guardroutine(guardloc : string, dayguard : integer);
|
|
var
|
|
sch : intlist;
|
|
code
|
|
{
|
|
heartbeat := WAIT_SEC*5;
|
|
:start:
|
|
pause;
|
|
if (mudhour == sch.[0])
|
|
{
|
|
exec("wake", self);
|
|
exec("say Time to get going!", self);
|
|
walkto(guardloc);
|
|
}
|
|
goto start;
|
|
}
|
|
dilend
|
|
```
|
|
|
|
speaking gem:
|
|
```dil
|
|
dilbegin gem_speak();
|
|
code
|
|
{
|
|
heartbeat := 90 * PULSE_SEC;
|
|
:start:
|
|
wait(SFB_TICK, TRUE
|
|
and self.outside.type == UNIT_ST_PC
|
|
and self.equip);
|
|
act("$1n says 'You look lovely today!'",
|
|
A_ALWAYS, self.outside, self, null, TO_CHAR);
|
|
goto start;
|
|
}
|
|
dilend
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```dil
|
|
dilbegin parrot();
|
|
var
|
|
memories : stringlist;
|
|
maxmem : integer;
|
|
count : integer;
|
|
phrase : string;
|
|
u : unitptr;
|
|
code
|
|
{
|
|
maxmem := 10;
|
|
memories := {};
|
|
heartbeat := PULSE_SEC * 5;
|
|
|
|
:start:
|
|
wait(SFB_COM, command("say") and activator.type == UNIT_ST_PC);
|
|
|
|
* Capture what was said
|
|
phrase := argument;
|
|
if (length(phrase) > 0)
|
|
{
|
|
addstring(memories, phrase);
|
|
count := length(memories);
|
|
if (count > maxmem)
|
|
{
|
|
memories := delstr(memories, 0);
|
|
}
|
|
}
|
|
|
|
goto chatter_wait;
|
|
|
|
:chatter_wait:
|
|
wait(SFB_TICK, TRUE);
|
|
pause;
|
|
if (rnd(1, 100) < 15 and length(memories) > 0)
|
|
{
|
|
goto chatter;
|
|
}
|
|
goto chatter_wait;
|
|
|
|
:chatter:
|
|
count := length(memories);
|
|
if (count > 0)
|
|
{
|
|
phrase := memories.[rnd(0, count - 1)];
|
|
exec("say " + phrase, self);
|
|
}
|
|
goto chatter_wait;
|
|
}
|
|
dilend
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```dil
|
|
dilbegin television();
|
|
var
|
|
is_on : integer;
|
|
channel : integer;
|
|
ch1 : stringlist;
|
|
ch2 : stringlist;
|
|
ch3 : stringlist;
|
|
current : stringlist;
|
|
msg : string;
|
|
code
|
|
{
|
|
is_on := 0;
|
|
channel := 1;
|
|
|
|
ch1 := {"The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."};
|
|
ch2 := {"Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."};
|
|
ch3 := {"Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."};
|
|
|
|
heartbeat := PULSE_SEC * 15;
|
|
|
|
:start:
|
|
wait(SFB_CMD, (command("pull") or command("turn") or command("spin"))
|
|
and ("television" in argument or "tv" in argument));
|
|
|
|
if (command("pull"))
|
|
{
|
|
if (is_on == 1)
|
|
{
|
|
is_on := 0;
|
|
act("You pull the knob. The television turns off with a click.",
|
|
A_ALWAYS, activator, self, null, TO_CHAR);
|
|
act("$1n turns off the television.",
|
|
A_ALWAYS, activator, self, null, TO_REST);
|
|
}
|
|
else
|
|
{
|
|
is_on := 1;
|
|
act("You pull the knob. The television flickers to life.",
|
|
A_ALWAYS, activator, self, null, TO_CHAR);
|
|
act("$1n turns on the television.",
|
|
A_ALWAYS, activator, self, null, TO_REST);
|
|
}
|
|
}
|
|
|
|
if ((command("turn") or command("spin")) and is_on == 1)
|
|
{
|
|
channel := (channel % 3) + 1;
|
|
act("You turn the dial. Now on channel " + itoa(channel) + ".",
|
|
A_ALWAYS, activator, self, null, TO_CHAR);
|
|
act("$1n changes the channel.",
|
|
A_ALWAYS, activator, self, null, TO_REST);
|
|
}
|
|
|
|
goto broadcast_check;
|
|
|
|
:broadcast_check:
|
|
wait(SFB_TICK, TRUE);
|
|
pause;
|
|
if (is_on == 0)
|
|
{
|
|
goto start;
|
|
}
|
|
|
|
* Select current channel content
|
|
if (channel == 1)
|
|
{
|
|
current := ch1;
|
|
}
|
|
else if (channel == 2)
|
|
{
|
|
current := ch2;
|
|
}
|
|
else
|
|
{
|
|
current := ch3;
|
|
}
|
|
|
|
msg := current.[rnd(0, length(current) - 1)];
|
|
act("The television shows: " + msg,
|
|
A_ALWAYS, self, null, null, TO_ALL);
|
|
|
|
goto broadcast_check;
|
|
}
|
|
dilend
|
|
```
|
|
|
|
---
|
|
|
|
## 7. MobProg (Merc/ROM/SMAUG family)
|
|
|
|
inline scripts on mob definitions. trigger types: `greet_prog`, `speech_prog`, `rand_prog`, `fight_prog`, `give_prog`, etc. `$n` = actor, `$i` = self. `if/else/endif` conditionals.
|
|
|
|
### real examples
|
|
|
|
wizard shop greeting:
|
|
```
|
|
>greet_prog 50~
|
|
if ispc($n)
|
|
if rand(45)
|
|
say How can I help you? Rings? Wands?
|
|
grin
|
|
else
|
|
smile $n
|
|
say Hello $n, what would you like today?
|
|
endif
|
|
endif
|
|
~
|
|
```
|
|
|
|
random idle chatter:
|
|
```
|
|
>rand_prog 5~
|
|
if rand(51)
|
|
say I know I put that bottle on the 53rd shelf.
|
|
grumble
|
|
else
|
|
grumble
|
|
say Adventurers always bothering me.
|
|
endif
|
|
~
|
|
```
|
|
|
|
temple guardian (exit trigger):
|
|
```
|
|
>exit_prog 100~
|
|
if isgood $n
|
|
say Hail!
|
|
if carries $n holy
|
|
mob transfer $n 2301
|
|
else
|
|
say Get a holy symbol first.
|
|
endif
|
|
else
|
|
curse $n
|
|
say Get lost, scum!
|
|
endif
|
|
~
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```
|
|
>speech_prog 100~
|
|
* Store speech in mob's memory using mpvariable
|
|
* MobProgs don't have dynamic arrays, so we use a circular buffer
|
|
if ispc($n)
|
|
if $speech != '!'
|
|
* Increment counter
|
|
if varcmp $i c == ''
|
|
mpvariable c 1
|
|
else
|
|
mpvariable c $c + 1
|
|
endif
|
|
|
|
* Store in slot (mod 10)
|
|
eval slot $c % 10
|
|
mpvariable mem$slot $speech
|
|
|
|
* Max stored
|
|
if $c > 10
|
|
mpvariable maxmem 10
|
|
else
|
|
mpvariable maxmem $c
|
|
endif
|
|
endif
|
|
endif
|
|
~
|
|
|
|
>rand_prog 10~
|
|
* Randomly repeat stored phrases
|
|
if rand(50)
|
|
if varcmp $i maxmem > 0
|
|
eval slot rnd($maxmem)
|
|
eval phrase $mem$slot
|
|
if $phrase != ''
|
|
say $phrase
|
|
endif
|
|
endif
|
|
endif
|
|
~
|
|
```
|
|
|
|
(note: variable handling in MobProgs is implementation-specific. some versions have `mpvariable`, others don't support dynamic storage at all.)
|
|
|
|
---
|
|
|
|
## 8. CoffeeMUD Scripts
|
|
|
|
MobProg-inspired with 60+ event types and 80+ commands. also supports embedded JavaScript via `<SCRIPT>` blocks.
|
|
|
|
### real examples
|
|
|
|
trader NPC:
|
|
```
|
|
GREET_PROG 75
|
|
say Welcome $n! I am a strange trader!
|
|
~
|
|
|
|
FIGHT_PROG 99
|
|
IF RAND(15)
|
|
say Think yer TOUGH eh?
|
|
ELSE
|
|
IF RAND(15)
|
|
say Take THAT!
|
|
ENDIF
|
|
ENDIF
|
|
~
|
|
|
|
GIVE_PROG all
|
|
IF GOLDAMT($o < 100)
|
|
say You can do better than THAT.
|
|
drop $o
|
|
ELSE
|
|
say THAT's what I LIKE!
|
|
MPOLOAD a nice tunic
|
|
give "a nice tunic" "$n"
|
|
ENDIF
|
|
~
|
|
```
|
|
|
|
speech-reactive NPC:
|
|
```
|
|
SPEECH_PROG weapon sword mace
|
|
IF WORN($n sword)
|
|
say You are wielding a sword!
|
|
ELSE
|
|
IF HAS($n sword)
|
|
say You have a sword!
|
|
ENDIF
|
|
ENDIF
|
|
~
|
|
```
|
|
|
|
embedded JavaScript:
|
|
```
|
|
GREET_PROG 100
|
|
<SCRIPT>
|
|
if(ISPC("$n"))
|
|
{
|
|
MPECHO("The ground shakes!");
|
|
}
|
|
</SCRIPT>
|
|
~
|
|
```
|
|
|
|
### parrot implementation (pure CoffeeMUD scripting)
|
|
|
|
```
|
|
SPEECH_PROG *
|
|
IF ISPC($n)
|
|
IF !STRCONTAINS($t !)
|
|
MPSETVAR $i mem_count $<$i.mem_count>+1
|
|
MPSETVAR $i mem_$<$i.mem_count> $t
|
|
IF $<$i.mem_count> > 10
|
|
MPSETVAR $i mem_count 10
|
|
ENDIF
|
|
ENDIF
|
|
ENDIF
|
|
~
|
|
|
|
RAND_PROG 10
|
|
IF RAND(50)
|
|
IF $<$i.mem_count> > 0
|
|
MPSETVAR I slot RAND(1 $<$i.mem_count>)
|
|
say $<$i.mem_$<I.slot>>
|
|
ENDIF
|
|
ENDIF
|
|
~
|
|
```
|
|
|
|
### tv implementation (JavaScript hybrid)
|
|
|
|
```
|
|
LOAD_PROG 100
|
|
<SCRIPT>
|
|
MPSETVAR("$i", "is_on", "0");
|
|
MPSETVAR("$i", "channel", "1");
|
|
MPSETVAR("$i", "ch1_1", "The weather is sunny.");
|
|
MPSETVAR("$i", "ch1_2", "Clouds rolling in.");
|
|
MPSETVAR("$i", "ch1_3", "Expect rain tonight.");
|
|
MPSETVAR("$i", "ch2_1", "Breaking news: mayor resigns!");
|
|
MPSETVAR("$i", "ch2_2", "Sports: home team wins!");
|
|
MPSETVAR("$i", "ch2_3", "Traffic is heavy downtown.");
|
|
MPSETVAR("$i", "ch3_1", "Soap opera: dramatic pause...");
|
|
MPSETVAR("$i", "ch3_2", "Commercial: buy now!");
|
|
MPSETVAR("$i", "ch3_3", "Sitcom: laugh track plays.");
|
|
</SCRIPT>
|
|
~
|
|
|
|
FUNCTION_PROG pull_trigger
|
|
IF $<$i.is_on> == 1
|
|
MPSETVAR $i is_on 0
|
|
MPECHO $n turns off the television.
|
|
TELL $n You pull the knob. The television turns off with a click.
|
|
ELSE
|
|
MPSETVAR $i is_on 1
|
|
MPECHO $n turns on the television.
|
|
TELL $n You pull the knob. The television flickers to life.
|
|
ENDIF
|
|
~
|
|
|
|
FUNCTION_PROG turn_trigger
|
|
IF $<$i.is_on> == 0
|
|
TELL $n The television is off.
|
|
ELSE
|
|
MPSETVAR $i channel ($<$i.channel> % 3) + 1
|
|
TELL $n You turn the dial. Now on channel $<$i.channel>.
|
|
MPECHO $n changes the channel.
|
|
ENDIF
|
|
~
|
|
|
|
RAND_PROG 20
|
|
IF $<$i.is_on> == 1
|
|
<SCRIPT>
|
|
var ch = GSTAT("$i", "channel");
|
|
var idx = RAND(1, 3);
|
|
var msg = GSTAT("$i", "ch" + ch + "_" + idx);
|
|
MPECHO("The television shows: " + msg);
|
|
</SCRIPT>
|
|
ENDIF
|
|
~
|
|
```
|
|
|
|
---
|
|
|
|
## 9. room.js (JavaScript MOO)
|
|
|
|
Node.js MOO-like system. verbs are `.js` files on the filesystem per object. dispatch signature: `({ player, dobj, iobj, verbstr, argstr, dobjstr, prepstr, iobjstr })`. `run.in()` for timers.
|
|
|
|
### real examples
|
|
|
|
guitar with song playback:
|
|
```javascript
|
|
function play({ player }) {
|
|
if (this.location !== player) {
|
|
player.tell("You don't have that.");
|
|
return;
|
|
}
|
|
this.stropheIndex = 1;
|
|
this.playing = player;
|
|
this.playEmote();
|
|
}
|
|
|
|
function playStrophe() {
|
|
const song = this.song.trim().split(/\n\n/);
|
|
let strophe = song[this.stropheIndex - 1];
|
|
this.playing.location.announce(() => strophe, this.playing);
|
|
this.stropheIndex++;
|
|
if (this.stropheIndex > song.length) {
|
|
this.stropheIndex = 0;
|
|
this.playing = null;
|
|
return;
|
|
}
|
|
run.in(this.id + '.playStrophe()', 4000);
|
|
}
|
|
```
|
|
|
|
mouse that flees on examine:
|
|
```javascript
|
|
function onExamine(player) {
|
|
player.tell(`The ${this.name} starts panicking!`);
|
|
const exitDirs = Object.keys(this.location.exits);
|
|
if (exitDirs.length === 0) return;
|
|
const picked = exitDirs[Math.floor(Math.random() * exitDirs.length)];
|
|
this.location.goDirection({ player: this, argstr: picked });
|
|
}
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```javascript
|
|
// parrot.js
|
|
function onCreate() {
|
|
this.memories = [];
|
|
this.maxMemories = 10;
|
|
this.chatting = false;
|
|
}
|
|
|
|
function onHear({ speaker, message }) {
|
|
// listen to other people's speech
|
|
if (speaker === this) return;
|
|
if (message.length === 0) return;
|
|
|
|
this.memories.push(message);
|
|
if (this.memories.length > this.maxMemories) {
|
|
this.memories.shift();
|
|
}
|
|
|
|
if (!this.chatting) {
|
|
this.startChattering();
|
|
}
|
|
}
|
|
|
|
function startChattering() {
|
|
this.chatting = true;
|
|
this.chatter();
|
|
}
|
|
|
|
function chatter() {
|
|
if (!this.location) {
|
|
this.chatting = false;
|
|
return;
|
|
}
|
|
|
|
if (this.memories.length > 0) {
|
|
const phrase = this.memories[Math.floor(Math.random() * this.memories.length)];
|
|
this.location.announce(() => `${this.name} squawks: "${phrase}"`);
|
|
}
|
|
|
|
const delay = 30000 + Math.random() * 60000; // 30-90 seconds
|
|
run.in(`${this.id}.chatter()`, delay);
|
|
}
|
|
|
|
function onMove() {
|
|
// restart chattering in new location
|
|
if (this.chatting && this.location) {
|
|
this.chatter();
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
onCreate,
|
|
onHear,
|
|
startChattering,
|
|
chatter,
|
|
onMove
|
|
};
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```javascript
|
|
// television.js
|
|
function onCreate() {
|
|
this.isOn = false;
|
|
this.channel = 1;
|
|
this.channels = [
|
|
["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."],
|
|
["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."],
|
|
["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
|
|
];
|
|
this.broadcasting = false;
|
|
}
|
|
|
|
function pull({ player }) {
|
|
if (this.isOn) {
|
|
this.isOn = false;
|
|
this.broadcasting = false;
|
|
player.tell("You pull the knob. The television turns off with a click.");
|
|
this.location.announce(() => `${player.name} turns off the television.`, player);
|
|
} else {
|
|
this.isOn = true;
|
|
player.tell("You pull the knob. The television flickers to life.");
|
|
this.location.announce(() => `${player.name} turns on the television.`, player);
|
|
this.startBroadcast();
|
|
}
|
|
}
|
|
|
|
function turn({ player }) {
|
|
if (!this.isOn) {
|
|
player.tell("The television is off.");
|
|
return;
|
|
}
|
|
this.channel = (this.channel % this.channels.length) + 1;
|
|
player.tell(`You turn the dial. Now on channel ${this.channel}.`);
|
|
this.location.announce(() => `${player.name} changes the channel.`, player);
|
|
}
|
|
|
|
function spin({ player }) {
|
|
this.turn({ player });
|
|
}
|
|
|
|
function startBroadcast() {
|
|
if (this.broadcasting || !this.isOn) return;
|
|
this.broadcasting = true;
|
|
this.broadcast();
|
|
}
|
|
|
|
function broadcast() {
|
|
if (!this.isOn || !this.location) {
|
|
this.broadcasting = false;
|
|
return;
|
|
}
|
|
|
|
const msgs = this.channels[this.channel - 1];
|
|
const msg = msgs[Math.floor(Math.random() * msgs.length)];
|
|
this.location.announce(() => `The television shows: ${msg}`);
|
|
|
|
run.in(`${this.id}.broadcast()`, 15000);
|
|
}
|
|
|
|
function onMove() {
|
|
this.broadcasting = false;
|
|
if (this.isOn && this.location) {
|
|
this.startBroadcast();
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
onCreate,
|
|
pull,
|
|
turn,
|
|
spin,
|
|
startBroadcast,
|
|
broadcast,
|
|
onMove
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Ranvier (YAML + JavaScript)
|
|
|
|
data-driven: entities defined in YAML, behaviors in JS modules. events connect them.
|
|
|
|
### real examples
|
|
|
|
NPC definition:
|
|
```yaml
|
|
- id: 2
|
|
name: "Rat"
|
|
keywords: ["rat"]
|
|
description: "A fat rat scurries about."
|
|
script: "1-rat"
|
|
items: ["limbo:sliceofcheese"]
|
|
quests: ["limbo:onecheeseplease"]
|
|
attributes:
|
|
health: 100
|
|
damage: "1-7"
|
|
behaviors:
|
|
lootable:
|
|
table:
|
|
pools:
|
|
- "limbo:junk"
|
|
```
|
|
|
|
script (unique per-NPC):
|
|
```javascript
|
|
module.exports = {
|
|
listeners: {
|
|
playerEnter: state => function(player) {
|
|
this.emit('combat', player);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```yaml
|
|
# areas/town/npcs.yml
|
|
- id: parrot
|
|
name: "colorful parrot"
|
|
keywords: ["parrot", "bird"]
|
|
description: "A bright tropical parrot with green and red plumage. It watches you with intelligent eyes."
|
|
script: "town-parrot"
|
|
behaviors:
|
|
parrot-mimic:
|
|
maxMemories: 10
|
|
```
|
|
|
|
```javascript
|
|
// behaviors/npc/parrot-mimic.js
|
|
module.exports = {
|
|
listeners: {
|
|
spawn: state => function() {
|
|
this.memories = [];
|
|
this.maxMemories = this.getBehavior('parrot-mimic').maxMemories || 10;
|
|
this.startChattering();
|
|
},
|
|
|
|
playerSay: state => function(player, message) {
|
|
if (message.length === 0) return;
|
|
|
|
this.memories.push(message);
|
|
if (this.memories.length > this.maxMemories) {
|
|
this.memories.shift();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add to NPC prototype
|
|
NPC.prototype.startChattering = function() {
|
|
const chatter = () => {
|
|
if (!this.room || this.memories.length === 0) {
|
|
setTimeout(chatter, 30000 + Math.random() * 60000);
|
|
return;
|
|
}
|
|
|
|
const phrase = this.memories[Math.floor(Math.random() * this.memories.length)];
|
|
B.sayAt(this.room, `${this.name} squawks: "${phrase}"`);
|
|
|
|
setTimeout(chatter, 30000 + Math.random() * 60000);
|
|
};
|
|
|
|
chatter();
|
|
};
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```yaml
|
|
# areas/town/items.yml
|
|
- id: television
|
|
name: "vintage television"
|
|
keywords: ["television", "tv"]
|
|
description: "An old black-and-white television with a pull knob and a dial."
|
|
script: "town-television"
|
|
behaviors:
|
|
tv-broadcast:
|
|
channels:
|
|
- ["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."]
|
|
- ["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."]
|
|
- ["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
|
|
metadata:
|
|
isOn: false
|
|
channel: 0
|
|
```
|
|
|
|
```javascript
|
|
// commands/pull.js
|
|
module.exports = {
|
|
usage: 'pull <item>',
|
|
command: state => (args, player) => {
|
|
const item = state.ItemManager.parseDot(args, player.room.items);
|
|
if (!item) {
|
|
return B.sayAt(player, "Pull what?");
|
|
}
|
|
|
|
if (item.hasKeyword('television')) {
|
|
const isOn = item.getMeta('isOn') || false;
|
|
if (isOn) {
|
|
item.setMeta('isOn', false);
|
|
B.sayAt(player, "You pull the knob. The television turns off with a click.");
|
|
B.sayAtExcept(player.room, `${player.name} turns off the television.`, player);
|
|
} else {
|
|
item.setMeta('isOn', true);
|
|
B.sayAt(player, "You pull the knob. The television flickers to life.");
|
|
B.sayAtExcept(player.room, `${player.name} turns on the television.`, player);
|
|
item.emit('startBroadcast');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// commands/turn.js
|
|
module.exports = {
|
|
aliases: ['spin'],
|
|
usage: 'turn <item>',
|
|
command: state => (args, player) => {
|
|
const item = state.ItemManager.parseDot(args, player.room.items);
|
|
if (!item) {
|
|
return B.sayAt(player, "Turn what?");
|
|
}
|
|
|
|
if (item.hasKeyword('television')) {
|
|
if (!item.getMeta('isOn')) {
|
|
return B.sayAt(player, "The television is off.");
|
|
}
|
|
|
|
const behavior = item.getBehavior('tv-broadcast');
|
|
const numChannels = behavior.channels.length;
|
|
const currentChannel = item.getMeta('channel') || 0;
|
|
const newChannel = (currentChannel + 1) % numChannels;
|
|
item.setMeta('channel', newChannel);
|
|
|
|
B.sayAt(player, `You turn the dial. Now on channel ${newChannel + 1}.`);
|
|
B.sayAtExcept(player.room, `${player.name} changes the channel.`, player);
|
|
}
|
|
}
|
|
};
|
|
|
|
// behaviors/item/tv-broadcast.js
|
|
module.exports = {
|
|
listeners: {
|
|
spawn: state => function() {
|
|
if (this.getMeta('isOn')) {
|
|
this.startBroadcast();
|
|
}
|
|
},
|
|
|
|
startBroadcast: state => function() {
|
|
const broadcast = () => {
|
|
if (!this.getMeta('isOn') || !this.room) {
|
|
return;
|
|
}
|
|
|
|
const behavior = this.getBehavior('tv-broadcast');
|
|
const channel = this.getMeta('channel') || 0;
|
|
const messages = behavior.channels[channel];
|
|
const msg = messages[Math.floor(Math.random() * messages.length)];
|
|
|
|
B.sayAt(this.room, `The television shows: ${msg}`);
|
|
|
|
setTimeout(broadcast, 15000);
|
|
};
|
|
|
|
broadcast();
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Evennia (Python Typeclasses)
|
|
|
|
Python classes with persistence hooks. `at_object_creation()` runs once. `db.*` persists. Scripts for timed behavior.
|
|
|
|
### real examples
|
|
|
|
NPC reacting to player entry:
|
|
```python
|
|
class NPC(Character):
|
|
def at_char_entered(self, character, **kwargs):
|
|
if self.db.is_aggressive:
|
|
self.execute_cmd(f"say Graaah! Die, {character}!")
|
|
else:
|
|
self.execute_cmd(f"say Greetings, {character}!")
|
|
```
|
|
|
|
room that notifies NPCs:
|
|
```python
|
|
class Room(DefaultRoom):
|
|
def at_object_receive(self, arriving_obj, source_location, **kwargs):
|
|
if arriving_obj.account:
|
|
for item in self.contents:
|
|
if utils.inherits_from(item, "typeclasses.npcs.NPC"):
|
|
item.at_char_entered(arriving_obj, **kwargs)
|
|
```
|
|
|
|
### parrot implementation
|
|
|
|
```python
|
|
from evennia import DefaultCharacter
|
|
from evennia.utils import delay
|
|
import random
|
|
|
|
class Parrot(DefaultCharacter):
|
|
"""
|
|
A parrot that listens to speech and randomly repeats it.
|
|
"""
|
|
|
|
def at_object_creation(self):
|
|
"""Called once when object is first created."""
|
|
super().at_object_creation()
|
|
self.db.memories = []
|
|
self.db.max_memories = 10
|
|
self.db.chatty = True
|
|
|
|
def at_object_receive(self, obj, source_location, **kwargs):
|
|
"""Called when moved to a new location."""
|
|
super().at_object_receive(obj, source_location, **kwargs)
|
|
if self.db.chatty:
|
|
self.start_chattering()
|
|
|
|
def hear_speech(self, speaker, message):
|
|
"""
|
|
Called by room when someone speaks.
|
|
Hook this into your room's msg() or create a custom at_say hook.
|
|
"""
|
|
if speaker == self:
|
|
return
|
|
|
|
if message and len(message) > 0:
|
|
self.db.memories.append(message)
|
|
if len(self.db.memories) > self.db.max_memories:
|
|
self.db.memories.pop(0)
|
|
|
|
def chatter(self):
|
|
"""Randomly repeat something heard."""
|
|
if not self.location:
|
|
return
|
|
|
|
if self.db.memories:
|
|
phrase = random.choice(self.db.memories)
|
|
self.location.msg_contents(
|
|
f'{self.get_display_name(looker=None)} squawks: "{phrase}"'
|
|
)
|
|
|
|
if self.db.chatty:
|
|
delay(random.randint(30, 90), self.chatter)
|
|
|
|
def start_chattering(self):
|
|
"""Begin the chatter loop."""
|
|
if self.db.chatty:
|
|
delay(random.randint(30, 90), self.chatter)
|
|
```
|
|
|
|
to integrate speech capturing, add to your Room typeclass:
|
|
|
|
```python
|
|
from evennia import DefaultRoom
|
|
|
|
class Room(DefaultRoom):
|
|
def at_say(self, speaker, message, **kwargs):
|
|
"""Hook called when someone speaks in the room."""
|
|
# notify all parrot-like objects
|
|
for obj in self.contents:
|
|
if hasattr(obj, 'hear_speech'):
|
|
obj.hear_speech(speaker, message)
|
|
```
|
|
|
|
### tv implementation
|
|
|
|
```python
|
|
from evennia import DefaultObject
|
|
from evennia.utils import delay
|
|
import random
|
|
|
|
class Television(DefaultObject):
|
|
"""
|
|
An interactive TV with pull knob and turn dial.
|
|
"""
|
|
|
|
def at_object_creation(self):
|
|
"""Called once when object is first created."""
|
|
super().at_object_creation()
|
|
self.db.is_on = False
|
|
self.db.channel = 1
|
|
self.db.channels = {
|
|
1: ["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."],
|
|
2: ["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."],
|
|
3: ["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
|
|
}
|
|
self.db.broadcasting = False
|
|
|
|
# add commands to the object
|
|
self.cmdset.add_default(TvCmdSet, persistent=True)
|
|
|
|
def get_display_name(self, looker=None, **kwargs):
|
|
"""Add power state to name."""
|
|
name = super().get_display_name(looker, **kwargs)
|
|
if self.db.is_on:
|
|
return f"{name} (glowing)"
|
|
return f"{name} (dark)"
|
|
|
|
def start_broadcast(self):
|
|
"""Begin broadcasting loop."""
|
|
if self.db.broadcasting or not self.db.is_on:
|
|
return
|
|
self.db.broadcasting = True
|
|
self.broadcast()
|
|
|
|
def broadcast(self):
|
|
"""Show a message from current channel."""
|
|
if not self.db.is_on or not self.location:
|
|
self.db.broadcasting = False
|
|
return
|
|
|
|
messages = self.db.channels.get(self.db.channel, [])
|
|
if messages:
|
|
msg = random.choice(messages)
|
|
self.location.msg_contents(f"The television shows: {msg}")
|
|
|
|
delay(15, self.broadcast)
|
|
|
|
def at_object_receive(self, obj, source_location, **kwargs):
|
|
"""When moved to new location."""
|
|
super().at_object_receive(obj, source_location, **kwargs)
|
|
self.db.broadcasting = False
|
|
if self.db.is_on and self.location:
|
|
self.start_broadcast()
|
|
|
|
|
|
from evennia import CmdSet, Command
|
|
|
|
class CmdPull(Command):
|
|
"""
|
|
Pull the TV knob to toggle power.
|
|
|
|
Usage:
|
|
pull television
|
|
"""
|
|
key = "pull"
|
|
|
|
def func(self):
|
|
tv = self.obj
|
|
if tv.db.is_on:
|
|
tv.db.is_on = False
|
|
tv.db.broadcasting = False
|
|
self.caller.msg("You pull the knob. The television turns off with a click.")
|
|
self.caller.location.msg_contents(
|
|
f"{self.caller.name} turns off the television.",
|
|
exclude=self.caller
|
|
)
|
|
else:
|
|
tv.db.is_on = True
|
|
self.caller.msg("You pull the knob. The television flickers to life.")
|
|
self.caller.location.msg_contents(
|
|
f"{self.caller.name} turns on the television.",
|
|
exclude=self.caller
|
|
)
|
|
tv.start_broadcast()
|
|
|
|
|
|
class CmdTurn(Command):
|
|
"""
|
|
Turn the TV dial to change channels.
|
|
|
|
Usage:
|
|
turn television
|
|
spin television
|
|
"""
|
|
key = "turn"
|
|
aliases = ["spin"]
|
|
|
|
def func(self):
|
|
tv = self.obj
|
|
if not tv.db.is_on:
|
|
self.caller.msg("The television is off.")
|
|
return
|
|
|
|
num_channels = len(tv.db.channels)
|
|
tv.db.channel = (tv.db.channel % num_channels) + 1
|
|
self.caller.msg(f"You turn the dial. Now on channel {tv.db.channel}.")
|
|
self.caller.location.msg_contents(
|
|
f"{self.caller.name} changes the channel.",
|
|
exclude=self.caller
|
|
)
|
|
|
|
|
|
class TvCmdSet(CmdSet):
|
|
"""Commands available on the TV object."""
|
|
|
|
def at_cmdset_creation(self):
|
|
self.add(CmdPull())
|
|
self.add(CmdTurn())
|
|
```
|
|
|
|
---
|
|
|
|
## patterns across languages
|
|
|
|
after looking at all these systems, some common patterns emerge:
|
|
|
|
**event-driven architecture**: almost every DSL is fundamentally event-driven. greet/speech/command/timer triggers dominate. even the procedural-looking ones (LPC, DIL) use `wait()` and callbacks.
|
|
|
|
**state storage**:
|
|
- prototype systems (MOO, room.js) use object properties directly
|
|
- file-based systems (LPC) use member variables
|
|
- limited systems (DG Scripts, MobProg) get creative with numbered variables or simple flags
|
|
- modern systems (Evennia) use persistent `db.*` attributes
|
|
|
|
**timer patterns**:
|
|
- `fork` + recursion (MOO)
|
|
- `call_out` (LPC)
|
|
- `wait` + goto (DIL)
|
|
- `RAND_PROG` percentage (MobProg)
|
|
- `setTimeout` / `delay` (JavaScript / Python)
|
|
|
|
**command dispatch**:
|
|
- verb matching (MOO, room.js)
|
|
- `add_action` (LPC)
|
|
- command triggers (DG Scripts, CoffeeMUD)
|
|
- `$pattern` (MUSHcode)
|
|
- typeclass methods (Evennia)
|
|
|
|
**variable substitution**: older systems love `%actor%`, `$n`, `@me` style placeholders. newer ones prefer real language variables.
|
|
|
|
**embedded vs external**: some systems inline scripts into data files (MobProg, DG Scripts), others separate code from data (LPC, Ranvier). the hybrid approach (YAML data + JS behaviors) seems to win for maintainability.
|
|
|
|
**complexity gradient**:
|
|
- simple: MobProg, DG Scripts (limited control flow, mainly `if/else`)
|
|
- medium: MOO, MUSHcode (more features but still constrained)
|
|
- full language: LPC, DIL, JavaScript, Python (you can do anything)
|
|
|
|
**the parrot problem**: most systems can handle it, but the approaches differ:
|
|
- dynamic lists: MOO, LPC, JavaScript, Python do it naturally
|
|
- fixed-size buffers: DG Scripts use numbered globals, very clever
|
|
- impossible: basic MobProg can't really do it without extensions
|
|
|
|
**the tv problem**: tests both state management and command handling. simpler systems struggle with the two-verb interaction (pull vs turn), while OOP systems handle it easily.
|
|
|
|
**inspiration for our DSL**:
|
|
- lean toward event-driven, not polling
|
|
- make state storage first-class (not a hack)
|
|
- support both inline scripts (for simple stuff) and external modules (for complex behaviors)
|
|
- variable substitution is nice for builders, real variables for power users
|
|
- timer/delay primitives are essential
|
|
- speech/emote/action hooks should be easy
|