[adv3Lite] Question about "persistent" ConvNode

Dear Eric and other adv3Lite users,

I have a question about the use of ConvNode to implement an NPC that are persistent on a certain topic and wouldn’t change the topic and wouldn’t let you leave the room until she gets an answer.

People who read the adv3Lite tutorial would recognize this as the NPC Angela in the Airport example game in the tutorial. But to simplify things, I have a trimmed version of the code below hidden in spoiler tag:

[spoiler][code]#charset “us-ascii”

#include <tads.h>
#include “advlite.h”

versionInfo: GameID
name = ‘ConvNode Test Example’
byline = ‘by Ming Hua’
htmlByline = ‘by Ming Hua
version = ‘1’
authorEmail = ‘Ming Hua minghua@somewhere.mail
desc = ‘Example for ConvNode feature in adv3Lite’
htmlDesc = ‘Example for ConvNode feature in adv3Lite’
;

gameMain: GameMainDef
initialPlayerChar = me
;

testRoom1: Room ‘The Starting Location’
"Just a room for test, you can leave through the door to the south. "

south = testRoom2

;

  • me: Thing ‘you’
    isFixed = true
    person = 2
    contType = Carrier
    ;

++ uniform: Thing ‘pilot’s uniform’
"The pilot’s uniform you stole from the security area. "

wornBy = me

;

testRoom2: Room ‘The Other Location’
"Just another test room, the only door leads to north. "

north = testRoom1

;

  • angela: Actor ‘flight attendant; statuesque young; woman angela; her’
    "She’s a statuesque and by no means unattractive young woman. "

    shouldNotAttackMsg = 'That would be cruel and unnecessary. ’

    globalParamName = ‘angela’

    makeProper
    {
    proper = true;
    name = ‘Angela’;
    return name;
    }

;

++ DefaultAskForTopic
"{The subj angela} listens to your request and shakes her head. Sorry, I
can’t help you with that, she says. "
;

++ DefaultCommandTopic
"<>Angela<>Miss<>, would you
<>, please? you request.\b
In reply she merely cocks an eyebrow at you and looks at you as if to say,
Who do you think you’re talking to? "
;

++ DefaultAnyTopic
"{The subj angela} smiles and shrugs. "
;

++ DefaultGiveShowTopic
"You offer {the angela} {the dobj}, but she shakes her head and pushes {him
dobj} away, saying, I’m afraid I can’t accept {that dobj} from you,
sir. "
;

++ DefaultShowTopic
"You point towards {the dobj}.\b
Very interesting, I’m sure, sir, {the subj angela} remarks without
much enthusiasm. "

isActive = gDobj.isFixed

;

++ angelaPilotAgenda: ConvAgendaItem
initiallyActive = true

invokeItem()
{
    isDone = true;
    "{The subj angela} looks up at you sharply and frowns. <q>Hey! You're
    one of the the passengers, aren't you?</q> she remarks. <q>I remember
    looking at your ticket! You certainly aren't our pilot. What are you
    doing in that uniform?</q><.convnodet uniform> ";
    
}

;

++ ConvNode ‘uniform’;

+++ SayTopic ‘you have a pilot’s license; i’
"It’s quite all right, I have a pilot’s license, you assure her.\b
Yes, but… she begins. Do you actually mean to say you intend to
fly this plane? <.convnodet intend-fly> "
;

+++ SayTopic ‘you just found the uniform; i’
"I found the uniform, you need a pilot, you reply with a smile and a
shrug. Besides, I do know how to fly – I have a license.\b
You mean you’re intending to fly this plane? she demands
incredulously. <.convnodet intend-fly> "
;

+++ DefaultAnyTopic, ShuffledEventList
[
'No, but answer my question, she interrupts you. What are you
doing in that uniform? <.convstay> ',

    '<q>That\'s not what I asked,</q> she complains. <q>Tell me why you\'re
    wearing that uniform!</q> <.convstay>',
    
    '<q>Why are you wearing that uniform?</q> she insists, brushing aside
    your irrelevant remarks. <.convstay> ',
    
    '<q>That still doesn\'t tell me what you\'re doing with that
    uniform,</q> she complains. <q>Why are you wearing it?</q> <.convstay> '
]

;

+++ NodeEndCheck
canEndConversation(reason)
{
switch(reason)
{
case endConvBye:
"Oh no, you’re not avoiding my question like that! she tells
you. Tell me, why are you wearing that pilot’s uniform? ";
return blockEndConv;
case endConvLeave:
"You’re not going anywhere until you tell me what you’re doing in
that uniform! {the subj angela} insists. ";
return blockEndConv;
default:
return nil;
}
}
;

