adv3Lite: Conversations

Hi Eric,

I think I reached a point of diminished returns tonight trying to get this to work, so I figured I would post here for help. :slight_smile:

I have 3 issues.

Here’s the scenario.

After a trigger, an NPC arrives and starts a conversation; I then want 3 responses suggested to the PC. Picking one of them advances the story. Anything else has a default response and then the 3 possible responses are suggested again.

  1. However, the suggestions aren’t shown until I interact with the NPC. Here’s the code:

Trigger code:

clientArrival() { client.actionMoveInto(pettingZoo); clientArrives.invokeItem();}
Code immediately after NPC object:

[code]+ clientArrives: ConvAgendaItem
isReady = true
invokeItem() {
isDone = true;
“The client found me. I wasn’t ready for this. He smiled broadly and [snip]
<.convnodet client-greeting>”;
}
;

  • clientChatting : ActorState
    isInitState = true
    specialDesc = "My client was rather patiently waiting for me to respond. "
    stateDesc = "He looked at me, waiting for me to speak. "
    isActive = true
    attentionSpan = nil
    ;

  • SayTopic ‘you're nervous; you are i'm i am’
    "Hahahahah, ahhhh! Ah! Sorry to be so nah, n-nervous! I’m glad, ah, that you met me here, however. Sorry, I
    didn’t get your name, however?

    <><.convnode client-findWoman><.topics>" name = 'I\'m nervous'

;

  • SayTopic ‘that I'm curious; you are i'm i am’
    "I’m extremely curious, naturally. But I’m a professional. I’m up for anything. Sorry, I didn’t get your name, however?

    <><.convnode client-findWoman><.topics>" name = 'I\'m curious'

;

  • SayTopic ‘I could guess’
    "Psh, I bet I could guess. I tried to come off as confident. I tried to come off as in control of the situation.
    I failed at those two things, because the client just chuckled. Rather than get all red-cheeked, I deflected.
    Sorry, I didn’t get your name, however?

    <><.convnode client-findWoman><.topics>" name = 'I could guess'

;

  • ConvNode ‘client-greeting’
    npcGreetingMsg = nil
    limitSuggestions = true
    npcContinueList : CyclicEventList {
    eventList = [
    ‘You're probably still wondering what this is all about? he reminded me.<.topics>’,
    nil,
    ‘You OK? he smiled cautiously, I asked you if you're wondering why I brought you here today. <.topics>’,
    nil
    ]}
    canEndConversation (actor, reason) {
    "I didn’t break off the conversation at that point. ";
    return nil;
    }
    ;

++ DefaultConvStayTopic
“There were more specific things I needed from him. <.convstay>”
isConversational = nil
;
[/code]
The trigger works fine but none of those say topics are suggested until I initiate conversation with the NPC, either by ‘talk to client’ or ‘client, hello’. This is the output:

  1. Also, the cycleEventList in the client-greeting convNode never gets triggered.

  2. In the ADV3 library, this is how the same code works:

In adv3Lite, it’s:

Is the ‘say’ optional in the ‘curious’ and ‘guess’ suggestions?

Apologies if this came across as ramblings, but I felt like I made a lot of progress tonight with no results to show for it… lol

The adv3Lite conversation system looks superficially similar to adv3’s on the surface, but there are quite a few differences once you get beyond the absolute basics. Not least among these, an adv3Lite ConvNode is rather different from an adv3 one. So there’s quite a few changes you need to make to your code to make it work as you want.

First, you need to locate your SayTopics inside your ConvNode.

Second, you’ve defined an adv3-style ConvNode, not an adv3Lite one. In adv3Lite ConnNode doesn’t have properties like npcContinueList and methods like canEndConversation(); you have to define separate NodeContinuationTopic and NodeEndCheck objects.

Third, there’s no such class as a DefaultConvStayTopic (unless you’ve defined it yourself), and you’d probably just use a DefaultAnyTopic here (as in adv3).

