Responding to Actions

In the previous section we taught the game how to respond in general to commands like PUSH STICK FORWARD and TURN WHEEL TO PORT, but we've yet to get the wheel and the stick to respond in a customized way appropriate to their role in the game. This will be our next task.

Turning the Wheel

For the moment we'll just worry about what effect turning the wheel has on the wheel. We'll worry about what it might do to the plane later.

To keep things reasonably but not absurdly simple, we'll assume the wheel can be turned to one of five positions: amidships, 30 degrees to port or starboard, or 60 degrees to port or starboard. We'll also assume that a TURN WHEEL LEFT/RIGHT command should turn it 30 degrees at a time. To keep track of where the wheel is currently turned to we'll define a custom angle property on the wheel object, which will be allowed to vary from -60 (hard to port) to +60 (hard to starboard). To help the player keep track of the wheel we'll also define a custom angleDesc() method that turns this angle into a description that can be used both in the description of the wheel (in response to EXAMINE WHEEL) and in the response to a TURN WHEEL LEFT or TURN WHEEL RIGHT command.

To prevent the wheel from being turned too far to the right or left, we'll use the check() phase of dobjFor(TurnRight) and dobjFor(TurnLeft). The former will stop the action if angle is already greater than or equal to 60, and the latter will stop the action if angle is already less than or equal to -60.

In the action() stages of dobjFor(TurnRight) or dobjFor(TurnLeft) we then merely need to add or subtract 30 to or from angle and report the result. You will recall that displaying text at the action() stage suppresses the display of the default message we defined for the report() stage.

The code to do all this is quite lengthy, but basically quite straightforward:

+++ wheel: Fixture 'wheel'
    "The wheel can be turned to port or starboard to steer the aircraft. It's
    currently <<angleDesc>>. "
    
    isTurnable = true
    angle = 0
    
    angleDesc()
    {
        switch(angle)
        {
        case -60:
            "hard to port";
            break;
        case -30:
            "slightly to port";
            break;
        case 0:
            "amidships";
            break;
        case 30:
            "slightly to starboard";
            break;
        case 60:
            "hard to starboard";
            break;
        }
    }
    
    dobjFor(TurnRight)
    {
        check()
        {
            if(angle >= 60)
                "It's already turned as far to starboard as it will go. ";
        }
        
        action()
        {
            angle += 30;
            "You turn the wheel 30 degrees to starboard so that it ends up
            <<angleDesc>>. ";
        }
    }
    
    dobjFor(TurnLeft)
    {
        check()
        {
            if(angle <= -60)
                "It's already turned as far to port as it will go. ";            
        }
        
        action()
        {
            angle -= 30;
            "You turn the wheel 30 degrees to port so that it ends up
            <<angleDesc>>. ";
        }
    }
;

One new feature to note here is the use of angle += 30 and angle -= 30 as a convenient shorthand way of adding 30 to angle and subtracting 30 from angle. As previously mentioned, all a check() method needs to do to stop an action is to display some text, which these check methods do if the wheel's already turned one way or the other as far as it will go. We're better off using check() rather than verify() not only because it happens to be a bit more convenient, but also because even when the wheel's turned as far as it will go it's still the best choice of object for a TurnRight or TurnLeft command, so we don't actually want to rule it out as illogical at the verify() stage.

There's one further refinement we could add here, since the player might reasonably try PUSH WHEEL or PULL WHEEL to mean PUSH CONTROL COLUMN or PULL CONTROL COLUMN. It would therefore make sense to redirect pushing or pulling the wheel to pushing or pulling the control column (of which it is in any case a part), which we can do quite simply with remap:

    dobjFor(Push)
    {
        remap = controlColumn
    }
    
    dobjFor(Pull)
    {
        remap = controlColumn
    }

What remap = controlColumn does is simply to tell the game to use controlColumn rather than the object actually specified (in this case, the wheel) as the direct object of a Push or Pull command.


Pulling and Pushing the Stick

To keep things reasonably simple we'll allow the control column to take one of only three different positions: pulled back, vertical or pushed forward. For the purposes of this game this should be quite sufficient. The implementation of the Push and Pull actions on the control column can then proceed in a very similar manner to that of the turning actions on the wheel:

++ controlColumn: Fixture 'control column;;stick'
    "It's basically a stick that can be pushed forward or pulled back, with a
    wheel attached at the top. It's currently <<positionDesc>>. "
    listOrder = 10
    
    position = 0
    
    positionDesc = ['pulled right back', 'vertical', 
      'pushed all the way forward'][position + 2]
    
    dobjFor(Push)
    {
        check()
        {
            if(position > 0)
                "It's already pushed forward as far as it will go. ";
        }
        
        action()
        {
            position++;
            "You push the control column so that it's now <<positionDesc>>. ";
        }
    }
    
    dobjFor(Pull)
    {
        check()
        {
            if(position < 0)
                "It's already pulled back as far as it will go. ";
        }
        
        action()
        {
            position--;
            "You pull the control column so that it's now <<positionDesc>>. ";
        }
    }
;

This shouldn't require a lot of comment. Note the use of position++ and position-- to increment and decrement the value of the position property by 1. You'll probably have already guessed that position is a custom property we've defined on the controlColumn object for the purpose of registering its position. The positionDesc property uses a neat trick to turn the value of the position property into a string describing it. ['pulled back', 'vertical', 'pushed forward'] is a list containing three elements. ['pulled back', 'vertical', 'pushed forward'][index] returns the indexth element from that list. By adding 2 to position we get an index that varies from 1 to 3 as position varies from -1 to 1. With just a little more arithmetical manipulation we could have used a similar device to turn the wheel angle into a description instead of using the switch statement on the wheel.

At the moment pushing and pulling the stick doesn't affect anything but the stick. We'll come back to its effect on the plane in the next section.

Pulling and Pushing the Thrust Lever

Finally, we come to the thrust lever, which is notionally meant to control the power output of the aircraft's engine(s). We could simply more or less repeat what we did in the implementation of the control column, but it would be more interesting to do something a little different. Let's suppose that the thrust lever can be in one of six positions, arbitrarily labeled 0 to 5. Rather than making the player type PUSH LEVER five times in succession to move the lever all the way forward (surely rather unrealistic in any case), let's suppose that the lever can be moved to the desired position directly with a command like MOVE LEVER TO 2 or PUSH LEVER TO 4. But let's suppose that we also want PUSH LEVER (by itself, without any particular position specified) to push the lever all the way forward to full thrust, i.e. the 5 setting, and PULL LEVER to move it all the way back to 0.

With this setup, the thrust lever has a kind of binary mode with just PULL and PUSH (all the way back or all the way forward) and also a multi-setting mode in which it can be moved to any setting between 0 and 5. The library defines a Lever class for a binary kind of lever and a Settable class for a control that can be set to a number of different settings. Fortunately we can combine the two through multiple inheritance:

++ thrustLever: Settable, Lever 'thrust lever'
    "It's a lever that can be pushed forward or pulled back. It's currently
    <<settingDesc>>. "
    listOrder = 20
    
    settingDesc()
    {
        switch(curSetting)
        {
        case '0':
            return 'pulled all the way back to 0';
        case '5':
            return 'pushed all the way forward to 5';
        default:
            return 'in the <<curSetting>> position';
        }
    }
    
    curSetting = '0'
;

Note that we use the curSetting property (a property of Settable) to hold the current setting (which Settable expects to find as a single-quoted string) and define a custom settingDesc() method to use a switch statement that translates the curSetting value into a fuller textual description of the current position of the lever.

We want to restrict the possible settings of the lever to numbers between 0 and 5. A NumberedDial will do that for us, but a Lever isn't a NumberedDial, and neither is a Settable. Fortunately there's a little trick TADS 3 lets us play if we want to "borrow" a method from a class the object we're defining doesn't inherit from. We can do so using the delegated keyword, followed by the name of the class whose method we want to borrow. Here we want to borrow the isValidSetting() method of the NumberedDial class to ensure that the lever can only be moved to numbers between 0 and 5. We don't need to use delegated with the minSetting and maxSetting properties we're also "borrowing" from NumberedDial, since they're just straightforward numerical properties we can define directly on our thrustLever. The definition of thrustLever can thus continue like this:

    minSetting = 0
    maxSetting = 5
    
    isValidSetting(val)
    {
        return delegated NumberedDial(val);
    }

