Dropping objects from the tree

Once Heidi reaches the top of the tree the player may encounter another problem, or at least a point at which the behaviour of the game appears to defy reality. If Heidi drops the bird or the nest while at the top of the tree the object will appear to remain suspended in mid-air, as it were. One solution might be to assume that anything dropped at the top of the tree is put on the branch, and we could implement that. This would be another good use of a Doer:

Doer 'drop Thing'
    execAction(curCmd)
    {
        curCmd.action.beforeAction();
        doInstead(PutOn, gDobj, branch);
    }
    
    where = topOfTree
;

Here 'drop Thing' will match the attempt to drop any Thing (in this context either the bird or the nest, since these are the only two portable objects in the game). The condition where = topOfTree ensures that this Doer only takes effect when the player character is at the top of the tree; when she's anywhere else we want the DROP command to work as normal (incidentally we could supply a list of rooms, such as [topOfTree, clearing], here if we wanted our Doer to take effect in more than one room). We call curCmd.action.beforeAction(); at the start of the execAction(curCmd) method to ensure that the before action notifications take place; while that isn't strictly necessary here it's a good habit to get into. We then call doInstead(PutOn, gDobj, branch); to turn the DROP command into a PutOn action. The first parameter we must pass is the the new action we want the player character to carry out, in this case the PutOn action (the action that would normally be triggered by a command of the form PUT X ON Y). We can then specify the two objects we want the PutOn action to work with; here gDobj means the existing direct object of the command..

With the PutOn action the direct object is the object being placed (the bird or the nest) and the indirect object is the object on which we want to place it (here the branch). In this instance the direct object doesn't chabge, since it will be the same as the direct object of the original DROP command (if Heidi drops the nest, it's the nest that should be put on the branch, and if she drops the bird, it's the bird that needs to be put on the branch), so we can just specify it as gDobj (the existing direct object). But we do need to specify the particular new indirect object of the command (here the branch), otherwise the doInstead() method won't know what to put the direct object on.

This is a perfectly good use of a Doer and a perfectly decent solution to the problem of what should happen to objects dropped at the top of the tree, but it's not the solution employed in the Inform Beginner's Guide, from which Heidi has been borrowed. One reason for that might be that it makes it a bit too easy for the player to win the game if DROP NEST effectively turns into the winning move PUT NEST ON BRANCH. Another might be that everywhere else in this game (and most others) DROP SOMETHING results in something falling to the ground, not in something being placed elsewhere. It could thus be argued that for the sake of consistency that's what should happen here: anything dropped at the top of the tree should fall to the ground, the problem in this case being that the ground is in the clearing at the bottom of the tree.

We therefore need to find some way of intercepting the DROP command to make objects dropped at the top of the tree fall to the clearing below. We could use a Doer to do this, but here we'll illustrate another way, using the roomBeforeAction() method of the location to intercept an action just before it takes place:

topOfTree: Room 'At the top of the tree'
    "You cling precariously to the trunk. "
    
    down = clearing
    
    cannotGoThatWay(dir)
    {
        "The only way from here is down. ";
    }
    
    roomBeforeAction()
    {
        if(gActionIs(Drop))
        {
            gDobj.actionMoveInto(clearing);
            "{The subj dobj} {falls} to the ground below. ";
            exit;
        }
    }
;

Now let's explain how this works. The roomBeforeAction() method is called on the player character's current location just before she's about to carry out an action. The test if(gActionIs(Drop)) checks whether the current action is a Drop action; if it is, the statements in braces immediately following the if-statement are executed (while if it isn't, the roomBeforeAction() method simply finishes its work without doing anything else). The statement gDobj.actionMoveInto(clearing); moves the direct object of the Drop command (either the bird or the nest) into the clearing; note the use of gDobj to get at the direct object of the current action. We could have just used moveInto(clearing) here, but using actionMoveInto() is a good habit to get into in this kind of situation, to ensure that the appropriate notifications are triggered (yes I know I keep on mentioning 'notifications' without explaining what I mean, but there's a limit to how many things I can explain at once without confusing you, and this one will just have to wait till a more opportune time; but if you really want to know about them now you could look at the section on Reacting to Actions in the adv3Lite manual).

The next statement, "{The subj dobj} {falls} to the ground below. ";, tells the player what's just happened. Since we don't know what object Heidi will drop (in this game it can only be the bird or the nest, but in a bigger game it could be one of a large number of things) we need to write this message in such a way that it will accurately refer to the dropped object whatever it is. We do this with the message substitution parameter {The subj dobj} which means "the direct object of the command, taken as the subject of the verb that follows". The "The" at the beginning tells the game to use the definite article (if appropriate; it would not be with proper names) with the first letter capitalized (appropriately for the start of the sentence). The next token , {falls}, tells the game to use the correct form of the verb 'to fall' to agree with the subject. We could just have written 'falls' here (without the curly braces), since in this game the subject of the sentence can only be the bird or the nest, so we know it'll always be singular. In a bigger game that might not always be the case, however. If Heidi was also carrying some nuts, and dropped them from the top of the tree you'd want the player to see "The nuts fall to the ground below" not "The nuts falls to the ground below", and by placing 'falls' in curly braces we ensure that that's what you'd get. Note, however, that this only works for verbs the library knows about, which is quite a few of the more common ones but by no means all (in fact I had to add 'falls' to the library for this example). If, for example, you had written "{The subj dobj} {plummets} to the ground below. "; it wouldn't have worked, because 'plummet' isn't one of the verbs the library knows how to conjugate (although you could add it to the library's stock of verbs just for your game, but we'd be getting ahread of ourselves if we went into how right now).

A message substitution parameter is thus a fancy name for a piece of text in curly braces that the library knows how to translate into something more relevant and useful before displaying it to the player (for example {The subj dobj} {falls} will become either "The bird's nest falls" or "The baby bird falls" depending on which object Heidi drops). If you're anxious to find out more about message substitution parameters straight away you can read all about them in the adv3Lite manual, where you can find a complete list.

The final statement, exit;, prevents the rest of the action from happening; otherwise we'd see the message "Dropped" after the message about the object falling to the ground below, and the object would actually end up in topOfTree instead of clearing, since that's where the standing handling for Drop would put it. The difference between exit and abort is that exit stops the action in its tracks but allows the rest of the turn cycle (Daemons, Fuses and advancing the turn count, but not the after action notifications) to go ahead, while abort additionally skips the entire remainder of the turn cycle (so that it doesn't even count as a turn).

Now try compiling and running the game again to check that everything once again behaves as you expect.