+++ NodeContinuationTopic
"<>I asked you a question<>I’m still waiting for an
answer<>, {the subj angela} <> reminds
you<> insists<> repeats<>. Why are you wearing that
uniform? "
;

++ ConvNode ‘intend-fly’
commonResponse = "\bVery well, then, she sighs. I suppose we don’t
have too much choice now, do we? Just as long as you know what you’re
doing… "
;

+++ YesTopic
“Yes, why not? you reply breezily. You can’t wait here all day –
Pablo Cortez and his merry crew won’t stand for it, for one thing!
<<location.commonResponse>>”
;

+++ QueryTopic ‘why not’
“Why not? you ask. You need a pilot and I need to get out of here.
Besides, I wouldn’t want to be in your shoes when this lot run out of
patience! You nod towards the gansgters and drug barons occupying the
passenger seats further down the aisle. <<location.commonResponse>>”
;

+++ QueryTopic ‘whether|if she has a better idea; you have’
“Do you have a better idea? you counter. There’s no sign of your
regular pilot, and I wouldn’t want to be in your shoes when your current
passengers run out of patience! <<location.commonResponse>>”
;

+++ DefaultAnyTopic
“Please answer my question, she insists. Do you really intend to
fly this plane? <.convstay>”
;

+++ NodeEndCheck
canEndConversation(reason)
{
switch(reason)
{
case endConvBye:
"That’s not an answer! she complains. Tell me, are
you proposing to fly this plane yourself? ";
return blockEndConv;
case endConvLeave:
"Don’t walk off until you’ve told me whether you’re proposing to
fly this plane, {the subj angela} insists. Well, are
you? ";
return blockEndConv;
default:
return nil;
}
}
;

+++ NodeContinuationTopic
"I’d appreciate it if you answered my question, {the subj angela}
insists. Are you really proposing to fly this aircraft? "
;
[/code][/spoiler]
This mostly work as expected, you can’t leave or change the topic:

However, the command ATTENDANT, GOODBYE doesn’t trigger the intended NodeEndCheck, therefore the PC can leave this persistent ConvNode that way:

Since the example from the tutorial behaves the same way, it’s either a bug in the library or a bug in the tutorial. The obvious workaround is, of course, adding a ByeTopic in the ConvNode:

+++ ByeTopic "<q>Well, cheerio for now then,</q> you say.\b <q>No, don't leave before telling me where you get the uniform!</q> {the subj angela} tells you. <.convstay> " ;
And it seems to work –

– until you try to leave immediately after the GOODBYE command:

Any insights?

I’ll need to look into this (and it may be a day or two before I can get to it), but my immediate reaction is that it must be a library bug, since the NodeEndCheck as defined ought to block the attempt to say goodbye to Angela at this point.

Okay, I think I’ve located and fixed the bug in the library.

It’s in actor.t at around line 248.

The block of code that reads:

       /* treat Actor, Bye as saying goodbye to the actor */
        else if(action.ofKind(Goodbye))
        {
            gCommand.actor = gPlayerChar;
            sayGoodbye();
        }  

Should instead read:

      /* treat Actor, Bye as saying goodbye to the actor */
        else if(action.ofKind(Goodbye))
        {
            gCommand.actor = gPlayerChar;
            endConversation(endConvBye); //THIS IS THE STATEMENT TO CHANGE 
        }   

A quick check indicates that this seems to fix the bug, which was that the original code was bypassing the check to ensure that ending the conversation was permissible

Thanks Eric, this indeed fixes the bug.

However I have another different but likely related problem: Giving Angela a command also escapes the ConvNode, because surprisingly, while the DefaultAnyTopic in the ConvNode catches all the Ask, Tell, Give, Show topics and whatnot, it doesn’t catch a Command topic and it slips back to the DefaultCommandTopic of the Actor object angela:

I know I can add a DefaultCommandTopic to the ConvNode to work around this, but should this be worked around in the first place?

You’re right; I think a DefaultAnyTopic should catch commands as well.

This can be fixed by changing the definition of DefaultAnyTopic to:

class DefaultAnyTopic: DefaultTopic
    /* 
     *   DefaultAnyTopics are included in all the lists of their TopicDatabase
     *   that contain lists of conversational responses.
     */
    includeInList = [&sayTopics, &queryTopics, &askTopics, &tellTopics,
        &giveTopics, &showTopics, &askForTopics, &talkTopics, &miscTopics,
    &commandTopics] // NOTE THE ADDITION HERE
    
    matchObj = static inherited + Action // AND THIS NEW PROPERTY DEFINITION
;

Yes this fix does it, thanks for the quick answers, Eric!