For more information on the delegated keyword, refer to the "Expressions and Operators" section of Part III of the TADS 3 System Manual.

Next, we'll probably want moving the thrust lever to be reported with something a little less bland than "You set the thrust lever to n", especially if the engine is running. We can do that by providing our custom message in the makeSetting() method:

    makeSetting(val)
    {
        local oldVal = curSetting;
        inherited(val);
        "You <<if oldVal < val>> push the thrust lever forward<<else>> pull the
        lever back<<end>> to <<curSetting>>. ";
        
        if(ignitionButton.isOn)
        {
            "The whine of the engine <<if oldVal < val>>increases <<else>>
            decreases<<end>> in pitch and volume<<if val=='0'>>, dying away to
            a barely perceptible whisper<<end>>. ";
        }
        
    }

Here we use the oldVal local variable to store the previous setting so we can test which way the player has just moved the lever when we want to display an appropriate message. We also use the value of ignitionButton.isOn to test whether the engine is running, so that if it is we can display a description of the change in engine noise that results from moving the thrust lever. We don't need to worry about the case where the player tries to move the lever to the same setting it's already at, since the verify() stage of SetTo will rule that out anyway. Also we don't need to worry about the default message the action would have displayed, since our custom message automatically suppresses anything that would have been displayed at the report stage.

The next job is to tie in the Lever's push/pull behaviour with the Settable behaviour we've just implemented. A Lever can be in one of two states, isPulled = nil or isPulled = true. For this Lever we want the isPulled = true state (when the thrust lever is pulled all the way back) to correspond to curSetting of '0', and the isPushed = true state (when the lever is pushed all the way forward) to correspond to a curSetting of '5'. By default the Lever class assumes that isPushed is true when isPulled is nil and vice versa, so we need to override those two properties to cancel that assumption, and completely change what the Lever's makePulled() method normally does:

    makePulled(stat)
    {        
        makeSetting(stat ? '0' : '5');
    }
    
    isPulled = (curSetting == '0')
    isPushed = (curSetting == '5')

The complete definition of the thrustLever object now looks as follows:

++ thrustLever: Settable, Lever 'thrust lever'
    "It's a lever that can be pushed forward or pulled back. It's currently
    <<settingDesc>>. "
    listOrder = 20
    
    settingDesc()
    {
        switch(curSetting)
        {
        case '0':
            return 'pulled all the way back to 0';
        case '5':
            return 'pushed all the way forward to 5';
        default:
            return 'in the <<curSetting>> position';
        }
    }
    
    curSetting = '0'
    minSetting = 0
    maxSetting = 5
    
    isValidSetting(val)
    {
        return delegated NumberedDial(val);
    }
    
    makeSetting(val)
    {
        local oldVal = curSetting;
        inherited(val);
        "You <<if oldVal < val>> push the thrust lever forward<<else>> pull the
        lever back<<end>> to <<curSetting>>. ";
        
        if(ignitionButton.isOn)
        {
            "The whine of the engine <<if oldVal < val>>increases <<else>>
            decreases<<end>> in pitch and volume<<if val=='0'>>, dying away to
            a barely perceptible whisper<<end>>. ";
        }
        
    }
    
    makePulled(stat)
    {        
        makeSetting(stat ? '0' : '5');
    }
    
    isPulled = (curSetting == '0')
    isPushed = (curSetting == '5')
;

We still have one further job to do. At the moment the thrust lever will respond to commands like SET LEVER TO 3 but we'd also like to respond to MOVE LEVER TO 3, or PUSH LEVER TO 3 or PULL LEVER TO 3. We've already seen how to accomplish that sort of thing in another context, namely by modifying the appropriate VerbRule:

modify VerbRule(SetTo)
    ('set' | 'move' | 'push' | 'pull') singleDobj 'to' literalIobj
    :
;

We've now got the controls to the point where they can be operated, even if we can't yet fly the plane with them. In the next section we'll make it possible for the plane to take off.