Going Shopping
|
The next task is to add the shop. The definition can go straight after the code listed above (so that the shop exterior is placed in the jetty room). If you haven't already tried defining your own shop interior, you could do so now, remembering to add a counter and maybe some items for sale (which could just be Decoration objects for now). You could also try adding a bell on the shop's counter, which Heidi can ring for service.
Here's our version
|
"The small, timber-clad shop has an open door, above which is a sign
reading GENERAL STORE"
;
insideShop : Room 'Inside Shop'
"The interior of the shop is lined with shelves containing all sorts of
items, including basic foodstuffs, sweets, snacks, stationery, batteries,
soft drinks and tissues. Behind the counter is a door marked 'PRIVATE'. "
out = jetty
north asExit(out)
south : OneWayRoomConnector
{
destination = backRoom
canTravelerPass(traveler) { return traveler != gPlayerChar; }
explainTravelBarrier(traveler)
{ "The counter bars your way to the door. "; }
}
;
+ Decoration 'private door*doors' 'door'
"The door marked 'PRIVATE' is on the far side of the counter, and there
seems to be no way you can reach it. The other door out to the jetty is to
the north. "
;
+ Fixture, Surface 'counter' 'counter'
"The counter is about four feet long and eighteen inches wide. "
;
++ bell : Thing 'small brass bell' 'small brass bell'
"The bell comprises an inverted hemisphere with a small brass knob
protruding through the top. Attached to the bell is a small sign. "
dobjFor(Ring)
{
verify() {}
check() {}
action() {"TING!";}
}
;
+++ Component, Readable 'sign' 'sign'
"The sign reads RING BELL FOR SERVICE. "
;
+++ Component 'knob/button' 'knob'
"The knob protrudes through the top of the brass hemisphere of the bell. "
dobjFor(Push) remapTo(Ring, bell)
;
backRoom: Room
north = insideShop
;
|
|
VerbRule(Ring)
'ring' singleDobj
: RingAction
verbPhrase = 'ring/ring (what)'
;
modify Thing
dobjFor(Ring)
{
preCond = [touchObj]
verify() { illogical('{You/he} can\'t ring {that dobj/him}'); }
}
;
|
There are several ways this could be done; the way we shall use here probably isn't the simplest or the most elegant, it's simply one that lets us try out some features of the library we haven't met yet. In brief, we'll cause the ringing of the bell to trigger a SoundEvent. We'll then add a SenseConnector between the inside of the shop and the back room so that the SoundEvent can be detected by the shopkeeper even when she's in the back room, but we also need to make the shopkeeper a SoundObserver so she'll be receptive to the sound. We'll then have the sound trigger a daemon on the shopkeeper to make her walk into the shop one turn later (a fuse would have done just as well, so it doesn't much matter which we use here.)
This probably sounds rather complicated, if not downright incomprehensible, so let's take it one step at a time. First, we need to define the SoundEvent:
|
triggerEvent(source)
{
"TING!<.p>";
inherited(source);
}
;
|
|
action() { bellRing.triggerEvent(self); }
|
|
|
connectorMaterial = paper
locationList = [backRoom, insideShop]
;
|
Now all we have to do is to define the shopkeeper. At this point we shan't program all her behaviour, just what's needed to get her to respond to the bell ring:
|
@backRoom
"The shopkeeper is a jolly woman with rosy cheeks and fluffy blonde curls. "
isHer = true
properName = 'Sally'
notifySoundEvent(event, source, info)
{
if(event == bellRing && daemonID == nil && isIn(backRoom))
daemonID = new Daemon(self, &daemon, 2);
|
"<q>All right, all right, here I am!</q> says {the
shopkeeper/she}.<.p>";
}
daemonID = nil
daemon
{
moveIntoForTravel(insideShop);
"{The shopkeeper/she} comes through the door and stands behind the
counter. ";
daemonID.removeEvent();
daemonID = nil;
}
globalParamName = 'shopkeeper'
;
|
The complicated part is setting up the daemon. A new daemon is created with a call to new Daemon(obj, prop, interval), where obj is the object it refers to, prop is the method on that object that is called each time the daemon is invoked, and interval is the number of turns between each invocation of the daemon. Here we define the daemon to run the daemon method (note that the parameter is supplied as &daemon) on self (the shopkeeper) every second turn (this means she won't come into the shop until the turn after the bell is rung). Since we want to be able to stop the daemon again we need to store a reference to the daemon, which we do in the property daemonID (note that we could have called the daemon method and the reference property anything we liked).
The daemon method first moves the shopkeeper into the shop and displays a suitable message to announce her arrival. We use moveIntoForTravel rather than moveInto to move the shopkeeper since with the latter the library code tries to find a path to move her through, and may well end up moving her through the SenseConnector with dire consequences (i.e. a runtime error); moveIntoForTravel avoids this problem. Once the shopkeeper has moved the daemon has done its work, so we get it to tidy up after itself, first by calling daemonID.removeEvent(), and finally by resetting daemonID back to nil so we can easily test for there no longer being an active daemon.
In this particular case we could have achieved the same effect slightly easier by using a fuse rather than a daemon. Instead of
|
daemonID = new Daemon(self, &daemon, 2);
|
|
|
daemonID = new Fuse(self, &daemon, 1);
|
|
|
daemonID.removeEvent();
|
|
Having reached this point, we can start expanding the definition of the shopkeeper using ActorStates and TopicEntries as with Joe the Charcoal Burner; you might like to try this out for yourself before reading this guide's version over the page.
|
specialDesc = "{The shopkeeper/she} is standing behind the counter
talking with you. "
stateDesc = "She's standing behind the counter talking with you. "
nextState = sallyWaiting
;
++ sallyWaiting : ConversationReadyState
specialDesc = "{The shopkeeper/she} is standing behind the counter,
checking the stock on the shelves. "
stateDesc = "She's checking the stock on the shelves behind the counter. "
isInitState = true
takeTurn
{
if(!gPlayerChar.isIn(insideShop) && shopkeeper.isIn(insideShop))
shopkeeper.moveIntoForTravel(backRoom);
inherited;
}
;
+++ HelloTopic
"<q>Hello, <<getActor.isProperName ? properName : 'Mrs Shopkeeper'>>,</q>
you say.<.p>
<q>Hello, <<getActor.isProperName ? 'Heidi' : 'young lady'>>, what can
I do for you?</q> asks {the shopkeeper/she}."
;
+++ ByeTopic
"<q>'Bye, then!</q> you say.<.p>
<q>Goodbye<<getActor.isProperName ? ', Heidi' : nil>>.
See you again soon!</q> {the shopkeeper/she} beams in return. "
;
+++ ImpByeTopic
"{The shopkeeper/she} turns away and starts checking the stock on the
shelves.<.p>"
;
|
The takeTurn() method is called once every turn that this is Sally's current ActorState. Here we use it to check whether Heidi is still inside the shop; if she isn't, and Sally still is, then we send Sally back to her back room. It may occur to you that the takeTurn method is effectively a kind of daemon; to produce the effect of Sally coming into the shop the turn after the bell is rung, we could simply have added a few extra lines of code to this takeTurn method, perhaps in conjunction with a custom property. We could have dispensed with the whole mechanism of SoundEvent and SenseConnector, and simply have added a line of code in the dobjFor(Ring) method of the bell to change the value of the custom property which the additional code in the takeTurn() method could test for. But then we'd have lost the opportunity to look at sensory events, sense connectors, fuses and daemons. If you want to try to do it the simpler way, by all means experiment.
|
|
initiateConversation(sallyTalking, 'sally-1');
|
|
|
npcGreetingMsg = "<q>Right, what can I get you?</q> she asks. <.p>"
;
|
|
"<q>I'm Heidi. What's your name?</q> you ask.<.p>
<q>Hello, Heidi; I'm <<shopkeeper.properName>>,</q> she smiles.
<<shopkeeper.makeProper>>"
;
+++ AltTopic
"<q>I'm feeling really <i>very</i> well today; how are you?</q> you
ask.<.p>
<q>I'm feeling very well too, thanks.</q> she tells you. "
isActive = (shopkeeper.isProperName)
;
++ AskTellTopic @burner
"<q>Do you know {the burner/him}, the old fellow who works in the
forest?</q> you enquire innocently.<.p>
<q>He's not <i>that</i> old,</q> she replies coyly. "
;
++ AskTellTopic @tWeather
"<q>Lovely weather we're having, don't you think?</q> you remark.<.p>
<q>Absolutely,</q> she agrees, <q>and with luck, it should stay fine
tomorrow.</q>"
;
++ DefaultAskTellTopic, ShuffledEventList
[
'<q>What do you think about ' + gTopicText + '?</q> you ask.<.p>
<q>Frankly, not a lot.</q> she replies. ',
'<q>I think it\'s really interesting that...</q> you begin.<.p>
<q>Oh yes, really interesting.</q> she agrees. ',
'You make polite conversation about ' + gTopicText + ' and
{the shopkeeper/she} makes polite conversation in return. '
]
;
Most of this should be fairly familiar. Note that placing a list in square brackets, as in the [shopkeeper, gPlayerChar] in the first AskTellTopic means that the topic can be triggered by any of the objects in the list; so this topic will work equally well for ask shopkeeper about herself or tell shopkeeper about yourself. Note also the use of string concatenation (joining strings together with the + operator) in the DefaultAskTellTopic to allow the use of a variable element (gTopicText) in an EventList. The other slight novelty (unless you already experimented with it at the end of the previous chapter) is the use of a Topic object to talk about the weather; since the weather is not a physical object defined anywhere in the game, we don't have a game object to match it to. To cope with this type of situation, where you want to be able to converse about things that are not game objects, there is a special Topic class. In this case all we need define is:
|
|
|
isKnown = nil
;
When the player then learns of the murder at a later point one could use the gSetKnown(tMurder) macro to set tMurder.isKnown = true.
Getting Started in TADS 3
[Main]
[Previous] [Next]