Table of Contents |
The Art of Conversation >
Angela Wants Answers
Angela Wants Answers
A Simple Conversation Node
You may recall that one of the exchanges we’ve defined for when the player character asks Angela what she’s doing tonight is:
'<q>What <i>are</i> you doing tonight?</q> you insist.\b
<q>I don\'t think that\'s any of your business,</q> she replies, with
rather a bleak smile. <q>Do you?</q> '
Many, if not most players will probably treat the “Do you?” at the end of Angela’s reply as a purely rhetorical question, but a few may try responding YES or NO. Such a response would be appropriate only just at that point in the conversation (there may be other points at which the player could respond YES or NO, but it wouldn’t mean the same thing, since it would constitute an answer to a different question). With Angela’s (possibly rhetorical) question “Do you?” the conversation enters a special state, which we might call a Conversation Node, meaning a particular point in the conversation at which particular responses become potentially appropriate. Once the conversation moves on, these particular responses (such as a YES or NO in response to Angela’s “Do you?”) cease to be appropriate once more.
We can model such a Conversation Node in adv3Lite by using a
<.convnode> tag, of the form \<.convnode
*key*\>
, where key corresponds to the convKey (or one of the
convKeys) of the TopicEntries we want to be active during that
Conversation Node. To ensure that those TopicEntries are only
available during that Conversation Node, we also need to set their
isActive
property to
nodeActive
. A YesTopic responds to YES and
a NoTopic responds to NO, so we could allow the player to reply YES
or NO to Angela’s possibly rhetorical question by adding a
\<.convnode\>
tag to the question and then the
following YesTopic
and
NoTopic
:
+ QueryTopic, StopEventList 'what' @tDoingTonight
[
'<q>What are you doing tonight?</q> you ask.\b
She cocks one eyebrow at you. <q>I have my plans,</q> she replies
vaguely. ',
'<q>What <i>are</i> you doing tonight?</q> you insist.\b
<q>I don\'t think that\'s any of your business,</q> she replies, with
rather a bleak smile. <q>Do you?</q> <.convnode not-your-business>',
'<q>About tonight...</q> you begin.\b
She cuts you off by pressing her lips together and raising her eyebrows
in a mildly disapproving manner, as if to say, <q>That topic is
closed.</q> '
]
convKeys = 'angela'
;
+ YesTopic
"<q>As a matter of fact I do,</q> you reply boldly.\b
<q>In that case we shall have to agree to differ,</q> she replies, just a
little stiffly."
convKeys = ['not-your-business']
isActive = nodeActive
;
+ NoTopic
"<q>No, I suppose not,</q> you concede.\b
<q>No; well, there you are then,</q> she remarks. "
convKeys = ['not-your-business']
isActive = nodeActive
;
This works, but it may seem a little repetitive to have to repeat the
convKeys
and
isActive
property on each TopicEntry in the
Conversation Node. We have already seen how we can use a
TopicGroup
to apply the same
isActive
and
convKeys
conditions to a group of
TopicEntries, and we can use a special kind of TopicGroup, a
ConvNode, to do that for us here:
+ ConvNode 'not-your-business';
++ YesTopic
"<q>As a matter of fact I do,</q> you reply boldly.\b
<q>In that case we shall have to agree to differ,</q> she replies, just a
little stiffly."
;
++ NoTopic
"<q>No, I suppose not,</q> you concede.\b
<q>No; well, there you are then,</q> she remarks. "
;
In most cases, this is probably the easier way to do it; it may also help make it more immediately apparent which TopicEntries relate to a particular Conversation Node.
A More Complex Case
A Conversation Node of the kind we’ve just defined is highly ephemeral. It lasts only for one conversational turn. If the player chooses to type YES or NO as the next conversational command following Angela’s question, then YesTopic or NoTopic will be triggered, but once the player types any other conversational command the moment is lost, and the TopicEntries in the node will no longer be available. In the case we’ve just implemented, that’s just as it should be, since the player can choose to say YES or NO or else treat the question as rhetorical and so ignore it altogether, in which case the opportunity for replying YES or NO is forever lost. But in another case, where Angela has just asked a question that clearly isn’t rhetorical, she may wish to insist on an answer, or at the very least to complain if the player tries to change the subject.
Suppose, for example, that when the player character is talking to Angela in the Jetway he’s given the option of talking to her about Pablo Cortez, and Angela wants to know why he’s so interested in the man. We could start by adding an AskTellTopic under angelaTalkingState that triggers an appropriate Conversation Node:
++ AskTellTopic, StopEventList @cortez
[
'<q>Do you know who that man waving a gun around at the front of the
plane is?</q> you ask, lowering your voice. <q>It\'s Pablo Cortez, El
Diablo\'s right-hand man!</q><.inform cortez>\b
Her smile becomes rather frosty as she replies, <q>What\'s that to
you?</q> <.convnodet what-to-you>',
'<q>You need to be <i>very</i> careful around Cortez,</q> you warn
her.\b
<q>I shall be,</q> she assures you. '
]
autoName = true
convKeys = 'top'
suggestAs = TellTopic
;
There are a few points to note about the way we’ve defined this
AskTellTopic. First, note that we’ve given it an autoName of true, so
that it will be suggested as a topic of conversation with the name
‘Pablo Cortez’. Note too that we needed to add
convKeys = 'top'
to make sure that it would be
included as a top-level suggestion in response to a TOPICS command (but
we only have to do that because we defined
suggestionKey = 'top'
on Angela). Next, note
how we’ve defined suggestAs = TellTopic
. Left
to its own devices the library will suggest an AskTellTopic with ‘ask’,
i.e. “You could ask her about Pablo Cortez”. We can override that with
the suggestAs
property to force the library to
suggest is as something else, here as if it were a TellTopic (i.e. “You
could tell her about Pablo Cortez”). Then note the use of the
\<.inform cortez\>
tag. This works much like a
\<.reveal\>
tag, but instead of recording the
fact that something has just been revealed to the player character, it
signals that something has been revealed by the player character to the
person he’s talking to (and, incidentally, to anyone else within
earshot). Finally note that instead of \<.convnode
what-to-you\>
we wrote \<.convnodet
what-to-you\>
, with an extra t (convnodet rather than just
convnode). The extra t tells the game to display a suggested list of
topics on entering the Conversation Node.
The next step is to define the TopicEntries making up the Conversation Node. We’ll once again use a ConvNode object to simplify the definition (so we don’t have to define the isActive and convKeys properties on each of the TopicEntries on the node). But we’ll also do something new: we’ll use a type of TopicEntry we haven’t seen before, the SayTopic. A SayTopic allows the player character to say just about anything, for example SAY SHE LOOKS NICE or SAY YOU ARE AFRAID or SAY THAT YOU’VE BEEN HERE BEFORE (the inclusion or exclusion of the THAT makes no difference). But a SayTopic will also match even if the command SAY is missing, provided that a conversation is in progress and that the player’s command doesn’t look like any other valid command (including any other valid conversational command). The player could thus type just SHE LOOKS NICE, YOU ARE AFRAID or YOU’VE BEEN HERE BEFORE or even, if the SayTopics have been defined carefully, YOU LOOK NICE, I AM AFRAID, or I’VE BEEN HERE BEFORE.
Defining a SayTopic is much like defining a QueryTopic, except that we
don’t need the qtype (who/what/where/why/when) part. We can define a
SayTopic either using a separate Topic object, or by defining the Topic
to be matched in-line on the SayTopic (just as we can for a QueryTopic).
For the full story, see the section on Special
Topics in the adv3Lite Library Manual.
Note that a SayTopic is included in topic suggestion lists automatically
(like a QueryTopic), that is, its autoName property is true by default.
If you don’t want the suggestion to begin with ‘say’ you can define
includeSayInName = nil
on the SayTopic.
For present purposes we’ll define our Conversation Node with one TellTopic and two SayTopics:
+ ConvNode 'what-to-you';
++ TellTopic @me
"<q>The name's Pond, Sherlock Pond,</q> you tell her. <q>I'm a British
secret agent on the track of these villains!</q>\b
<q>Indeed!</q> she replies with ill-disguised scepticism. <.inform agent>"
name = 'yourself'
;
++ SayTopic 'Cortez is dangerous'
"<q>Pablo Cortez is a <i>very</i> dangerous man,</q> you warn her. <q>He's
killed more men than I've had hot dinners!</q>\b
<q>Anyone waving a gun around aboard a passenger aircraft might be
considered dangerous,</q> she points out pragmatically. <.inform cortez-dangerous> "
;
++ SayTopic 'she should call security; you'
"<q>You should call airport security to deal with him!</q> you urge her.\b
<q>Airport security -- in Narcosia?</q> she asks incredulously. <q>Somehow I
don't think that will exactly help the situation!</q> "
;
What happens if the player responds with something not corresponding to
one of these three TopicEntries? We can trap that by adding a
DefaultAnyTopic to the Conversation Node to trap any other
conversational commands. The first time round Angela will complain and
repeat her question; the second time she’ll complain but let the matter
drop. To make this happen we add a \<.convstay\>
tag to Angela’s first default conversational response to tell the
game to keep the Conversation Node active for another conversational
turn:
++ DefaultAnyTopic, StopEventList
[
'<q>No, but what is it to you who this man is?</q> she interrupts you.
<.convstay> ',
'She shakes her head. <q>Very well, don\'t answer my question then,</q>
she mutters. '
]
;
The other thing the player could do to throw our Conversation Node off
the rails, besides coming up with a response we hadn’t planned for, is
to try to end the conversation prematurely in the middle of the node,
either with an explicit BYE command or by simply walking away. To
control whether we want to allow this we can add a NodeEndCheck
object to the Conversation Node, on which we then define one method,
canEndConversation(reason), which determines whether or not the
player character is allowed to end the conversation while this node is
active. The reason parameter can take a number of values but the two
most common ones, and the ones that concern us here, are
endConvBye
(meaning the player is trying to
end the conversation with a BYE command) and
endConvLeave
(meaning the player is trying to
end the conversation by having the player character walk away from it).
The canEndConversation(reason)
can then return
true
to allow the conversation to end for that
reason, or either nil
or
blockEndConv
to prevent the conversation from
ending. The difference is that blockEndConv
signals that the actor the player character is speaking with has now
spoken on the current turn; it’s therefore appropriate to return
blockEndConv
if our
canEndConversation()
method displays something
said by the actor to explain why she won’t allow the conversation to
end.
We can add a NodeEndCheck object to our current Conversation Node thus:
++ NodeEndCheck
canEndConversation(reason)
{
if(reason == endConvBye)
{
"<q><q>Goodbye,</q> isn't an answer,</q> {the subj angela}
complains. <q>Why are you so bothered about this man Cortez?</q> ";
return blockEndConv;
}
if(reason == endConvLeave)
{
"This doesn't seem a good point to break off the conversation. ";
return nil;
}
return true;
}
;
Angela Demands an Answer
In the first Conversation Node we implemented above, the player could reply YES or NO or just let the matter pass, since Angela’s question could easily have been purely rhetorical. In the second, Angela makes some attempt to insist on an answer, but lets the matter go if the player refuses to give one. A third type of Conversation Node might be one in which Angela absolutely demands an answer and the game can’t proceed until she gets one.
When Angela sees the player character return to the plane wearing the pilot’s uniform she might be a little surprised. She may recognize him as the man who talked to her on the jetway, but she certainly doesn’t recognize him as the pilot she’s expecting, so she’ll probably demand to know who he is.
The first step is to create a new AgendaItem for Angela to ask her
question — a ConvAgendaItem would be appropriate, since that’s triggered
as soon as conversation becomes possible — and then make sure it’s added
to Angela’s agenda list at some suitable point, such as the
invokeItem()
method of angelaReboardingAgenda,
which causes her to reboard the plane once the player is wearing the
pilot’s uniform (and the takeover scene comes to an end):
+ angelaReboardingAgenda: AgendaItem
isReady = (takeover.hasHappened)
invokeItem()
{
isDone = true;
getActor.moveInto(planeFront);
getActor.setState(angelaSeatedState);
getActor.addToAgenda(angelaPilotAgenda);
}
;
+ angelaPilotAgenda: ConvAgendaItem
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> ";
}
;
Next we can set up the corresponding Conversation Node and populate it with some SayTopics. Whichever response the player chooses we’ll have Angela ask if the player character really intends to fly the plane and then switch to another Conversation Node for an answer to that question:
+ ConvNode 'uniform';
++ SayTopic 'all British agents learn to fly'
"<q>I told you, I'm a British agent, and all British agents learn to fly --
it's part of our training,</q> you tell her.\b
<q>You mean you actually intend to fly this aircraft?</q> she demands,
startled. <.convnodet intend-fly> "
isActive = gInformed('agent')
;
++ SayTopic 'you have a pilot\'s license; i'
"<q>It's quite all right, I have a pilot's license,</q> you assure her.\b
<q>Yes, but...</q> she begins. <q>Do you actually mean to say you intend to
fly this plane?</q> <.convnodet intend-fly> "
isActive = !gInformed('agent')
;
++ SayTopic 'you\'re the replacement pilot; you are i am i\'m'
"<q>You said you were waiting for the pilot, but there's no sign of him, so
I'm standing in for him,</q> you reply.\b
<q>You!</q> she exclaims. <q>You mean, <i>you're</i> going to fly this
plane?</q> <.convnodet intend-fly> "
isActive = gRevealed('pilot-awaited')
;
++ SayTopic 'you just found the uniform; i'
"<q>I found the uniform, you need a pilot,</q> you reply with a smile and a
shrug. <q>Besides, I do know how to fly -- I have a license.</q>\b
<q>You mean you're intending to fly this plane?</q> she demands
incredulously. <.convnodet intend-fly> "
;
Note the use of the isActive
on the first
three SayTopics to determine whether or not they’re appropriate in the
light of what has or hasn’t been said before. The
gInformed(key)
tests whether or not the actor
has previously been informed of key by the player character via an
\<.inform key\>
tag, so either the first or
the second SayTopic will be active depending on whether or not the
player character has already told Angela who he is. The
gRevealed(key)
tests whether key has
previously been revealed to the player character via a
\<.reveal key\>
tag, so the third SayTopic
will be active if Angela has previously mentioned that the plane is
waiting for its pilot. The fourth and final SayTopic will be available
under all circumstances. The player will thus have either two or three
responses to choose from at this point.
Next we can add a catch-all DefaultAnyTopic that won’t allow the player to leave the Conversation Node until he’s chosen one of the above SayTopics:
++ DefaultAnyTopic, ShuffledEventList
[
'<q>No, but answer my question,</q> she interrupts you. <q>What are you
doing in that uniform?</q> <.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> '
]
;
Note how we add a \<.convstay\>
tag to each of
the default responses to ensure that we remain in the current
Conversation Node. The next step is to prevent the player from breaking
off the conversation prematurely, which we can once again do with a
NodeEndCheck object:
++ NodeEndCheck
canEndConversation(reason)
{
switch(reason)
{
case endConvBye:
"<q>Oh no, you're not avoiding my question like that!</q> she tells
you. <q>Tell me, why are you wearing that pilot's uniform?</q> ";
return blockEndConv;
case endConvLeave:
"<q>You're not going anywhere until you tell me what you're doing in
that uniform!</q> {the subj angela} insists. ";
return blockEndConv;
default:
return nil;
}
}
;
The other thing the player could try to deflect Angela’s question is to carry out a whole lot of irrelevant non-conversational commands (even just a repeated WAIT or I or LOOK). To have Angela express her impatience at such a tactic we can allow her to insist on an answer on each turn we remain in the node and there hasn’t been a conversational exchange. For that purpose we can define a NodeContinuationTopic (which we also locate in the ConvNode in question):
++ NodeContinuationTopic
"<q><<one of>>I asked you a question<<or>>I'm still waiting for an
answer<<cycling>>,</q> {the subj angela} <<one of>> reminds
you<<or>> insists<<or>> repeats<<cycling>>. <q>Why are you wearing that
uniform?</q> "
;
Note the use of the \<\<one of\>\>
embedded
expression constructs to vary what’s displayed slightly on each
occasion. We could achieve greater variety by mixing in the
NodeContinuationTopic with a ShuffledEventList, say, but what we’ve done
here will suffice for now.
The final step is to define the Conversation Node the player is taken to next, the ‘intend-fly’ node:
+ ConvNode 'intend-fly'
commonResponse = "\b<q>Very well, then,</q> she sighs. <q>I suppose we don't
have too much choice now, do we? Just as long as you know what you're
doing...</q> "
;
++ YesTopic
"<q>Yes, why not?</q> you reply breezily. <q>You can't wait here all day --
Pablo Cortez and his merry crew won't stand for it, for one thing!</q>
<<location.commonResponse>>"
;
++ QueryTopic 'why not'
"<q>Why not?</q> you ask. <q>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!</q> You nod towards the gangsters and drug barons occupying the
passenger seats further down the aisle. <<location.commonResponse>>"
;
++ QueryTopic 'whether|if she has a better idea; you have'
"<q>Do you have a better idea?</q> you counter. <q>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!</q> <<location.commonResponse>>"
;
++ DefaultAnyTopic
"<q>Please answer my question,</q> she insists. <q>Do you really intend to
fly this plane?</q> <.convstay>"
;
++ NodeEndCheck
canEndConversation(reason)
{
switch(reason)
{
case endConvBye:
"<q>That's not an answer!</q> she complains. <q>Tell me, are
you proposing to fly this plane yourself?</q> ";
return blockEndConv;
case endConvLeave:
"<q>Don't walk off until you've told me whether you're proposing to
fly this plane,</q> {the subj angela} insists. <q>Well, are
you?</q> ";
return blockEndConv;
default:
return nil;
}
}
;
++ NodeContinuationTopic
"<q>I'd appreciate it if you answered my question,</q> {the subj angela}
insists. <q>Are you really proposing to fly this aircraft?</q> "
;
We’ve saved ourselves a bit of typing here by defining a
commonResponse
property on the ConvNode object
and then calling it from each of the TopicEntries located in it to
provide Angela’s response. Otherwise the pattern of this Conversation
Node pretty much follows that of the previous one, although we have
shown how Conversation Nodes can be chained together, a process that
could be continued in principle as long as we liked. We could also make
the conversation branch to different nodes depending on the topic chosen
by the player.
Game authors aren’t restricted to the Conversation Node coding patterns illustrated here, although the three shown above are likely to be the most common ones. The adv3Lite library aims to allow as much flexibility as possible in how you use Conversation Nodes in your own game. For a complete account, see the section on Conversation Nodes in the adv3Lite Library Manual.
adv3Lite Library Tutorial
Table of Contents |
The Art of Conversation >
Angela Wants Answers