Defining New Actions

Although adv3Lite comes with a fairly substantial range of built-in actions (almost as many as adv3), most games will want to define at least one or two additional ones, and in this section we discuss how. Here we'll focus on some of the basics, and concentrate on how to define new IActions, TActions and TIActions (actions that take no objects, a direct object that's a physical game object, or a direct object and an indirect object that are both physical game objects). Actions involving literals and topics raise additional issues that we'll look at in the sections to follow.

Synonyms for Existing Actions

Before discussing how to define new actions, we should first consider how to go about defining synonyms for existing actions, for in some cases that may be all you need to do. There are basically two ways to define synonyms for an existing action: either modify an existing VerbRule or create a new one. In either case you'll probably want to find the existing VerbRule defined in the adveLite library and then copy it and paste it into your own code to use as a model. VerbRules (the grammar rules that define how a player can describe an action to the parser) are found in the file grammar.t under the english directory (that is, in the folder called 'english') within the adv3Lite library directory. To find the one you want, search on a word that describes the action (e.g. 'take') or on the name of the action (e.g. 'LockWith') if you know it.

Suppose, for example, you wanted to make the commands GRAB and SNATCH work just like TAKE. Here's how you'd do it by modifying a VerbRule:

modify VerbRule(Take)
    ('take' | 'pick' 'up' | 'get' |'grab'| 'snatch') multiDobj
    | 'pick' multiDobj 'up' :
;

