The Flight Attendant — Getting in a State
The third and final NPC we'll implement is the flight attendant, who'll also be the most complex of the three. For a change we'll implement her as a female NPC, and her name, which the player character may or may not learn, will be Angela. Once again we'll start by defining the basic Actor object:
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' ;
The one new feature we've introduced here is the globalParamName property. Because the player character may learn the flight attendant's name at some point during the course of the game, it's hard to know when text that mentions her should call her 'the flight attendant' and when it should call her 'Angela'. By giving her a globalParamName we can use a message substitution parameter like '{the subj angela}' whenever we need to refer to her, and either 'the flight attendant' or 'Angela' will be substituted for it, according to the current value of her name property. At the start of the game, she starts out as 'The flight attendant'. The player character can only learn her name once we've implemented some conversational responses, which we'll leave until Chapter 11.
Note that the value we give to the globalParamName can be any single-quoted string we like, so long as it's not the same as any other globalParamName in the game. In particular, we didn't have to give Angela a globalParamName of 'angela' just because that's what her name is. It's usually a good idea, however, to use a globalParamName that corresponds to the object name, since that generally helps makes your code more readable. If we see '{The subj angela}' in a message somewhere, it'll be pretty obvious it refers to the angela NPC. If we saw '{The subj attendant}' we'd probably make the same deduction, but it would then be a bit too easy to get confused and write {The subj angela} somewhere else, because we wouldn't be following such a consistent convention. If we wrote '{The subj blonde}' or '{The subj foo}' we might rather too easily lose track of who it was meant to refer to.
Thus globalParamName creates a label that can be used in message substitutions, and later we'll see how we can change in the middle of the game what that label refers to.
Otherwise this definition looks even simpler than those of the previous two NPCs, but that's because most of the complexity is going to be farmed out to ActorState objects. While Pablo Cortez and the security guard don't change much over the course of the game — Cortez only makes a relatively fleeting appearance in one scene, and the guard just keeps on guarding — the flight attendant will be in different states at different points in the game. When we first meet her she'll be greeting passengers as they board the plane and asking to see their tickets. During the takeover scene she'll be standing in the jetway seeing to the disembarking passengers, or else talking with the player character. Finally, she'll be back aboard the plane to look after the new passengers, effectively as their hostage.
Rather than trying to represent these four different states with a lot of if-statements or switch-statements on the Actor object, we'll represent each of them with an ActorState. As its name hopefully implies, an ActorState is a special kind of object used to track the state of an Actor. During the course of a game, an NPC may change from doing one thing to another, stacking cans at one moment, sweeping the floor at another, talking with the player character at another, and maybe walking down the street at another. The NPC needs to be described differently on each occasion. In a room description one would need to see "Bob is busily stacking cans" or "Bob is sweeping the floor" or "Bob is watching you, waiting for you to speak" or "Bob is walking rapidly down the street". Ordinarily such paragraphs would come from an object's specialDesc() property, but in the case of Bob we'd need to write quite a complex specialDesc that varied according to state. This would become more complicated (and messier) the more different activities Bob could be engaged in over the course of the game. And it isn't just his specialDesc that needs to vary with what Bob is doing or the state he's in. The way he reacts to things (including attempts by the player character to talk with him) may differ quite a lot depending on whether he's calmly stacking cans, desperately trying to put a fire out, or sound asleep in an alcoholic stupor. Rather than writing lots of complex code on the Bob object to cater for these differences, we can encapsulate Bob's different activities or states by representing each one of them by an ActorState object. The Actor then uses its current ActorState object to decide how to respond.
Every Actor contains a property called curState that serves as a slot to point to the ActorState object corresponding to its current state. You can think of an ActorState object as a bundle of methods (and properties) meant to replace the usual Actor methods while the Actor is in that state.
You don't have to use ActorState objects. For really simple NPCs like the security guard and Pablo Cortez, they aren't really needed. But if an Actor does have a current ActorState then the Actor object gets the value of various of its properties and methods from the corresponding property on its current ActorState object. For example, Actor.specialDesc() simply calls curState.specialDesc() if curState is not nil (if it is nil it calls actorSpecialDesc instead). To illustrate this by means of a crude flowchart:
+--------------------+ | Actor.specialDesc | +--------------------+ | / \ / \ / \ / \ / \ / Does the \ +------------------+ NO / actor have \ YES +-----------------------+ | Use |<---< a current > ----> | Use | | actorSpecialDesc | \ ActorState? / | curState.specialDesc | +------------------+ \ / +-----------------------+ \ / \ / \ / \ / \ / V
You can see from this flowchart that it's a bad idea to override actor.specialDesc because that's where the switching logic resides and if you override it you'll break the code that reroutes specialDesc to the curState when available. Furthermore, you can see that actor.actorSpecialDesc is where you want to place the fallback special description that should hold whenever no special state is in effect. The same pattern holds for the following properties and methods of actor:
- specialDesc
- stateDesc (an extra description that can be tacked on to the desc defined on the Actor)
- beforeAction()
- afterAction()
- beforeTravel()
- afterTravel()
As previously mentioned, we can see what ActorState an Actor is currently in from the value of its curState property (which is allowed to be nil, if there is no ActorState currently associated with the Actor). If you want an Actor to start out in a particular state, you can just set isInitState = true on the ActorState in question. Like AgendaItems, ActorStates are associated with their Actor by being located within them using the + syntax. We can thus define Angela's initial ActorState like this:
+ 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; default: break; } } } ticketSeen = nil ;
The specialDesc defined on this ActorState is how the flight attendant will be shown in the room description when the player character first encounters her. The stateDesc will be added to the Actor's desc when she's examined, so that X FLIGHT ATTENDANT will initially produce "She's a statuesque and by no means unattractive young woman. Right now, she's wearing a fixed professional smile." We use the beforeTravel() method on this ActorState to make Angela prevent the player character from venturing into the cockpit, or moving further into the plane until Angela has seen his ticket, for which purpose we define a custom ticketSeen property on the ActorState object.
Angela's other three ActorStates can be defined in like manner, though obviously without defining isInitState = true on any of them, and without any need for a beforeTravel() method:
+ 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. " ; + angelaTalkingState: ActorState specialDesc = "{The subj angela} {is} facing you, waiting for you to speak. " ; + angelaSeatedState: ActorState specialDesc = "{The subj angela} {is} sitting near the front of the plane. " stateDesc = "Right now, though, she's looking worried and afraid. " ;
Our final task in this chapter is to make sure that the flight attendant ends up in the right state (and the right place) at the right time. To change an Actor's current ActorState you should always call its setState(state) method. We could do this in the whenStarting() and whenEnding() methods of the takeover scene, but we'll once again use a couple of AgendaItems to do the job:
+ 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); } ;
If you compile and run the game now, you'll find that there's now no way for the player character to reach the rear of the plane and trigger the takeover scene, other than by using debugging commands to teleport around the map. What we need is one more AgendaItem that is triggered when the player character first boards the plane that causes the flight attendant to ask for the player character's ticket. At the moment we don't have the means for him to show her the ticket, so we'll temporarily assume that the attendant is satisfied if she can see it. For this purpose we'll use a ConvAgendaItem, which is triggered as soon as the player character is in a position to speak to the actor concerned, which in this case will happen as soon as he enters the front of the plane:
+ angelaTicketAgenda: ConvAgendaItem
initiallyActive = true
invokeItem()
{
isDone = true;
"<q>Welcome aboard, sir,</q> {the subj angela} greets you with a smile.
<q>May I see your ticket please?</q> ";
/* Temporary code until we reach Chapter 11 */
if(ticket.isDirectlyIn(me))
{
"She glances down at the ticket in your hand, and temporarily takes
it off you to check it. <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;
}
}
;
That's fine apart from one thing: if the player character boards the plane without the ticket, he won't get a second chance to show it to the flight attendant and the game will be in an unwinnable state. The best way to cure that is to have the beforeTravel() method of angelaGreetingState reset angelaTicketAgenda (i.e. add it to Angela's agendaList again) if the player character leaves the plane when Angela hasn't seen the ticket:
+ 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 ;
And that's where we'll leave Angela and her ActorStates for now, although we'll find further uses for them in the next chapter when we come to implement some conversation. In the meantime, if you'd like more information on ActorStates, see the adv3Lite Library Manual.