Got another one. In the game I’m working on there’s a fair amount of NPC scripting, where the NPCs engage in various behaviors in a semi-autonomous fashion. Most of this is handled via agendas. What I’m trying to do is use an order from the player in the form of (for example) ALICE, TAKE THE PEBBLE to trigger an agenda that handles the task, possibly involving multiple steps, some of which may not be in scope when the order is given.
First, an example of the agenda behavior. In this sample game there are two rooms: a starting room containing the player and Alice, and a room to the north containing a box which in turn contains a pebble. The player can TELL ALICE ABOUT PEBBLE, prompting Alice to go on a “quest” to retrieve the pebble with no further interaction with the player. This is a very simplified example and doesn’t handle exceptions (like the player rushing in and grabbing the pebble before Alice can), but it illustrates the general form of what I’m talking about:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
startRoom: Room 'Void'
"This is a featureless void. "
north = northRoom
;
northRoom: Room 'North'
"This is the north room. Also a void. "
south = startRoom
;
+ box: OpenableContainer, Fixture 'wooden box' 'box'
"A small wooden box. "
location = northRoom
isListed = true
contentsListed = nil
contentsListedInExamine = nil
;
++ pebble: Thing 'small round pebble' 'pebble'
"A small, round pebble. "
;
class PebbleActor: Actor
;
class PebbleAgenda: AgendaItem
agendaOrder = 99
initiallyActive = true
// What we're after
target = nil
// All we need is a target
isReady = (target != nil)
// True if we're carrying our target, nil otherwise
haveTarget = ((target != nil)
&& (target.getCarryingActor() == getActor()))
// The "quest" is over
resetQuest() { target = nil; }
// Main logic for the "quest"
invokeItem() {
local actor, rm;
actor = getActor();
// Should never happen, give up
if(target == nil)
return;
// Figure out where we want to be
if(haveTarget()) {
// If we have the target, return to the start room
rm = startRoom;
} else {
// If we don't have the target, we want to go where
// it is
rm = target.roomLocation;
}
// If we're not where we want to be, figure out how to
// get there
if(actor.roomLocation != rm) {
// Comically simplistic "path finding"
switch(rm) {
case startRoom:
newActorAction(actor, South);
break;
default:
newActorAction(actor, North);
break;
}
return;
}
// If we have the target and we reached this point, we're
// where we want to be, so just declare victory
if(haveTarget()) {
"<q>Quest complete,</q> <<actor.name>> declares. ";
resetQuest();
return;
}
// We're in the target location and we don't have it yet,
// so see if we can just take it
if(actor.canSee(pebble)) {
newActorAction(actor, Take, pebble);
return;
}
// We can't see the pebble, but we're in the same room as
// it is, so it must be in the box (or something esoteric
// beyond the scope of this example code).
newActorAction(actor, Open, box);
}
;
alice: PebbleActor 'alice' 'Alice'
"She looks like the first person you'd turn to in a problem. "
isHer = true
isProperName = true
location = startRoom
;
+ alicePebbleAgenda: PebbleAgenda;
+ PebbleTopic: Topic 'pebble';
+ TellTopic @PebbleTopic
topicResponse() {
"<q>I think there's a pebble in the room north of here,</q>
you mention casually. ";
alicePebbleAgenda.target = pebble;
}
;
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 all works as written, but it can’t be directly converted into behavior triggered by a command (i.e. ALICE, TAKE THE PEBBLE instead of TELL ALICE ABOUT PEBBLE), for several reasons: the pebble doesn’t start out in scope (because nobody can see it), the various verify()
methods all fail for various reasons, and so on.
Here’s a slightly different example game which does not work the way I want it to, that illustrates some of the problems I’ve identified an how I’ve tried to approach them. Instead of two rooms we’ve been reduced to one to simplify things, and there are a bunch of additional modifications to library classes which I’ll discuss after the code:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
//#define __DEBUG_PEBBLE
startRoom: Room 'Void'
"This is a featureless void. "
;
+ box: OpenableContainer, Fixture 'wooden box' 'box'
"A small wooden box. "
location = startRoom
material = glass
isListed = true
contentsListed = nil
contentsListedInExamine = nil
;
++ pebble: Thing 'small round pebble' 'pebble'
"A small, round pebble. "
dobjFor(Take) {
verify() {
if(gActor._cmdFlag)
return;
inherited();
}
action() {
if(gActor._cmdFlag)
return;
inherited();
}
}
;
modify Action
objInScope(obj) {
#ifdef __DEBUG_PEBBLE
"\nobjInScope(<<obj.name>>): gActor is <<gActor.name>>\n ";
#endif
return(gActor.hasSeen(obj));
}
/*
isConversational(issuingActor) {
if(issuingActor == me) return(true);
return(nil);
}
*/
;
modify PreCondition
checkPreCondition(obj, allowImplicit) {
if(isThisAnOrder(obj))
return(nil);
return(inherited(obj, allowImplicit));
}
verifyPreCondition(obj) {
#ifdef __DEBUG_PEBBLE
"\nverifyPreCondition(<<obj.name>>):
gActor is <<gActor.name>>\n";
#endif
if(isThisAnOrder(obj))
return;
inherited(obj);
}
isThisAnOrder(obj) {
if(gActor._cmdFlag) return(true);
return((gIssuingActor != nil) && (gActor != gIssuingActor));
}
;
class PebbleActor: Actor
_cmdFlag = nil
obeyCommand(fromActor, action) {
if(action.ofKind(TakeAction)) {
#ifdef __DEBUG_PEBBLE
"obeyCommand(): accepting order\n ";
#endif
_cmdFlag = true;
return(true);
}
return(inherited(fromActor, action));
}
resetCmd() {
_cmdFlag = nil;
}
actorAction() {
inherited();
if(_cmdFlag) {
#ifdef __DEBUG_PEBBLE
"actorAction() for <<gActor.name>>\n ";
"\tgDobj is <<gDobj.name>>\n ";
#endif
alicePebbleAgenda.target = gDobj;
resetCmd();
//throw new TerminateCommandException();
exit;
}
resetCmd();
}
;
class PebbleAgenda: AgendaItem
agendaOrder = 99
initiallyActive = true
// What we're after
target = nil
// All we need is a target
isReady = (target != nil)
// True if we're carrying our target, nil otherwise
haveTarget = ((target != nil)
&& (target.getCarryingActor() == getActor()))
// The "quest" is over
resetQuest() { target = nil; }
// Main logic for the "quest"
invokeItem() {
local actor;
actor = getActor();
// Should never happen, give up
if(target == nil)
return;
// If we have the target and we reached this point, we're
// where we want to be, so just declare victory
if(haveTarget()) {
"<q>Quest complete,</q> <<actor.name>> declares. ";
resetQuest();
return;
}
// We're in the target location and we don't have it yet,
// so see if we can just take it
if(actor.canSee(pebble)) {
newActorAction(actor, Take, pebble);
return;
}
// We can't see the pebble, but we're in the same room as
// it is, so it must be in the box (or something esoteric
// beyond the scope of this example code).
newActorAction(actor, Open, box);
}
;
alice: PebbleActor 'alice' 'Alice'
"She looks like the first person you'd turn to in a problem. "
isHer = true
isProperName = true
location = startRoom
;
+ alicePebbleAgenda: PebbleAgenda;
+ PebbleTopic: Topic 'pebble';
+ TellTopic @PebbleTopic
topicResponse() {
"<q>I think there's a pebble in the box,</q>
you mention casually. ";
alicePebbleAgenda.target = pebble;
}
;
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
;
The first issue is scope. When issuing a command of the form ALICE, TAKE THE PEBBLE the parser will almost immediately give up because nobody can see the pebble. In this example the box now has material = glass
so everybody can see its contents, but what I want is to rely on NPC knowledge (or lack of knowledge) of where the pebble is. The code modifies Action
to include
objInScope(obj) {
return(gActor.hasSeen(obj));
}
…which is approximately what I want, but gActor
is always the player when the scope is initially evaluated, even when the context is the player issuing a command to another actor. So that’s problem number one: what I want is to evaluate the scope for the actor receiving the command, but if there’s some way to do that early enough in the process that the command won’t immediately fail (with “Alice sees no pebble.”) I don’t know about it.
The next problem is the implicit action that the parser will immediately add to the actor’s pending action queue. What I’m attempting, and failing, to do in the code is to use obeyCommand()
to flag that the NPC’s current action is them following a command. If this flag is set, the intent is to bypass verify()
and PreCondition
checks as well as the default action()
behavior. Processing will eventually reach actorAction()
where we can trigger the agenda.
The problem is that the implicit action logic means that by the time we reach actorAction()
the gDobj is box
, not pebble
…because the parser has figured out that we need to open the box to get to the pebble.
But at this point I’m wondering if this basic approach is just completely off-base. I also explored, but made no particular progress with, trying to modify Action
's isConversational()
method to try to handle ALICE, TAKE THE PEBBLE as a conversational item instead of an order.
Because I really just want to tell the parser to skip the normal “issuing an order” logic and just let me get at the parsed input late enough in the process that i.e. gDobj has been set (it is not set when obeyCommand
is called, or we could just call the agenda from there and then abort the rest of the command processing).
I hope this is all reasonably clear. The main moving parts of the problem I’m having trouble with are:
- Tweaking the object scope to reflect NPC knowledge when the NPC is following an order (it’s straightforward when the actor is initiating the action on their own)
- Passing a currently-invalid command through processing far enough to be able to use it to trigger an agenda
If there’s a library or something that handles this sort of thing, a pointer to it would be appreciated.