Note that we start by using the keyword modify and then our redefinition with a colon following our new grammar specification (followed, of course, by the semicolon showing we've reached the end of the object definition). The various words or phrases we can use to mean 'take' are separated by the vertical bar symbol (|) and grouped in brackets to show that they are alternatives that can precede 'multiDobj'. The token 'multiDobj' in turn means that this is where the player would specify one or more direct objects of the command. If you wanted to restrict the command to only a single direct object at a time you would use the token 'singleDobj' instead. Finally, another vertical bar symbol divides this phrasing from an alternative way of phrasing the command, PICK object UP.

The other way of adding synonyms to an existing action is to create a new VerbRule that refers to the existing action. For example:

VerbRule(Grab)
    ('grab' | 'snatch') multiDobj  
    : VerbProduction
    action = Take
    verbPhrase = 'grab/grabbing (what)'
    missingQ = 'what do you want to grab'
;

Here we define the new vocabulary that we want to trigger the action in much the same way as before, except this time we do only have to define the new vocabulary (and not the existing vocabulary as well, since that still exists on the original VerbRule). We give the new VerbRule a distinctive name — in this case Grab — to identify it (we could call it anything so long as no other VerbRule has the same name, but it makes sense to call it something meaningful and relevant). After the colon we add the class name VerbProduction (this is common to all adv3Lite VerbRules), and then define the action this VerbRule relates to on its action property. The verbPhrase is used for building implicit action announcements and the like and here takes the form 'infinitive/participle (placeholder for object)', except that the infinitive lacks the initial 'to' (we write 'grab', not 'to grab' here). The forms for other types of action will be described below as we come to them, but they all start infinitive/participle. Finally the missingQ property defines the question the parser will ask if the player issues a command that lacks the appropriate object, e.g.:

>grab
What do you want to grab?

>ball
You take the red ball.

(If you're used to adv3 you'll notice that the adv3Lite way of defining a VerbRule is quite similar but a little different in some details).

Whether to modify an existing VerbRule or create a new one is entirely up to you; do whichever you find easier.


VerbRules

We've just seen that modifying the words used to refer to an action requires modifying or creating a VerbRule. Creating a new action always requires defining a VerbRule to define how the player can describe the action to the parser (and how the parser can respond with certain qualifications and queries). As this is always a required step in defining any new action, we should next go into a bit more detail about how VerbRules are defined.

English's predicate syntax is highly positional. That is, the role of each word in a predicate is determined largely by its position in the phrase. There are a several common patterns to the predicate word order, but the specific pattern that applies to a given verb is essentially idiomatic to that verb, especially with respect to complement words (like the "up" in "pick up"). Our approach to defining the predicate grammar is therefore to define a separate, custom syntax rule for each verb. This makes it easy to add rules for the odd little idioms in English verbs.

For verbs that take indirect objects, the indirect object is usually introduced by a preposition (e.g., PUT KEY IN LOCK). Since we consider the preposition in such a case to be part of the verb's grammatical structure, we write it directly into the grammar rule as a literal. This means that we wouldn't be able to parse input that's missing the whole indirect object phrase (e.g., PUT KEY). We don't want to just reject those without explanation, though, which means we have to define separate grammar rules for the truncated verbs. Some of these cases are valid commands in their own right: UNLOCK DOOR and UNLOCK DOOR WITH KEY are both valid grammatically. But PUT KEY isn't, so we need to mark this as missing its indirect object. We do this by setting the missingRole property for these rules to the role (usually IndirectObject) of the phrase that's missing.

The first step in defining a VerbRule is to use the VerbRule() macro with a suitable name tag, as in our previous example:

VerbRule(Grab)

The name-tag is arbitrary, but must be different for each VerbRule. It is usual (and advisable) to use something that relates as closely as possible to the action being defined, typically the name of the action itself except where we're defining more than one VerbRule for the same action.

We next define the grammar that applies to the action (i.e. the way the player can refer to it when entering commands). e.g.:

VerbRule(Grab)
   ('grab' | 'snatch') multiDobj

Exactly how this should be defined for each type of action is something we'll explain as we come to each type of action in turn. We next define the VerbRule to be of the VerbProduction class:

VerbRule(Grab)
   ('grab' | 'snatch') multiDobj
   : VerbProduction

From this point on each VerbRule has several properties and methods that it either can or must define:

Note that for the most part when defining your own actions you'll mostly only have to worry about the first two or three of these properties (and occasionally the last), but there may occasionally be times when you need to know about the others, so they're included here for completeness. Note also that although it's possible to define a VerbRule for an action that takes three objects (direct object, indirect object and accessory), the adv3Lite library has no direct support for such actions unless you include the TIAAction extension.

Sometimes you may want to define a command with one or more optional words; for example you might want HANG COAT ON HOOK or HANG COAT UP ON HOOK to mean the same thing. To achieve this you can define options between parentheses leaving one of them empty thus: ('up'| ). The complete VerbRule for Hang On (let us suppose for the sake of argument that it simply performs a PutOn action here) might then look like this:

 VerbRule(HangOn)
    'hang' ('up'|) singleDobj ('up'|) 'on' singleIobj
    : VerbProduction
    action = PutOn
    verbPhrase = 'hang/hanging (what) (on what)'
    missingQ = 'what do you want to hang; what do you want to hang it on'    
 ;
 

This would then respond to commands like HANG COAT ON PEG, HANG UP COAT ON PEG, HANG COAT UP ON PEG or even HANG UP COAT UP ON PEG.

In the case like our VerbRule(Grab) example above, i.e. a VerbRule for an action that takes only one, direct, object, the parser will automatically take care of incomplete commands like GRAB or SNATCH by posing the question defined in the missingQ property: "What do you want to grab?". But in some cases, such as the HangOn example above, we may need to do a bit more work with the VerbRule for such a TIAction where the player may type an incomplete command (like HANG CLOAK). We'll defer discussion of this, however, until we come to look more closely at TIActions below. In the meantime, note that it's not an issue where there are in fact two underlying actions, a TAction (such as Attack, as in ATTACK THE TROLL) and a TIAction (such as ATTACK THE TROLL WITH THE SWORD), since in such a case ATTACK THE TROLL is a complete Attack command, not an incomplete AttackWith command.


Defining New IActions

An IAction is one that consists purely of a verb with no objects. There are two steps to defining any new IAction in adv3Lite:

  1. Define the new VerbRule
  2. Define the new Action

We've already seen how to define a new VerbRule, but we'll now show how to do it for a new IAction. Suppose you want to define an action that responds to the command WAKE UP (in its intransitive sense, i.e. telling the player character to wake up, not trying to wake up another character as in WAKE BOB). The VerbRule might look like this:

VerbRule(WakeUp)
    'wake' 'up'
    : VerbProduction
    action = WakeUp
    verbPhrase = 'wake/waking up'        
;

Note that we don't define the missingQ property here, since an IAction can never have a missing object. Otherwise we define the rest of the VerbRule in the manner already described, first giving the grammar we want the VerbRule to match (here simply WAKE UP), then ': VerbProduction', then the action this command will trigger (WakeUp, which we've about to define below), and finally the verbPhrase, which simply gives the infinitive and present participle forms of the command phrase (either 'wake up' or 'waking up', though note we don't repeat the 'up').

The second step is to define the action, which for an IAction we do with the DefineIAction(action-name) macro, where action-name is the name of the new action we're defining. We also have to define the action's execAction(cmd) method to define what the action does. A minimal definition in the case of our new WakeUp action might look like this:

DefineIAction(WakeUp)
    execAction(cmd)
    {
        "{I}{\'m} not asleep. ";
    }
;       

Of course, your action may need to be more complex than this, in which case you'd need to define a more complicated execAction() method, for example to handle situations when the player character actually is asleep, in a dream sequence say (although another way of handling this might be via a Doer to handle the special cases, for example:)

Doer 'wake up'         
    execAction(c)
    {
        "You wish you could wake up from this nightmare. ";
    }
    during = dreamScene
; 

One further small point to note: in the adv3 library DefineIAction(WakeUp) would have defined a new class called WakeUpAction which inherits from IAction; in adv3Lite it defines an object called WakeUp, which is of the IAction class.


Defining New TActions

A TAction is an action that takes a direct object. There are three steps in defining a new TAction:

  1. Define the new VerbRule
  2. Define the new Action
  3. Define the Action handling on Thing (and perhaps other classes as well)

For example, suppose we wanted to define a new Rub action, which can be applied to one or more objects at a time. Our VerbRule would look like this:

VerbRule(Rub)
    'rub' multiDobj
    : VerbProduction
    action = Rub
    verbPhrase = 'rub/rubbing (what)'
    missingQ = 'what do you want to rub'
;

As a second example, we could define a Repair command that can only act on one object at a time:

VerbRule(Repair)
   ('repair' | 'mend' | 'fix') singleDobj
   : VerbProduction
   action = Repair
   verbPhrase = 'repair/ repairing (what)'
   missingQ = 'what do you want to repair'
;

Note how we use the bar (|) symbol to define alternative phrasings and the brackets to group them. We want the command to match REPAIR FOO, MEND FOO or FIX FOO; without the brackets it would match: REPAIR or MEND, or FIX FOO.

The second step is usually very simple indeed; we just define the new action using the DefineTAction(action-name) macro, for example:

DefineTAction(Rub)
;

DefineTAction(Repair)
;

And normally that's all there is to it; note in particular that when defining a TAction (or TIAction) we don't override its execAction(cmd) method, since the library already makes it own use for this for TActions and TIActions, and overriding it will stop things working properly (especially if you override it without calling inherited(cmd) in the overridden method, but there normally shouldn't be any reason to override it at all). Occasionally, though, you might want do define the method getAll(cmd, role) which defines which objects should match ALL in a command like RUB ALL (here cmd is the current Command object and role is either DirectObject or IndirectObject). The default behaviour is to return every object in scope, but for some actions you may want to restrict this. For example on the Take action the adv3Lite library defines:

    getAll(cmd, role)
    {
        return scopeList.subset({ x: !x.isDirectlyIn(cmd.actor) && !x.isFixed});
    }

This stops TAKE ALL from trying to take objects the actor is already holding (which is unlikely to be what the player intended) or objects that are fixed in place (which again is unlikely to be what the player intended), and so avoids a whole lot of failure messages from objects that obviously can't be taken.

To prevent an action accepting ALL altogether you can set its allowAll property to nil. Any action you do this on will respond to FOO ALL with "Sorry; ALL is not allowed with this command." You can also set this globally for most actions using gameMain.allActionsAllowAll; see the description of the gameMain object.

Both these ways of modifying what ALL does can be used on both new and existing actions (in the case of existing ones you'd need to use a modify statement). A third method that can be applied to both is the hideFromAll(action) method of Thing. If this method returns true for action, then the Thing in question will be excluded from the list of things FOO ALL applies to (when FOO is the relevant action). For example, if our game includes a bottle of deadly poison, we may want to prevent the player from accidentally killing the player character with a DRINK ALL or EAT ALL command by doing something like the following:

poisonBottle: Thing 'dark green bottle; of[prep]; poison'
  "It's labeled POISON. "
  isDrinkable = true
  
  hideFromAll(action)
  {
     return action is in (Eat, Drink, Taste);
  }
  
  dobjFor(Drink)
  {
     action()
     {
         "It tastes foul, but that's the least of your worries...";
         finishGameMsg(ftDeath, [finishOptionUndo]);
     }
  }
  
  dobjFor(Taste) asDobjFor(Drink)
;

Note that while making hideFromAll(action) return true will certainly exclude the item from ALL, having it return nil does not necessarily mean that the item will be included in ALL, since it may already have been excluded by the action's allowAll setting or getAll() method. In particular, the hideFromAll() method is used to filter out items from the list returned by the current action's getAll() method; it cannot be used to add any items back in.

One other property you might want to define on your action object is announceMultiAction. This is nil by default, but if it's true and the action applies to more than one direct object (e.g. EXAMINE ALL) then it will announce the name of the object each time it runs the actions routine, so you could get an output like:

>X ALL
red ball: It's a small red ball, about the size of a cricket ball.
blue ball: It's a large plastic beach-ball.
me: You look as bedraggled as you feel.

The reason announceMultiAction is nil by default is that for most actions you'd use the report method to show a summary of the action (e.g. "You take the red ball and blue ball") so that announcing each direct object as above would be superfluous (although the examine action is one exception to this).

The third step is defining the appropriate action handling methods on Thing (and on any subclass of Thing for which you want different default behaviour). For this step you basically need to follow the principles already explained in the Action Results section above. So for our Rub and Repair actions we might define:

modify Thing
   dobjFor(Rub)
   {
      preCond = [touchObj, objVisible]
      
      report() { "{I} rub{s/?ed} <<gActionListStr>>. "; }
   }

   isRepairable = nil
   
   dobjFor(Repair)
   {
      preCond = [touchObj]
      
      verify()
      {
          if(!isRepairable)
             illogical(cannotRepairMsg);
      }      
   }
   
   cannotRepairMsg = '{The subj dobj} {doesn\'t need} mending. '
;

We could have defined the message '{The subj dobj} {doesn\'t need} mending. ' directly in the illogical() macro, but by taking the extra step of going via a cannotRepairMsg property we make it much easier to customise the failure response on individual objects; either way note that this message must be defined as a single-quoted string, not a double-quoted one. We don't strictly need to define the new isRepairable property and test it in the verify() method, but the library follows this coding pattern by and large and it makes it that much easier when it comes to defining objects that do need repairing (we just define isRepairable = true rather than having to override the complete verify method). Note how we define the message parameter substitution 'rub{s/?ed}' to tell the parser how to conjugate 'rub', but if we knew that the Rub action was only ever going to be performed by the player character and the game was never going to change tense, then we could skip this step and simply define:

   report() { "You rub <<gActionListStr>>. "; }

At some point you'll presumably want to define objects where these actions do something more interesting, e.g.:

magicLamp: Thing 'brass lamp;magic; lantern'
  ...
  
  dobjFor(Rub)
  {
      check()
      {
         if(!genie.isIn(nil))
           "It's dangerous to rub the lamp again once you've already summoned the genie;
             There's no telling what he might do!" ;
      }
      
      action()
      {
         genie.moveInto(getOutermostRoom);
         "As soon as you rub the lamp a genie appears! ";
      }
  }
;  

radio: Thing 'radio'
   ...
   isSwitchable = true
   isRepairable = true
   
   dobjFor(Repair)
   {            
       check()
       {
          "Great idea; the next question is how? ";
       }
   }
 ;
   

Defining TIActions

TIActions are actions that take both a direct object and an indirect object. The steps in defining a new TIAction are very similar to those for defining a TAction. Here we'll use new RubWith and RepairWith actions as our examples. The first step to define the VerbRules that trigger the actions:

VerbRule(RubWith)
    'rub' multiDobj 'with' singleIobj
    : VerbProduction
    action = RubWith
    verbPhrase = 'rub/rubbing (what) (with what)'
    missingQ = 'what do you want to rub; what do you want to rub it with'
;


VerbRule(RepairWith)
   ('repair' | 'mend' | 'fix') singleDobj 'with' singleIobj
   : VerbProduction
   action = RepairWith
   verbPhrase = 'repair/ repairing (what) (with what)'
   missingQ = 'what do you want to repair; what do you want to repair it with'
;

These follow much the same pattern as VerbRules for TActions, but with a couple of additions. First, note how the verbPhrase property is defined in each case, with the addition of '(with what)'; obviously we us the preposition appropriate to the action here, so in other cases it might be '(in what)' or '(to what)' or whatever. Second, note how the missingQ property is extended with a second question to ask in the case of a missing indirect object. Third note the use of the singleIobj token to represent the place of the indirect object in the command grammar. In principle we could use multiDobj here, and the parser would understand it, but the adv3Lite library doesn't provide full support for commands with multiple indirect objects because there's hardly any case in which they're actually useful, let alone essential. If you really must have one in your game, consider using a Doer to iterate over the indirect objects. In any case, if the VerbRule uses multiDobj it cannot also use multiObj.

The second step is to define the actual actions, which we do with the DefineTIAction(action-name) macro:

DefineTIAction(RubWith)
   resolveIobjFirst = nil   
;

DefineTIAction(RepairWith)
   resolveIobjFirst = nil
;

Note that we once again don't override the execAction(cmd) method when defining a new TIAction. An additional point to note here is the use of the resolveIobjFirst property. In adv3Lite the verify() properties are only consulted once (at most) during object resolution; the resolveIobjFirst property controls whether the indirect object's verify() method is consulted first (the default, resolveIobjFirst = true), or whether the direct object's verify() method is run first (as here). The significance is that whichever property is run first has no access to the identity of the other object. So when resolveIobjFirst is true, the direct object is not known when the indirect object is resolved, but the indirect object is known when the direct object is resolved, so the direct object's verify() method can make use of the identity of the indirect object to decide whether or not the direct object is a logical choice. When resolveIobjFirst is false, it's the other way about. For actions like PutIn and PutOn, resolveIobjFirst = true is a good choice, since it's helpful to know what we want to put something in or on before deciding whether it's logical to put a particular object in it or on it. For our new RubWith and RepairWith actions, however, it's probably better to resolve the direct object first, so that when the parser comes to resolve the indirect object the the potential indirect objects' verify() routines can see what it is that's to be rubbed or repaired before deciding with the proposed indirect object is a logical choice to rub or repair it with.

The third step proceeds as before, except that we now have to define the iobjFor(Action) methods as well as the dobjFor(Action) methods:

modify Thing
   dobjFor(RubWith)
   {
       preCond = [touchObj]
       report() { "{I} {rub} <<gActionListStr>> with {the iobj}. ";}
   }

   iobjFor(RubWith)
   {
       preCond = [objHeld]
   }
     
   
   dobjFor(RepairWith)
   {
       preCond = [touchObj, objVisible]
       
       verify()   
       {  
          if(!isRepairable)
             illogical(cannotRepairMsg);   
       }
   }
   
   canRepairWithMe = nil
   
   iobjFor(RepairWith)
   {
       preCond = [objHeld]
       
       verify()  
       {  
           if(!isRepairable)
              illogical(cannotRepairWithMsg); 
           
           if(gDobj == self)
              illogicalSelf(cannotRepairWithSelfMsg);
       }
   }
   
   cannotRepairWithMsg = '{I} {can\'t} repair anything with {that iobj}. '
   cannotRepairWithSelfMsg = '{I} {can\'t} repair anything with itself. '
;

Here we're assuming that we've previously defined a Repair action as above, so we can re-use the cannotRepairMsg on the direct object of a RepairWith action (another reason for defining it on a message property) along with the isRepairable property. Note too how we take advantage of the fact that the direct object is resolved first to test that we're not trying to repair anything with itself on the indirect object; if there's a red box and a red screwdriver, REPAIR BOX WITH RED will then result in the red screwdriver being selected as the indirect choice since even if the red box doesn't need repairing and the screwdriver couldn't repair it in any case, attempting to repair the box with the screwdriver is less illogical than attempting to repair the box with itself. Note too the definition of a new cannotRepairWithMe property (following the naming convention used in the library). This simplifies writing the code for objects that can be used to repair other ones with, since we can then simply define canRepairWithMe = true without having to override the verify() method again and remembering to copy the code to block the attempt to repair something with itself.

In addition to defining particular objects in our game that can be repaired, we might want to define a Tool class for objects we can reasonably attempt repairs with:

class Tool: Thing
   canRepairWithMe = true
   
   iobjFor(RepairWith)
   {
           
      check()
      {
           if(valToList(itemsRepaired).indexOf(gDobj) == nil)
             "{I} {can\'t} repair {the dobj} with {the iobj}. ";
      }
      
      action()
      {
           "Using {the iobj} {i} manage{s/d} to repair {the dobj}. ";
           gDobj.makeRepaired(true);
      }
   }
   
   itemsRepaired = nil
;

This assumes that you've defined a suitable makeRepaired(stat) method on the various things that might need mending over the course of your game. Note the use of a custom itemsRepaires property to define which objects a particular Tool can be used to repair. Note too the use of the valToList() function to convert this property into a list (if it isn't a list already) regardless of whether it's defined as nil, a single object, or a list of objects, so that the check() method can treat it as a list no matter how it's defined on individual objects.

As noted above in the section on VerbRules, in some cases we may need to do a bit more work with the VerbRule for a TIAction where the player may type an incomplete command. In such cases we need to tell the parser which object to ask for and how to interpret the reply. So, for example, if we want the parser also to respond to a command like HANG COAT or HANG UP COAT or HANG COAT UP with a response like "What do you want to hang it on?" we need to provide an additional VerbRule to field the incomplete command. Such a VerbRule would take the form:

 VerbRule(HangOnWhat)
    [badness 500] 'hang' ('up'|) singleDobj ('up'|)
    : VerbProduction
    action = PutOn
    verbPhrase = 'hang/hanging (what) (on what)'
    missingQ = 'what do you want to hang; what do you want to hang it on'   
    missingRole = IndirectObject
    iobjReply = onSingleNoun    
 ;
 

(HangOnWhat) is simply an arbitrary name (or tag) we give this VerbRule which has to be unique among VerbRules (although it's usually a good idea to use something reasonably meaningful). The additional features here are [badness 500], missingRole, and iobjReply. We'll explain each in turn.

The badness tag is simply a way of telling the parser that this is potentially a badly-formed command, so that, for example, if a player types a command HANG X ON Y the parser should try to match this to hanging X on another object Y before it tries to match it to an attempt to hang a single object called 'X ON Y' without another object specified. The number 500 is a more or less arbitrary value that makes a defective HANG X command (without an indirect object) that much a less preferred interpretation compared with a fully specified HANG X ON Y command.

The missingRole property allows us to specify which role (DirectObject or IndirectObject) is missing in a command like HANG CLOAK. In this case it's the IndirectObject (such as the peg or hook the player might want to hang the cloak on).

Finally, iobjReply tells the parser what kind of phrase to expect in response to the missingQ question "What do you want to hang it on?". In this case onSingleNoun means that the parser should expect an answer of the form ON THE PEG or just THE PEG (i.e. 'on' followed by a noun phrase or just a noun phrase). Other common possibilities include inSingleNoun, forSingleNoun, toSingleNoun, throughSingleNoun, fromSingleNoun, onSingleNoun, withSingleNoun, atSingleNoun and outOfSingleNoun, all of which work analogously (i.e. they can match either just a noun phrase or the relevant preposition followed by a noun phrase). In fact the first of these, inSingleNoun, can match several related phrases, such as IN X, INTO X or IN TO X, where X is any noun phrase (such as PEG, WOODEN PEG or THE BIG WOODEN PEG). If we needed something that didn't match any of these we could simply define our own grammar entity like so:

grammar underSingleNoun(main):
   singleNoun->np_ | 'under' singleNoun->np_ : Production
;

This would then match either a plain noun phrase (e.g. just BED) or a noun phrase preceded by 'under' (e.g. UNDER THE BED).

In addition to the TIActions of the kind we've discussed here, which involve a direct object and an indirect object which are both physical objects within the simulation, there are other commands that may look like TIActions at first sight but are in fact something rather different, such as WRITE HELLO WORLD IN NOTEBOOK or ASK MAUDE ABOUT THE MASKED BALL. In the first of these example HELLO WORLD is not a game object but a literal, and in the second THE MASKED BALL is not a physical object but a topic. The next two chapters will discuss how to deal with such commands, but first we'll take a quick look at actions that take more than two objects.


Actions with Three Objects

Actions involving three objects are relatively rare in Interactive Fiction, (a) because they are seldom necessary and (b) because players don't expect them and would be unlikely to hit on the relevant commands without being heavily prompted. Actions involving a direct object, indirect object and accessory object (e.g. PUT COIN IN SLOT WITH TWEEZERS) are not directly supported in the main adv3Lite library but can be implemented with the use of the TIAAction extension. Actions involving two simulation objects and a literal or topic can, however, in principle be implemented with the tools in the main library by using a couple of tricks.

The main trick is to use one of the existing action classes, such as TIAction to define the action. The subsidiary trick is to define the action and its accompanying VerbRule such that the physical objects involved in the action are the direct and indirect objects and the topic or literal takes the third object slot (the accessory object). This doesn't mean that it has to come last in the VerbRule, but simply that we match it to literalAobj or topicAobj when we construct the VerbRule, and then assign it to the literal/topic property of the action when we define the action.

This may be clearer with the aid of an example. Suppose we want to allow an action like WRITE TEXT IN NOTEBOOK WITH PEN. We could define the action and its VerbRule thus:

/* 
 * Although it takes three objects, we can define this action as a TIAction
 * since the third object is a literal.
 */
DefineTIAction(WriteOnWith)    
    execAction(cmd)
    {
        /* 
         *  Store the literal value from the aobj (or acc) property of
         *  the current command object.
         */  
       
        literal = cmd.aobj.theName;
        
        /* Then carry out the inherited handling. */
        inherited(cmd);
    }
    
;

/*
 *  Note that when we define the VerbRule we assign the literal text to
 *  be entered as the third (literalAobj) object so that the the physical
 *  objects involved occupy the dobj and iobj slots.
 */
 
VerbRule(WriteOnWith)
    'write' literalAobj ('on' | 'in') singleDobj 'with' singleIobj
    : VerbProduction
    action = WriteOnWith
    verbPhrase = 'write/writing (on what) (with what) (what)'
    missingQ = 'what do you want to write on; what do you want to write with;
        what do you want to write'
;
 
 

This allows us define the default handling on Thing in the normal way:

  modify Thing    
    dobjFor(WriteOnWith)
    {
        preCond = [touchObj]
        verify()
        {
            if(!canWriteOnMe)
                illogical(cannotWriteOnMsg); 
        }
    }
    
    iobjFor(WriteOnWith)
    {
        preCond = [objHeld]
        verify()
        {
            if(!canWriteWithMe)
                illogical(cannotWriteWithMsg);
        }
    }
    
    canWriteWithMe = nil
    cannotWriteWithMsg = '{I} {can\'t} write anything with {the iobj}. '   
; 
  

Finally, we can define a couple of objects, a notebook and pen, say, that do allow writing with the WriteOnWith action. In this case we probably want the existing WriteOn command to insist on a writing implement. In outline the code might look something like this:

 
+ notebook: Thing 'notebook'
    canWriteOnMe = true
    
    readDesc
    {
        if(inscription == '')
            "The notebook is blank. ";
        else
            "On the notebook is written:\n<<inscription>> ";
    }
    
    inscription = ''
    
    dobjFor(WriteOn)
    {
        action()
        {
            /* 
             *   We need to swap the roles around before asking for a writing
             *   implement, so that the aobj, dobj, and iobj will be the right
             *   way round for the new WriteOnWith action.
             */
            gAobj = gCommand.dobj;
            gDobj = gCommand.iobj;
            askForIobj(WriteOnWith);
        }
    }
    
    dobjFor(WriteOnWith)
    {
        action()
        {
            inscription += (gLiteral + '\n');
            "{I} {write} <<gLiteral>> on the notebook. ";
        }
    }
;
    
+ pen: Thing 'pen'
    
    canWriteWithMe = true    
;  
  
  

The treatment here has deliberately been kept brief, but hopefully it will be enough to illustrate the principle. Game authors may need to adapt what's been shown here to the particular cases they want to implement, but this is unlikely to occur very often in practice.