Table of Contents |
The Art of Conversation >
Summary
Summary
We’ve now taken the Angela NPC as far as we need to for the purposes of this tutorial, though she’s not really complete (try asking her about flight departures and the pilot after the player character’s made it clear he intends to fly the plane, for example; the exchanges are then rather incongruous). We could certainly add a bit more polish, and we might want to extend her conversational range, but these can be left as exercises for the interested reader (they might be quite useful exercises if you want more practice at using the adv3Lite conversation system).
As you’ll see from the listing below, the code for Angela has already become quite complex. This is inevitable if you want to implement an NPC of any sophistication. Writing even a reasonable approximation to realistic conversation in IF is a lot of work; an authoring system can provide you with the tools for the job, but it can’t do the work for you. What adv3Lite does do (following principles borrowed from adv3) is to allow you to program conversations in a largely declarative style spread over a large number of objects. This avoids spaghetti coding with lots of if-branches and case statements, and makes your code easier to write, read and maintain. If you read through the complete listing below you’ll see there’s actually very little procedural code; it’s mainly a matter of defining objects and their properties. This makes it about as easy for you as an IF conversation system can make it.
That said, there’s no getting away from the fact that the conversation system in adv3Lite is quite complex; it is easily the most complex part of the library. But it is also scalable, which means you don’t have to use all the complexity if you don’t want to; you can just use the bits you need for your own particular game. We should also add that although we’ve covered most of the main features of the adv3Lite conversation system in this chapter, we haven’t covered them all. The system is intended to be highly flexible to allow you to write the kinds of conversation you want to write. You don’t have to follow the coding patterns illustrated in this chapter, although they’ll often prove useful.
Probably the next step is for you to read through the entire Actors part of the adv3Lite Library Manual to refresh your memory and see what else is there, and then perhaps (or in parallel, perhaps) try increasing Angela’s conversational range a bit more. In the meantime, here’s a brief summary of what we’ve covered (and what we’ve missed) in this chapter, with links to the relevant sections of the adv3Lite Library Manual.
At its simplest, conversation in adv3Lite can be implemented as a Basic
Ask/Tell system using various kinds of
TopicEntry objects (such as AskTopic
and TellTopic). If you like, you can suggest
certain topics of conversation to the player by giving your TopicEntries
a name
property. The availability of
TopicEntries to respond to the player’s conversation commands depends on
a number of factors, including which
ActorState the NPC is in, the
isActive
property of the TopicEntry and the
convKeys
property, which can be used for a
variety of purposes. Where several TopicEntries share the same values of
these properties it can be useful to group them under a common
TopicGroup.
There are various ways you can make things more elaborate. A
SayTopic allows the player
character to say just about anything to an NPC (within reason!), while a
QueryTopic allows the player
character to ask a wide range of much more specific questions than is
possible with an AskTopic (users familiar with adv3 might like to know
that there is no restriction on where these two types of TopicEntry may
be used, unlike an adv3 SpecialTopic). A particular point in the
conversation at which particular responses or questions become
momentarily appropriate is called a Conversation
Node and can be most conveniently implemented
using a combination of a ConvNode
object and a
\<.convnode\>
tag. In many situations it is
also appropriate to implement Greeting Protocols,
whereby conversations are properly begun and ended with some equivalent
of “hello” and “goodbye” and the NPC can optionally change between
conversational and non-conversational ActorStates.
In order to ensure that a conversational exchange remains sensible and
appropriate, it’s often necessary to keep track of what both the player
character and the NPC s/he’s talking to currently know. Player Character
and NPC Knowledge can be tracked using
\<.reveal key\>
and
\<.inform key\>
tags, and tested with
gRevealed(key)
and
gInformed(key
), typically used on the isActive
property of a TopicEntry (or perhaps a TopicGroup).
A couple of topics we only touched on were Giving Orders to
NPCs (e.g. BOB, PUT THE BALL IN THE BOX) and
NPC-Initiated Conversation. Orders given to
NPCs are typically handled by CommandTopics
and DefaultCommandTopics
, which are similar in
principle to other TopicEntries but can be a little more complex to
specify. One way we’ve seen for an NPC to initiate a conversation is via
a ConvAgendaItem. Another,
which we didn’t cover, might be through an
InitiateTopic. A particularly
sophisticated technique (which again we haven’t covered in this
tutorial) is to combine a ConvAgendaItem
with
a DefaultAgendaTopic, which
allows an NPC to pursue his or her own conversational agenda instead of
giving a canned default response when the player tries a conversational
command that hasn’t otherwise been specifically catered for; instead of
giving a disguised version of “I haven’t been programmed to respond in
that area”, the NPC can take the opportunity to seize the conversational
initiative.
Finally, we have met a number of tags that can be used in conversation, such as <.reveal>, but there are several more that we haven’t covered in this tutorial that can be used for a variety of purposes. A full list is provided in the NPC Overview section of the manual.
Complete Angela Listing
Since some readers may have found it a little hard to keep track of exactly what goes where, here’s a complete listing of all the code related to the Angela NPC as far as we have reached:
angela: Actor 'flight attendant; statuesque young; woman angela; her'
@planeFront
"She's a statuesque and by no means unattractive young woman. "
checkAttackMsg = 'That would be cruel and unnecessary. '
globalParamName = 'angela'
makeProper
{
proper = true;
name = 'Angela';
return name;
}
suggestionKey = 'top'
;
+ TopicGroup 'top';
++ AskTopic @angela
keyTopics = 'angela'
name = 'herself'
;
++ QueryTopic 'when' 'this plane is going to leave; depart take off'
"<q>When is this plane going to leave?</q> you ask.\b
<q>Just as soon as the pilot comes aboard,</q> she tells you. <.reveal
pilot-awaited> "
askMatchObj = tFlightDepartures
;
++ AskTopic @tPilot
"<q>What's happened to the pilot?</q> you ask.\b
<q>I don't know; we're still waiting for him,</q> she replies. <q>But don't
worry; I'm sure he'll turn up any moment now.</q> "
autoName = true
isActive = gRevealed('pilot-awaited')
;
+ QueryTopic 'what' 'her name is; your'
"<q>What's your name?</q> you ask.\b
<q><<getActor.makeProper>>,</q> she replies. "
isActive = !getActor.proper
convKeys = 'angela'
;
+ 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'
;
+ 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. "
;
+ QueryTopic 'when' 'this plane is going to leave; depart take off'
"<q>When is this plane going to leave?</q> you ask.\b
<q>Just as soon as the pilot comes aboard,</q> she tells you. <.reveal
pilot-awaited> "
askMatchObj = tFlightDepartures
;
+ DefaultAskForTopic
"{The subj angela} listens to your request and shakes her head. <q>Sorry, I
can't help you with that,</q> she says. "
;
+ DefaultCommandTopic
"<q><<if angela.proper>>Angela<<else>>Miss<<end>>, would you
<<actionPhrase>>, please?</q> you request.\b
In reply she merely cocks an eyebrow at you and looks at you as if to say,
<q>Who do you think you're talking to?</q> "
;
+ 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, <q>I'm afraid I can't accept {that dobj} from you,
sir.</q> "
;
+ DefaultShowTopic
"You point towards {the dobj}.\b
<q>Very interesting, I'm sure, sir,</q> {the subj angela} remarks without
much enthusiasm. "
isActive = gDobj.isFixed
;
+ TopicGroup
isActive = getActor.curState == angelaSeatedState
;
++ DefaultAskQueryTopic
"<q>That question's too difficult for me!</q> she declares. "
;
+ angelaGreetingState: ActorState
isInitState = true
specialDesc = "{The subj angela} {is} standing just inside the entrance
greeting passengers as they board. "
stateDesc = "Right now, she's wearing a fixed professional smile. "
beforeTravel(traveler, connector)
{
if(traveler == me)
{
switch(connector)
{
case cockpitDoor:
"<q>I'm afraid you can't go in there, sir,</q> {the subj angela}
stops you. <q>Only flight crew are allowed in the cockpit.</q>.
";
exit;
case planeRear:
if(!ticketSeen)
{
"<q>I'm afraid I can\'t let you board the plane till I\'ve
seen your ticket, sir,</q> {the subj angela} insists. ";
exit;
}
break;
case jetway:
if(!ticketSeen)
getActor.addToAgenda(angelaTicketAgenda);
break;
default:
break;
}
}
}
ticketSeen = nil
;
++ GiveShowTopic @ticket
topicResponse()
{
"<q>Here you are,</q> you say, holding out the ticket for {the angela}
to see.\b
She glances down at the ticket in your hand, and temporarily takes it
off you to check. <q>That's fine, sir,</q> she assures you as she
returns it to you. <q>Please move to the rear of the plane to find a
seat.</q> ";
angelaGreetingState.ticketSeen = true;
}
;
++ QueryTopic 'if|whether' @tEnjoyWork
"<q>Do you enjoy your work?</q> you ask.\b
<q>Of course, sir,</q> she replies with a bland smile. "
convKeys = 'angela'
;
+ TopicGroup +5
isActive = angela.curState == angelaGreetingState &&
!angelaGreetingState.ticketSeen
;
++ DefaultAskQueryTopic
"<q>I really need to see your ticket, sir,</q> she insists <<one
of>>politely<<or>>once more<<stopping>>. "
;
++ DefaultSayTellTalkTopic
"{The subj angela} listens <<one of>>politely<<or>>a little impatiently
<<stopping>> to what you have to say, then replies, <q>May I see your
ticket, sir?</q> "
;
+ TopicGroup +5
isActive = angela.curState == angelaGreetingState &&
angelaGreetingState.ticketSeen
;
++ DefaultAskQueryTopic
"<q>If you have any further questions perhaps you could ask them once we're
in flight,</q> she <<one of>>suggests<<or>>repeats<<stopping>>. <q><<one
of>>It would be best if you moved <<or>>Please move<<stopping>> to the
rear of the plane and <<one of>>took<<or>>take<<stopping>> your seat now,
sir.</q> "
;
++ DefaultSayTellTalkTopic
"{The subj angela} holds up her hand to stop you in mid-flow. <q>Can I ask
you to move to the rear of your plane and take your seat now, sir?</q> she
<<one of>>requests<<or>>repeats<<or>>insists<<stopping>>. "
;
+ angelaAssistingState: ActorState
specialDesc = "{The subj angela} {is} standing in the middle of the jetway,
trying to calm the passengers who have just been forced off the plane. "
stateDesc = "Right now, she's looking rather harrassed. "
;
++ HelloTopic, StopEventList
[
'<q>Excuse me, might I have a word?</q> you say.\b
{The subj angela} turns to you with a fixed smile, no doubt mentally
preparing herself for another barrage of complaints. <q>Yes; how can I
help?</q> she replies. ',
'<q>Might I have another word?</q> you ask.\b
<q>Yes?</q> she replies, turning to you just a little warily. '
]
changeToState = angelaTalkingState
;
+ angelaTalkingState: ActorState
specialDesc = "{The subj angela} {is} facing you, waiting for you to speak.
"
;
++ QueryTopic 'if|whether' @tEnjoyWork
"<q>Do you enjoy your work -- at times like these?</q> you ask.\b
<q>At times like these...</q> she leaves the sentence unfinished with an
expressive grimace. "
convKeys = 'angela'
;
++ ByeTopic
"<q>Well, cheerio for now then,</q> you say.\b
<q>Goodbye,</q> she replies with a brisk nod, before turning to yet another
importuning displaced passenger anxious for her attention. "
changeToState = angelaAssistingState
;
++ LeaveByeTopic
"{The subj angela} looks momentarily taken aback at your somewhat abrupt
departure, but quickly turns back to the other passengers clamouring for
her attention. "
changeToState = angelaAssistingState
;
++ 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>\b
Her smile becomes rather frosty as she replies, <q>What\'s that to
you?</q> <.inform cortez> <.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
;
+ 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><.inform cortez-dangerous>\b
<q>Anyone waving a gun around aboard a passenger aircraft might be
considered dangerous,</q> she points out pragmatically. "
;
++ 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> "
;
++ 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. '
]
;
++ 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;
}
;
+ TopicGroup +5
isActive = angela.curState == angelaTalkingState
;
++ DefaultAskQueryTopic, ShuffledEventList
[
'{The subj angela} mutters something inaudible and looks round, as if
dropping a heavy hint that she has other people besides you to attend
to. ',
'<q>Maybe we can discuss that some other time,</q> she suggests, with
a significant glance at the other passengers anxious to attract her
attention. ',
'<q>Hm, well,</q> she says, in a tone of voice that rather suggests
she has more urgent things on her mind. ',
'<q>I think perhaps...</q> she begins, and then trails off as one of the
other passengers taps her on the arm in an attempt to grab her
attention. '
]
;
++ DefaultSayTellTalkTopic
"{The subj angela} listens to what you have to say without comment, but with
the air of one who has other things on her mind. "
;
+ angelaSeatedState: ActorState
specialDesc = "{The subj angela} {is} sitting near the front of the plane. "
stateDesc = "Right now, though, she's looking worried and afraid. "
;
++ QueryTopic 'if|whether' @tEnjoyWork
"<q>Are you enjoying your work now?</q> you ask.\b
<q>I'll be glad when this particular flight is over,</q> she replies
quietly. "
convKeys = 'angela'
;
++ QueryTopic, StopEventList 'what' @tDoingTonight
[
'<q>What are your plans for tonight now?</q> you ask.\b
<q>I\'m not sure,</q> she replies, just a little nervously. <q>I think
I\'d rather wait until this plane has safely landed at its destination
and -- well, you know.</q> She indicates the new set of passengers with a
flick of her eyes. <q>I think I\'d rather wait until this is all over
before making any further plans.</q> ',
'<q>About later tonight...</q> you begin.\b
<q>Let\'s discuss it when we\'ve arrived at the other end,</q> she
insists. '
]
convKeys = 'angela'
;
+ TopicGroup +5
isActive = angela.curState == angelaSeatedState
;
++ DefaultAskQueryTopic, ShuffledEventList
[
'{The subj angela} lowers her voice and swivels her eyes just enough to
remind you of the other people in earshot. <q>Perhaps we should discuss
that some other time,</q> she suggests. ',
'<q>I don\'t think I care to answer that right now,</q> she replies,
with just enough movement of the head to indicate how easily you might
be overheard by the hoodlums in the other passenger seats. ',
'<q>I think...</q> she begins, and then breaks off. <q>I think this may
not be the best time to talk about that,</q> she concludes. ',
'<q>Hm,</q> she says, <q>right.</q> It\'s obviously intended as a
non-answer, perhaps because she\'s worried about who else might hear
what she says. '
]
;
++ DefaultSayTellTalkTopic
"{The subj angela} merely listens, looking faintly disapproving at your
garrulousness. "
;
+ angelaAssistingAgenda: AgendaItem
initiallyActive = true
isReady = (takeover.isHappening)
invokeItem()
{
isDone = true;
getActor.moveInto(jetway);
getActor.setState(angelaAssistingState);
getActor.addToAgenda(angelaReboardingAgenda);
}
;
+ 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> ";
}
;
+ 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> "
;
++ 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> '
]
;
++ 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;
}
}
;
++ 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> "
;
+ 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 gansgters 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> "
;
+ angelaTicketAgenda: ConvAgendaItem
initiallyActive = true
invokeItem()
{
isDone = true;
"Welcome aboard, sir, {the subj angela} greets you with a smile.
May I see your ticket please? ";
}
;
adv3Lite Library Tutorial
Table of Contents |
The Art of Conversation >
Summary