Fourth, you mostly defeat the purpose of a ConvAgendaItem if you override its isReady property; instead you want to set its initiallyActive property to true. Then you shouldn’t need to call its invokeItem() method explicitly; just move the client to the location of the Player Char with moveInto() or actionMoveInto() and the ConvAgendaItem will trigger.

Your code thus needs to be rearranged along the following lines:

+ clientArrives: ConvAgendaItem
    initiallyActive = true
    invokeItem()  {
        isDone = true;
        "The client found <i>me.</i> I wasn't ready for this. He smiled broadly
        and  [snip] <.convnodet client-greeting>";
    }
;
+ clientChatting : ActorState
    isInitState = true
    specialDesc = "My client was rather patiently waiting for me to respond. "
    stateDesc = "He looked at me, waiting for me to speak. "
    isActive = true
    attentionSpan = nil
;



+ ConvNode 'client-greeting'
    
;

++ SayTopic 'I\'m nervous; you are you\'re i\'m i am'
    "<q>Hahahahah, ahhhh! Ah! Sorry to be so nah, n-nervous! I'm glad, ah, that
    you met me here, however. Sorry, I didn't get your name, however?</q>
    <p><<client.nameResponse>><.convnodet client-findWoman>"    
;

++ SayTopic 'I\'m curious; you are i\'m i am'
    "<q>I'm extremely curious, naturally. But I'm a professional. I'm up for
    anything. Sorry, I didn't get your name, however?</q>
    <p><<client.nameResponse>><.convnodet client-findWoman>"
    
    includeSayInName = nil
;

++ SayTopic 'I could guess; you'
    "<q>Psh, I bet I could guess.</q> I tried to come off as confident. I tried
    to come off as in control of the situation. I failed at those two things,
    because the client just chuckled. Rather than get all red-cheeked, I
    deflected. <q>Sorry, I didn't get your name, however?</q>
    <p><<client.nameResponse>><.convnodet client-findWoman>"   
    
    includeSayInName = nil
;

++ NodeContinuationTopic, CyclicEventList
    [
        '<q>You\'re probably still wondering what this is all about?</q> he
        reminded me.<.topics>',
        nil,
        '<q>You OK?</q> he smiled cautiously, <q>I asked you if you\'re
        wondering why I brought you here today. </q><.topics>',
        nil
    ]
;
    
++ NodeEndCheck
    canEndConversation (actor, reason) {
        "I didn't break off the conversation at that point. ";
        return nil;
    }    
;

++ DefaultTopic
    "There were more specific things I needed from him. <.convstay>"
    isConversational = nil
;

I’ve tested the above code and it seems to work as I think you want it to.

This should be fixed by moving the CyclicEventList to a separate NodeContinuationTopic, as shown above.

You can suppress the ‘say’ in the suggested name of these topics by setting their includeSayInName property to nil (as shown above). Also, you don’t need to define their name property explicitly, since the library can figure it out from the name section of their vocab property (again as shown above).

I hope that helps you to get going again!

That did the trick – but one note. Funnily enough, I had SayTopics inside Convnode along with turning on/off numerous flags in several of the iterations, but the underlying problem was calling clientArrives.invokeItem() explicitly for the trigger. With the code you provided, I left that alone and it still didn’t work. It wasn’t until I removed the invokeItem() that it worked like it should. Just FYI.

I can now use that as a template for the rest of the conversation, so thanks!

One minor issue, since this is still in first person, can I change the:

(You can say…)

to

(I can say…)

– Mike

That doesn’t surprise me. Calling invokeItem() directly bypasses code that the conversation system needs in order to function properly. Internally the library calls invokeItemBase(caller) on the AgendaItem in question. That’s one reason why it’s better to let the library take care of it through the AgendaItem mechanism rather than trying to invoke an AgendaItem manually.

I’ll have to look into this, since it looks to me as if this is something the library ought to be taking care of this automatically (I take it you have set person = 1 on your player character object), so I need to investigate why it isn’t. As a temporary fix you could try defining:

CustomMessages
    messages = [
      Message(suggestion list intro, 'I could')
    ]  
;

If you want to revert to ‘you could’ later in the game then you can add an active property to the CustomMessages object that turns it on and off as needed, e.g.

