Most TADS3 code examples I’ve seen handle NPC scripting more or less by circumventing the normal verify()/check()/action() logic the player goes through, and instead just directly doing whatever the script wants to accomplish. For example, if an NPC is meant to take an object and say something while doing it, there will be something like:
someAction() {
"Bob notices a widget and quickly grabs it. ";
widget.moveInto(bob);
}
…or whatever. That’s fine, but I’m interested in something that works with more “autonomous” NPC behavior, where actions might be coming from, for example, a simple FSM in an agenda, and are being executed in the form of newActorAction()
calls.
The approach I’ve worked out is creating a subclass (or several subclasses) of npcActionMessages and replacing specific action messages with hooks to call a method in the NPC. Here’s a simple example:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
startRoom: Room 'Void'
"This is a featureless void. "
// Simple room daemon that will reset the pebble's position
// after it's been in anyone's inventory for a couple turns
pebbleCounter = 0
pebbleFlag = nil
roomDaemon() {
if(pebbleFlag == true) {
roomDemonCounter();
} else {
roomDemonReset();
}
}
roomDemonReset() {
if(pebble.location == self)
return;
pebbleFlag = true;
pebbleCounter = 3 + rand(4);
}
roomDemonCounter() {
local holder;
if(pebbleCounter > 0) {
pebbleCounter -= 1;
return;
}
holder = pebble.location;
gMessageParams(holder, pebble);
"<.p>
The room daemon cackles quietly to itself as it moves
{the pebble/him} from {the's holder/her}
inventory and into the room.<.p> ";
pebble.moveInto(self);
pebbleFlag = nil;
}
;
+ pebble: Thing 'small round pebble' 'pebble'
"A small, round pebble. "
isEquivalent = true
;
class PebbleActor: Actor
willNotLetGo(obj) {
gMessageParams(self, obj);
return('{The self/he} {won\'t} let {you/him} have
{that obj/him}. ');
}
initializeThing() {
local a;
inherited();
a = new pebbleAgenda();
a.location = self;
self.addToAgenda(a);
}
;
class pebbleAgenda: AgendaItem
agendaOrder = 99
isReady() {
return(inherited()
&& (self.location.contents.indexOf(pebble) == nil));
}
invokeItem() {
if(rand(100) > 25)
return;
newActorAction(self.location, Take, pebble);
}
;
class pebbleActionMessages: npcActionMessages
willNotLetGoMsg(holder, obj) {
return(holder.willNotLetGo(obj));
}
;
alice: PebbleActor 'alice' 'Alice'
"She looks like the first person you'd turn to in a problem. "
isHer = true
isProperName = true
location = startRoom
getActionMessageObj = new aliceActionMessages
willNotLetGo(obj) {
gMessageParams(self, obj);
return('<<gActor.name>> tries taking a pebble from
{The self/him}.<.p>
<q>Nope. Mine,</q> {The self/he} say{s},
<q>All mine.</q>');
}
;
aliceActionMessages: pebbleActionMessages
okayTakeMsg() {
if(gDobj.ofKind(pebble)) {
return('<q>I spy with my little eye</q> says Alice,
<q>A pebble!</q>. She gleefully takes the
pebble. ');
}
gMessageParams(gActor, gDobj);
return('{The gActor/he} take{s} {the obj/him}. ');
}
;
bob: PebbleActor 'bob' 'Bob'
"He looks like a Robert, only shorter. "
isProperName = true
location = startRoom
getActionMessageObj = new bobActionMessages
willNotLetGoList = [
'<<gActor.name>> reaches for one of
{the\'s self/her} pebbles.<.p>
<q>Never!</q> {The self/he} says{s}, laughing,
<q>The pebbles shall all be mine!</q>',
'<<gActor.name>> tries taking one of {the\'s self/her}
pebbles.<.p>
<q>Avast, varlet!</q> {The self/he} yell{s},
pulling away, <q>You\'ll never have my pebbles!</q>',
'{The gActor/he} sidles up to {The self/him} with an
innocent look on {its gActor/her} face.<.p>
<q>Don\'t even think about it, </q> {The self/he}
growls. '
]
willNotLetGo(obj) {
gMessageParams(self, obj, gActor);
return(self.willNotLetGoList[rand(self.willNotLetGoList.length()) + 1]);
}
;
bobActionMessages: pebbleActionMessages
okayTakeMsg() {
if(gDobj.ofKind(pebble)) {
return('<q>Ha!</q> says Bob, <q>A pebble!</q>. Bob
greedily takes the pebble. ');
}
gMessageParams(gActor, gDobj);
return('{The gActor/he} take{s} {the obj/him}. ');
}
;
me: Actor
location = startRoom
;
versionInfo: GameID
name = 'sample'
byline = 'nobody'
authorEmail = 'nobody <foo@bar.com>'
desc = '[This space intentionally left blank]'
version = '1.0'
IFID = '12345'
;
gameMain: GameMainDef
initialPlayerChar = me
;
This is a simple drama involving two NPCs, a slightly kleptomaniacal room daemon, and a single pebble. The long-suffering Alice and Bob both covet the pebble, and each have an agenda that sees them attempting to take it, wherever it is, with a certain probability each turn. The room daemon notices whenever the pebble has been in anyone’s inventory for more than a couple turns and returns it to the room. So it’s an endless cycle of one NPC taking the pebble and then the other NPC trying and failing to take it from them.
If this toy problem is all I was worried about it would be easier, probably, to just implement all the actor-specific behaviors (in this case just flavor text for the actions) in a bunch of conditional statements in, i.e. dobjFor(Take)
on the pebble and so on. Or just “fake” the behavior in the agenda by having the invokeItem() do what my someAction()
does above: just display text and force actions without going through newActorAction()
.
But in the “real” code I’m writing I actually want to be able to use all the verify()
and check()
logic of the involved objects (without having to re-implement it in the NPC scripting).
Is there a cleaner/more straightforward way to do this sort of thing? Basically it’s fairly easy to change the behavior of objects in an action by for example twiddling the object’s dobjFor()
corresponding to that action. But there doesn’t seem to be a similar thing for changing the behavior of an NPC when they are the one initiating an action.