CustomMessages
    messages = [
      Message(suggestion list intro, 'I could')
    ]  

    active = gameMain.usePastTense
;

That does the trick, but I see you’re testing me as well. It’s not Message but Msg. :slight_smile:

– Mike

Quite right! I’m glad you were paying attention! (Which seems to be more than I was doing at that point…)

Now that I’m back at my machine, I’ve taken a look at this, and can confirm that the library does take care of changing from ‘You could…’ to ‘I could…’ automatically, but only if your player character object has ‘I’ as its name property. If you’d defined your player character object as:

me: Thing  'I' @wherever     
    
    person = 1  
    contType = Carrier   
    isFixed = true
;

Then you should have seen ‘I could say…’ without having to create a CustomMessages object (at least I did when I tried it). If however you defined it as

me: Thing  'you' @wherever     
    
    person = 1  
    contType = Carrier   
    isFixed = true
;

Then you will indeed see ‘You could say…’ (at least I do). And you may get ‘you’ instead of ‘I’ in several other places as well.

Note that in the next version of adv3Lite you won’t have to worry about the name property of a first- or second-person player character, since the library will automatically figure it out from the person property. In the current (0.7) version you still have to handle this manually, however.

I think I’ve run into an issue but I’m not smart enough to go through the library to confirm it.

In the NPC object, in afterAction(), when something is true, I change the convnode to something else so that the next set of topics can be displayed.

"Text "

The “Text” is displayed, but the suggested topics will not display until the next turn. So I’m guessing that suggested topics don’t get displayed when convnodet is executed within afterAction in the NPC object?

This code works fine in the Adv3 library.

Also, when suggesting topics, the period appears to be missing from each suggestion.

ie:

(I could say I’m nervous, that I’m curious, or I could guess)

Is this a setting or library change?

Thanks,
– Mike

That’s probably right (I assume that in your code sample is a typo for <.convnodet client-endConversation> and not what you actually have in your code, otherwise the missing dot would be a problem). I can’t give a full response to this until I’m back at my own machine this evening and can try stepping through the code to see precisely what’s going on in this situation, but from experience I’ve noticed that adv3Lite can be a bit fussy about where tags like <.convnodet> get used! I’m not sure how easy it’ll be to fix this until I get a chance to get a good look at it, so I shan’t attempt to suggest a library patch at this point.

In the meantime one workaround that might work is to add a couple of lines after your text display:

if(whatever)
{
     "Text <.convnodet client-endConversation>";
     activeKeys = pendingKeys;        
     keepPendingKeys = nil;
}

The reason I think this may work is that it’s effectively what ConvAgendaItem does behind the scenes to avoid this kind of problem.

If that doesn’t work, another coding patten that should would be:

triggerObj: object;

....

if(whatever)
{
     initiateTopic(triggerObj);
}

...

+ InitiateTopic @triggerObj
    "Text <.convnodet client-endConversation>"
;

A couple of other points here: if you’re overriding the NPC object’s afterAction() method you need to include a call to inherited() if you still want afterAction() to work on any of the NPC’s ActorStates. You may know this already, since the same would be true of adv3, but I thought I should point it out just in case it was an accident waiting to happen.

Also, if you were using the afterAction() method of an ActorState, you’d need to include references to getActor everywhere, for example:

if(whatever)
{
     local actor = getActor;
     "Text <.convnodet client-endConversation>";
     actor.activeKeys = actor.pendingKeys;        
     actor.keepPendingKeys = nil;
}

Or

if(whatever)
{
     getActor.initiateTopic(triggerObj);
}

As I said, I’ll have to see if there’s a way this can be made a bit less fiddly in the next release, but I’m not making any promises! (The underlying problem - if you see it as a problem - is that the mechanism for doing part of the ‘book-keeping’ with these conversation tags is in the handleTopic() method of the Actor object, which is only invoked in response to a conversational command, and moving it somewhere else might break other features of the conversation system).

This was a deliberate stylistic choice on my part, since I felt the period looked superfluous with the parentheses. Thus, for an implicit topic inventory you get:

(I could say I’m nervous, that I’m curious, or I could guess)

Whereas for an explictly requested one (in response to a TOPICS command) you’d get the period:

I could say I’m nervous, that I’m curious, or I could guess.

At the moment this would be difficult for a game author to change (you’d have to copy a very lengthy method just to insert the period in one short string). In the next release I’ll try to remember to factor out separate showListPrefix() and showListSuffix() methods on the suggestedTopicLister object to make this easier to customize (and if people feel strongly about it I can add back the period before the closing parenthesis as the default).

That work-around works great. Thanks!

That makes sense. The only reason I picked up on it is that I was comparing the output script from Adv3 against the adv3Lib. I’m okay with your stylistic choice; I only mentioned it because I didn’t know it was intentional.

– Mike

I started to come up with a coding scheme that looked that it might fix your first problem, but then I realized that this could be a seriously bad idea.

The reason is that I’d be supporting a coding pattern that’s seriously inadvisable, potentially leading to game code full of obscure and hard-to-find bugs.

The conversation tags such as <.convnode> and all the rest are intended to be used in specific places, such as the topicResponse or eventList properties of TopicEntries or the invokeItem() methods of ConvAgendaItems. Using them outside these contexts is a recipe for potential confusion (I’m fairly sure this would be as true of adv3 as it is of adv3Lite). You may appear to get away with using these tags outside these particular contexts, but you could simply be storing up trouble for yourself. For that reason, it’s probably better that I don’t try to fix the issue that you raised about the suggested topics not appearing until the next turn (when I tried it, they didn’t appear at all), since at least this unexpected behaviour may signal to the game author that something may be amiss.

The general problem here is that bypassing the conversational API provided by the library bypasses its ability to keep the various parts of the conversation system in sync, which may eventually lead to unpredictable results.

The more specific problem with using conversational tags like <.convnodet new-node> in a double-quoted string in an afterAction() method is that they may well not do what you expect.

What <.convnodet new-node> means is something like “Make new-node the current conversation node of the current interlocutor and then display a list of current topic suggestions for the current interlocutor”. This is safe enough in the context of a TopicEntry or ConvAgendaItem, since it’s clear who the current interlocutor is meant to be, and the tag is being used within methods that allow the library to keep control of this. But it may not be at all clear who the current interlocutor is outside such contexts.

Suppose, for example, that there are two NPCs in scope, let’s call them Bob and Nancy. Now suppose you put your code in the afterAction method of Bob:

afterAction()
{
     if(whatever)
        "Text <.convnodet client-endConversation>";
}

Assuming this worked as you expected, which actor would be affected by this? You may simply assume it would be Bob, because the method is defined on Bob, but this is far from being necessarily the case. If the player character were in conversation with Nancy when this code was triggered, it would be Nancy who’d be put into the client-endConversation node and her suggested topics that would be listed, which probably isn’t what was intended (and could be confusing to player and game author alike). There’s nothing in this code that tells the conversation filter which object the <.convnodet> tag was invoked from, so it will continue to make its default assumption (which is really the only assumption that it can make) that it’s dealing with the current interlocutor, which could be Bob or Nancy or no one at all. You may think you’re sure that the whatever condition can only be true when Bob is the current interlocutor, but it would be very easy to get this wrong.

This kind of coding pattern should therefore be avoided, since in general it’s far from clear what its actual effect will be (and hence it can all too easily lead to subtle and hard-to-find bugs).

As a compromise with the convenience of doing what you were trying to do here, the next release of adv3Lite will define a new actorSay() method on Actor, so you could then safely write:

afterAction()
{
     if(whatever)
        actorSay('Text <.convnodet client-endConversation> ');
}

This will be safe because (a) the actorSay() method must be called on a specific actor (so it’s clear which actor it relates to) and (b) a method like actorSay() can take care of the housekeeping (which simply displaying a string can’t).

The next release will also separate out the showListPrefix() and showListSuffix() methods of suggestedTopicLister (in line with other Listers), which will make it much easier for game code to customize how these lists begin and end.