misc.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library - miscellaneous definitions
  *   
  *   This module contains miscellaneous definitions that don't have a
  *   natural grouping with any larger modules, and which aren't complex
  *   enough to justify modules of their own.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   When a call is made to a property not defined or inherited by the
  *   target object, the system will automatically invoke this method.  The
  *   method will be invoked with a property pointer as its first argument,
  *   and the original arguments as the remaining arguments.  The first
  *   argument gives the property that was invoked and not defined by the
  *   object.  A typical definition in an object would look like this:
  *   
  *   propNotDefined(prop, [args]) { ... }
  *   
  *   If this method is not defined by the object, the system simply
  *   returns nil as the value of the undefined property evaluation or
  *   method invocation.
  */
 property propNotDefined;
 export propNotDefined;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   The library base class for the gameMain object.
  *   
  *   Each game MUST define an object called 'gameMain' to define how the
  *   game starts up.  You can use GameMainDef as the base class of your
  *   'gameMain' object, in which case the only thing you're required to
  *   specify in your object is the 'initialPlayerChar' property - you can
  *   inherit everything else from the GameMainDef class if you don't
  *   require any further customizations.  
  */
 class GameMainDef: object
     /*
      *   The initial player character.  Each game's 'gameMain' object MUST
      *   define this to refer to the Actor object that serves as the
      *   initial player character. 
      */
     initialPlayerChar = nil
 
     /*
      *   Show the game's introduction.  This routine is called by the
      *   default newGame() just before entering the main command loop.  The
      *   command loop starts off by showing the initial room description,
      *   so there's no need to do that here.
      *   
      *   Most games will want to override this, to show a prologue message
      *   setting up the game's initial situation for the player.  We don't
      *   show anything by default.  
      */
     showIntro() { }
 
     /*
      *   Show the "goodbye" message.  This is called after the main command
      *   loop terminates.
      *   
      *   We don't show anything by default.  If you want to show a "thanks
      *   for playing" type of message as the game exits, override this
      *   routine with the desired text.  
      */
     showGoodbye() { }
 
     /*
      *   Begin a new game.  This default implementation shows the
      *   introductory message, calls the main command loop, and finally
      *   shows the goodbye message.
      *   
      *   You can override this routine if you want to customize the startup
      *   protocol.  For example, if you want to create a pre-game options
      *   menu, you could override this routine to show the list of options
      *   and process the user's input.  If you need only to customize the
      *   introduction and goodbye messages, you can simply override
      *   showIntro() and showGoodbye() instead.  
      */
     newGame()
     {
         /* try restoring the global defaults */
         settingsManager.restoreSettings();
 
         /* 
          *   Show the statusline before we display our introductory.  This
          *   will help minimize redrawing - if we waited until after
          *   displaying some text, we might have to redraw some of the
          *   screen to rearrange things for the new screen area taken up by
          *   the status line, which could be visible to the user.  By
          *   setting up the status line first, we'll probably have less to
          *   redraw because we won't have anything on the screen yet when
          *   figuring the layout.  
          */
         statusLine.showStatusLine();
 
         /* show the introduction */
         showIntro();
 
         /* run the game, showing the initial location's full description */
         runGame(true);
 
         /* show the end-of-game message */
         showGoodbye();
     }
 
     /*
      *   Restore a game and start it running.  This is invoked when the
      *   user launches the interpreter using a saved game file; for
      *   example, on a Macintosh, this happens when the user double-clicks
      *   on a saved game file on the desktop.
      *   
      *   This default implementation bypasses any normal introduction
      *   messages: we simply restore the game file if possible, and
      *   immediately start the game's main command loop.  Most games won't
      *   need to override this, but you can if you need some special effect
      *   in the restore-at-startup case.  
      */
     restoreAndRunGame(filename)
     {
         local succ;
 
         /* mention that we're about to restore the saved position */
         gLibMessages.noteMainRestore();
 
         /* try restoring it */
         succ = RestoreAction.startupRestore(filename);
 
         /* show a blank line after the restore result message */
         "<.p>";
 
         /* if we were successful, run the game */
         if (succ)
         {
             /* 
              *   Run the command loop.  There's no need to show the room
              *   description, since the RESTORE action will have already
              *   done so. 
              */
             runGame(nil);
 
             /* show the end-of-game message */
             showGoodbye();
         }
     }
 
     /*
      *   Set the interpreter window title, if applicable to the local
      *   platform.  This simply displays a <TITLE> tag to set the title to
      *   the string found in the versionInfo object.  
      */
     setGameTitle()
     {
         /* write the <TITLE> tag with the game's name */
         "<title><<versionInfo.name>></title>";
     }
 
     /*
      *   Set up the HTML-mode about-box.  By default, this does nothing.
      *   Games can use this routine to show an <ABOUTBOX> tag, if desired,
      *   to set up the contents of an about-box for HTML TADS platforms.
      *   
      *   Note that an <ABOUTBOX> tag must be re-initialized each time the
      *   main game window is cleared, so this routine should be called
      *   again after any call to clearScreen().  
      */
     setAboutBox()
     {
         /* we don't show any about-box by default */
     }
 
     /*
      *   The gameMain object also specifies some settings that control
      *   optional library behavior.  If you want the standard library
      *   behavior, you can just inherit the default settings from this
      *   class.  Some games might want to select non-default variations,
      *   though.  
      */
 
     /* 
      *   The maximum number of points possible in the game.  If the game
      *   includes the scoring module at all, and this is non-nil, the SCORE
      *   and FULL SCORE commands will display this value to the player as a
      *   rough indication of how much farther there is to go in the game.
      *   
      *   By default, we initialize this on demand, by calculating the sum
      *   of the point values of the Achievement objects in the game.  The
      *   game can override this if needed to specify a specific maximum
      *   possible score, rather than relying on the automatic calculation.
      */
     maxScore()
     {
         local m;
         
         /* ask the score module (if any) to compute the maximum score */
         m = (libGlobal.scoreObj != nil
              ? libGlobal.scoreObj.calcMaxScore : nil);
 
         /* supersede this initializer with the calculated value */
         maxScore = m;
 
         /* return the result */
         return m;
     }
 
     /*
      *   The score ranking list - this provides a list of names for
      *   various score levels.  If the game provides a non-nil list here,
      *   the SCORE and FULL SCORE commands will show the rank along with
      *   the score ("This makes you a Master Adventurer").
      *   
      *   This is a list of score entries.  Each score entry is itself a
      *   list of two elements: the first element is the minimum score for
      *   the rank, and the second is a string describing the rank.  The
      *   ranks should be given in ascending order, since we simply search
      *   the list for the first item whose minimum score is greater than
      *   our score, and use the preceding item.  The first entry in the
      *   list would normally have a minimum of zero points, since it
      *   should give the initial, lowest rank.
      *   
      *   If this is set to nil, which it is by default, we'll simply skip
      *   score ranks entirely.  
      */
     scoreRankTable = nil
 
     /* 
      *   Verbose mode.  If this is on, the full room description is
      *   displayed each time the player enters a room, regardless of
      *   whether or not the player has seen the room before; if this is
      *   nil, the full description is only displayed on the player's first
      *   entry to a room, and only the short description on re-entry.  Note
      *   that the library provides VERBOSE and TERSE commands that let the
      *   player change this setting dynamically.
      *   
      *   We use a BinarySettingsItem to store the current mode, so that
      *   this setting's default will be taken from the user's global
      *   cross-game preferences.  
      */
     verboseMode = verboseModeSettingsItem
 
     /*
      *   Option flag: allow the player to use "you" and "me"
      *   interchangeably in referring to the player character.  We set
      *   this true by defalt, so that the player can refer to the player
      *   character in either the first or second person, as long as the
      *   player character normally uses either or these (in other words,
      *   this option is meaningless in a game when the narration refers to
      *   the player character in the third person).
      *   
      *   If desired, the game can set this flag to nil to force the player
      *   to use the correct pronoun to refer to the player character.
      *   
      *   We set the default to allow using "you" and "me" interchangeably
      *   because this will create no confusion in most games, and because
      *   most experienced IF players will be accustomed to using "me" to
      *   refer to the player character (because the majority of IF refers
      *   to the player character in the second person, and expects the
      *   player to conflate the player character with the player and hence
      *   to refer to the player character in the first person).  It is
      *   relatively unconventional for a game to refer to the player
      *   character in the first person in the narration, and thus to
      *   expect the player to use the second person to refer to the PC; as
      *   a result, experience players might tend to use the first person
      *   out of habit in such games, and might find it jarring to find the
      *   usage unacceptable.  Furthermore, in games that use a
      *   first-person narration, it seems unlikely that there will also be
      *   a second-person element to the narration; as long as both aren't
      *   present, it will cause no confusion for the game to accept either
      *   "you" or "me" as equivalent in commands.  However, the library
      *   provides this option in case such as situation does arise.  
      */
     allowYouMeMixing = true
 
     /*
      *   Option flag: filter plural phrase matches exclude the most obvious
      *   illogicalities, such as trying to TAKE an object that's already
      *   being held, or trying to OPEN an object that's already open.
      *   
      *   This is set to true by default, which means that we exclude an
      *   object from matching a plural phrase when the object's "verify"
      *   routine for the verb has an "illogical-already" or an
      *   "illogical-self" result.
      *   
      *   If you would prefer that plural words are simply matched to
      *   everything present that matches the vocabulary, without any
      *   filtering at all, override this and set it to nil.  
      */
     filterPluralMatches = true
 
     /*
      *   Option flag: allow ALL to be used for every verb.  This is true by
      *   default, which means that players will be allowed to use ALL with
      *   any command - OPEN ALL, EXAMINE ALL, etc.
      *   
      *   Some authors don't like to allow players to use ALL with so many
      *   verbs, because they think it's a sort of "cheating" when players
      *   try things like OPEN ALL.  This option lets you disable ALL for
      *   most verbs; if you set this to nil, only the basic inventory
      *   management verbs (TAKE, TAKE FROM, DROP, PUT IN, PUT ON) will
      *   allow ALL, and other verbs will simply respond with an error
      *   ("'All' isn't allowed with that verb").
      *   
      *   If you're writing an especially puzzle-oriented game, you might
      *   want to set this to nil.  It's a trade-off though, as some people
      *   will think your game is less player-friendly if you disable ALL.  
      */
     allVerbsAllowAll = true
 
     /*
      *   When a command fails, should we continue processing any remaining
      *   commands on the same command line, or simply ignore them?  The
      *   reason we might want to ignore additional commands is that they
      *   might not do what the player was expecting if an earlier command
      *   failed; this can sometimes create confusing situations, because
      *   the player expected one effect but got something quite different.
      *   On the other hand, *not* executing all the commands on the command
      *   line could be confusing in its own way, since the game's
      *   assessment of what constitutes "failure" might not be clear to the
      *   player; from the player's perspective, the game might appear to be
      *   inexplicably skipping commands.
      *   
      *   There's no perfect solution.  As always, the ideal is to
      *   understand the player's intentions and act accordingly.  But when
      *   a command fails, it's usually because the player's idea of what's
      *   going on is out of sync with the game's - in other words, if we're
      *   in this situation to start with, it's probably because our best
      *   effort to understand the player's intentions has already failed.
      *   This isn't always the case; sometimes we understand the player's
      *   intentions perfectly well, but the command fails anyway because of
      *   some surprising new development.  In these cases, aborting the
      *   rest of the command is arguably the right approach, because the
      *   player will need a chance to reconsider the pre-typed commands in
      *   light of the new information.  In other cases, though, it's not so
      *   clear.  For many players, the prime virtue for the parser is to be
      *   predictable, and the most predictable thing to do is to simply
      *   plow through the rest of the command line in all cases.
      *   
      *   Our traditional approach (from the early adv3 versions, and even
      *   in tads 2) has been the simple-minded approach - just keep going
      *   in all cases.  So, we make this the default.  You can abort
      *   remaining commands on a command failure by setting this to true.  
      */
     cancelCmdLineOnFailure = nil
 
     /*
      *   Should we use distinguishers when generating action object
      *   announcement messages?  If this is true, we'll be as specific as
      *   possible when listing default objects, vaguely matched objects,
      *   and multiple objects in action reports.  By default, this is nil,
      *   which means that we just use the object's base name in these
      *   announcements.
      *   
      *   (This is optional because the current implementation is a bit
      *   twitchy for most people's taste.  The problem is that it's a bit
      *   *too* specific in many cases, showing more qualification than is
      *   really necessary.  We make this an option so that authors can
      *   enable it and possibly tweak it a bit to meet their needs.)  
      */
     useDistinguishersInAnnouncements = nil
 ;
 
 /*
  *   The VERBOSE mode settings item. 
  */
 verboseModeSettingsItem: BinarySettingsItem
     /* VERBOSE mode is on by default */
     isOn = true
 
     /* our configuration file variable ID */
     settingID = 'adv3.verbose'
 
     /* show our description */
     settingDesc = (gLibMessages.shortVerboseStatus(isOn))
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Clear the main game window.  In most cases, you should call this
  *   rather than calling the low-level clearScreen() function directly,
  *   since this routine takes care of a couple of chores that should
  *   usually be done at the same time.
  *   
  *   First, we flush the transcript to ensure that no left-over reports
  *   that were displayed before we cleared the screen will show up on the
  *   new screen.  Second, we call the low-level clearScreen() function to
  *   actually clear the display window.  Finally, we re-display any
  *   <ABOUTBOX> tag, to ensure that the about-box will still be around;
  *   this is necessary because any existing <ABOUTBOX> tag is lost after
  *   the screen is cleared.  
  */
 cls()
 {
     /* flush any captured transcript output */
     if (gTranscript != nil)
         gTranscript.flushForInput();
 
     /* clear the screen */
     clearScreen();
 
     /* re-initialize any <ABOUTBOX> tag */
     gameMain.setAboutBox();
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Run the game.  We start by showing the description of the initial
  *   location, if desired, and then we read and interpret commands until
  *   the game ends (via a "quit" command, winning, death of the player
  *   character, or any other way of terminating the game).
  *   
  *   This routine doesn't return until the game ends.
  *   
  *   Before calling this routine, the caller should already have set the
  *   global variable gPlayerChar to the player character actor.
  *   
  *   'look' is a flag indicating whether or not to look around; if this is
  *   true, we'll show a full description of the player character's initial
  *   location, as though the player were to type "look around" as the first
  *   command.  
  */
 runGame(look)
 {
     /* show the starting location */
     if (look)
     {
         /* run the initial "look around" in a dummy command context */
         withActionEnv(EventAction, gPlayerChar,
                       {: gPlayerChar.lookAround(true) });
     }
 
     /* run the scheduling loop until the game ends */
     runScheduler();
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Main program entrypoint.  The core run-time start-up code calls this
  *   after running pre-initialization and load-time initialization.  This
  *   entrypoint is called when we're starting the game normally; when the
  *   game is launched through a saved-position file, mainRestore() will be
  *   invoked instead.  
  */
 main(args)
 {
     try
     {
         /* initialize the display */
         initDisplay();
 
         /* call the gameMain object to set up a new game */
         gameMain.newGame();
     }
     catch (QuittingException q)
     {
         /* 
          *   ignore this exception - it's just a signal to quit the game,
          *   which we will now proceed to do by returning from this
          *   function, which exits the program 
          */
     }
 }
 
 /*
  *   Initialize the display.  We call this when we first enter the
  *   interpreter to set up the main game window.  
  */
 initDisplay()
 {
     /* set the interpreter window title */
     gameMain.setGameTitle();
 
     /* set up the ABOUT box */
     gameMain.setAboutBox();
 }
 
 /*
  *   Main program entrypoint for restoring a saved-position file.  This is
  *   invoked from the core run-time start-up code when the game is launched
  *   from the operating system via a saved-position file.  For example, on
  *   Windows, double-clicking on a saved-position file on the Windows
  *   desktop launches the interpreter, which looks in the save file to find
  *   the game executable to run, then starts the game and invokes this
  *   entrypoint.  
  */
 mainRestore(args, restoreFile)
 {
     try
     {
         /* initialize the display */
         initDisplay();
 
         /* call the gameMain object to restore the specified saved game */
         gameMain.restoreAndRunGame(restoreFile);
     }
     catch (QuittingException q)
     {
         /* 
          *   ignore this exception - it's just a signal to quit the game,
          *   which we will now proceed to do by returning from this
          *   function, which exits the program 
          */
     }
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Determine if the given object overrides the definition of the given
  *   property inherited from the given base class.  Returns true if the
  *   object derives from the given base class, and the object's definition
  *   of the property comes from a different place than the base class's
  *   definition of the property.  
  */
 overrides(obj, base, prop)
 {
     return (obj.ofKind(base)
             && (obj.propDefined(prop, PropDefGetClass)
                 != base.propDefined(prop, PropDefGetClass)));
 }
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Library Pre-Initializer.  This object performs the following
  *   initialization operations immediately after compilation is completed:
  *   
  *   - adds each defined Thing to its container's contents list
  *   
  *   - adds each defined Sense to the global sense list
  *   
  *   This object is named so that other libraries and/or user code can
  *   create initialization order dependencies upon it.  
  */
 adv3LibPreinit: PreinitObject
     execute()
     {
         /* save each SettingsItem's factory default settings */
         forEachInstance(SettingsItem,
                         {i: i.factoryDefault = i.settingToText()});
 
         /* set the initial player character, as specified in gameMain */
         gPlayerChar = gameMain.initialPlayerChar;
         
         /* 
          *   visit every VocabObject, and run its vocabulary initializer
          *   (this routine will be defined in the language-specific part
          *   of the library to enter each object's vocabulary words into
          *   the dictionary) 
          */
         forEachInstance(VocabObject, { obj: obj.initializeVocab() });
 
         /* visit every Thing, and run its general initializer */
         forEachInstance(Thing, { obj: obj.initializeThing() });
 
         /* initialize SpecialTopic objects */
         forEachInstance(SpecialTopic, { obj: obj.initializeSpecialTopic() });
 
         /* 
          *   Initialize each MultiInstance object.  Do this after
          *   initializing the Thing objects, because we'll be dynamically
          *   constructing new Thing objects for the instances.  Those new
          *   Things will be initialized by their constructors, so we don't
          *   want to initialize them redundantly with explicit
          *   initializeThing calls.  
          */
         forEachInstance(MultiInstance, { obj: obj.initializeLocation() });
 
         /* add every Sense to the global sense list */
         forEachInstance(Sense, { obj: libGlobal.allSenses += obj });
 
         /* 
          *   initialize each ActorState - do this before initializing
          *   actors, since we want each actor's initial state to plug
          *   itself into its actor before we initialize the actors 
          */
         forEachInstance(ActorState, { obj: obj.initializeActorState() });
 
         /* initialize each Actor */
         forEachInstance(Actor, { obj: obj.initializeActor() });
 
         /* 
          *   initialize the AltTopics first, to set up their parents' lists
          *   of their AltTopic children 
          */
         forEachInstance(AltTopic, { obj: obj.initializeAltTopic() });
 
         /* initialize the topic database entries */
         forEachInstance(TopicEntry, { obj: obj.initializeTopicEntry() });
 
         /* initialize the suggested topics */
         forEachInstance(SuggestedTopic,
             { obj: obj.initializeSuggestedTopic() });
 
         /* initialize the master direction list */
         Direction.initializeDirectionClass();
 
         /* initialize the noise/odor notification daemon */
         local d = new Daemon(SensoryEmanation, &noteSenseChanges, 1);
 
         /* 
          *   give it a later-than-default event order, so that it runs
          *   after most other daemons and fuses 
          */
         d.eventOrder = 500;
 
         /* set up a daemon for the current location */
         new Daemon(BasicLocation, &dispatchRoomDaemon, 1);
 
         /* 
          *   Initialize the status line daemon.  Set this daemon's event
          *   order to a high value so that it runs last, after all other
          *   daemons - we want this to be the last prompt daemon so that
          *   the status line is updated after any other daemons have done
          *   their jobs already, in case any of them move the player
          *   character to a new location or affect the score, or make any
          *   other changes that should be reflected on the status line.  
          */
         local sld = new PromptDaemon(statusLine, &showStatusLineDaemon);
         sld.eventOrder = 1000;
 
         /* 
          *   Attach the command sequencer output filter, the
          *   language-specific message parameter substitution filter, the
          *   style tag formatter filter, and the paragraph filter to the
          *   main output stream.  Stack them so that the paragraph manager
          *   is at the bottom, since the library tag filter can produce
          *   paragraph tags and thus needs to sit atop the paragraph
          *   filter.  Put the command sequencer above those, since it
          *   might need to write style tags.  Finally, put the sense
          *   context filter on top of those.  
          */
         mainOutputStream.addOutputFilter(typographicalOutputFilter);
         mainOutputStream.addOutputFilter(mainParagraphManager);
         mainOutputStream.addOutputFilter(styleTagFilter);
         mainOutputStream.addOutputFilter(langMessageBuilder);
         mainOutputStream.addOutputFilter(commandSequencer);
         mainOutputStream.addOutputFilter(conversationManager);
         mainOutputStream.addOutputFilter(senseContext);
 
         /* 
          *   Attach our message parameter filter and style tag filter to
          *   the status line streams.  We don't need most of the main
          *   window's filters in the status line.  
          */
         statusTagOutputStream.addOutputFilter(styleTagFilter);
         statusTagOutputStream.addOutputFilter(langMessageBuilder);
 
         statusLeftOutputStream.addOutputFilter(styleTagFilter);
         statusLeftOutputStream.addOutputFilter(langMessageBuilder);
 
         statusRightOutputStream.addOutputFilter(styleTagFilter);
         statusRightOutputStream.addOutputFilter(langMessageBuilder);
     }
 
     /* 
      *   Make sure the output streams we depend on are initialized before
      *   me (so that they set up properly internally).  Also, make sure
      *   that the message builder object (langMessageBuilder) is set up
      *   first, so that we can add entries to its parameter substitution
      *   table.  
      */
     execBeforeMe = [mainOutputStream, statusTagOutputStream,
                     statusLeftOutputStream, statusRightOutputStream,
                     langMessageBuilder]
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Library Initializer.  This object performs the following
  *   initialization operations each time the game is started:
  *   
  *   - sets up the library's default output function 
  */
 adv3LibInit: InitObject
     execute()
     {
         /* 
          *   Set up our default output function.  Note that we must do
          *   this during run-time initialization each time we start the
          *   game, rather than during pre-initialization, because the
          *   default output function state is not part of the load-image
          *   configuration. 
          */
         t3SetSay(say);
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Generic script object.  This class can be used to implement a simple
  *   state machine.  
  */
 class Script: object
     /* 
      *   Get the current state.  This returns a value that gives the
      *   current state of the script, which is usually simply an integer.  
      */
     getScriptState()
     {
         /* by default, return our state property */
         return curScriptState;
     }
 
     /*
      *   Process the next step of the script.  This routine must be
      *   overridden to perform the action of the script.  This routine's
      *   action should call getScriptState() to get our current state, and
      *   should update the internal state appropriately to take us to the
      *   next step after the current one.
      *   
      *   By default, we don't do anything at all.  
      */
     doScript()
     {
         /* override to carry out the script */
     }
 
     /* 
      *   Property giving our current state.  This should never be used
      *   directly; instead, getScriptState() should always be used, since
      *   getScriptState() can be overridden so that the state depends on
      *   something other than this internal state property. The meaning of
      *   the state identifier is specific to each subclass.  
      */
     curScriptState = 0
 ;
 
 /*
  *   Random-Firing script add-in.  This is a mix-in class that you can add
  *   to the superclass list of any Script subclass to make the script
  *   execute only a given percentage of the time it's invoked.  Each time
  *   doScript() is invoked on the script, we'll look at the probability
  *   settings (see the properties below) to determine whether we really
  *   want to execute the script this time; if so, we'll proceed with the
  *   scripted event, otherwise we'll just return immediately, doing
  *   nothing.
  *   
  *   Note that this must be used in the superclass list *before* the Script
  *   subclass:
  *   
  *   myScript: RandomFiringScript, EventList
  *.    // ...my definitions...
  *.  ;
  *   
  *   This class is especially useful for random atmospheric events, because
  *   it allows you to make the timing of scripted events random.  Rather
  *   than making a scripted event happen on every single turn, you can use
  *   this to make events happen only sporadically.  It can often feel too
  *   predictable and repetitious when a random background event happens on
  *   every single turn; firing events less frequently often makes them feel
  *   more realistic.  
  */
 class RandomFiringScript: object
     /* 
      *   Percentage of the time an event occurs.  By default, we execute an
      *   event 100% of the time - meaning every time that doScript() is
      *   invoked.  If you set this to a lower percentage, then each time
      *   doScript() is invoked, we'll randomly decide whether or not to
      *   execute an event based on this percentage.  For example, if you
      *   want an event to execute on average about a third of the time, set
      *   this to 33.
      *   
      *   Note that this is a probabilistic frequency.  Setting this to 33
      *   does *not* mean that we'll execute exactly every third time.
      *   Rather, it means that we'll randomly execute or not on each
      *   invocation, and averaged over a large number of invocations, we'll
      *   execute about a third of the time.  
      */
     eventPercent = 100
 
     /* 
      *   Random atmospheric events can get repetitive after a while, so we
      *   provide an easy way to reduce the frequency of our events after a
      *   while.  This way, we'll generate the events more frequently at
      *   first, but once the player has seen them enough to get the idea,
      *   we'll cut back.  Sometimes, the player will spend a lot of time in
      *   one place trying to solve a puzzle, so the same set of random
      *   events can get stale.  Set eventReduceAfter to the number of times
      *   you want the events to be generated at full frequency; after we've
      *   fired events that many times, we'll change eventPercent to
      *   eventReduceTo.  If eventReduceAfter is nil, we won't ever change
      *   eventPercent.  
      */
     eventReduceAfter = nil
     eventReduceTo = nil
 
     /*
      *   When doScript() is invoked, check the event probabilities before
      *   proceeding.  
      */
     doScript()
     {
         /* process the script step only if the event odds allow it */
         if (checkEventOdds())
             inherited();
     }
 
     /*
      *   Check the event odds to see if we want to fire an event at all on
      *   this invocation.  
      */
     checkEventOdds()
     {
         /* 
          *   check the event odds to see if we fire an event this time; if
          *   not, we're done with the script invocation 
          */
         if (rand(100) >= eventPercent)
             return nil;
 
         /* 
          *   we're firing an event this time, so count this against the
          *   reduction limit, if there is one 
          */
         if (eventReduceAfter != nil)
         {
             /* decrement the limit counter */
             --eventReduceAfter;
             
             /* if it has reached zero, apply the reduced frequency */
             if (eventReduceAfter == 0)
             {
                 /* apply the reduced frequency */
                 eventPercent = eventReduceTo;
                 
                 /* we no longer have a limit to look for */
                 eventReduceAfter = nil;
             }
         }
 
         /* indicate that we do want to fire an event */
         return true;
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An "event list."  This is a general-purpose type of script that lets
  *   you define the scripted events separately from the Script object.
  *   
  *   The script is driven by a list of values; each value represents one
  *   step of the script.  Each value can be a single-quoted string, in
  *   which case the string is simply displayed; a function pointer, in
  *   which case the function is invoked without arguments; another Script
  *   object, in which case the object's doScript() method is invoked; a
  *   property pointer, in which case the property of 'self' (the EventList
  *   object) is invoked with no arguments; or nil, in which case nothing
  *   happens.
  *   
  *   This base type of event list runs through the list once, in order, and
  *   then simply stops doing anything once we pass the last event.  
  */
 class EventList: Script
     construct(lst) { eventList = lst; }
 
     /* the list of events */
     eventList = []
 
     /* advance to the next state */
     advanceState()
     {
         /* increment our state index */
         ++curScriptState;
     }
 
     /* by default, start at the first list element */
     curScriptState = 1
 
     /* process the next step of the script */
     doScript()
     {
         local idx;
         
         /* get our current event state */
         idx = getScriptState();
 
         /* if it's a valid index in our list, fire the event */
         if (idx >= 1 && idx <= eventList.length())
         {
             /* carry out the event */
             doScriptEvent(eventList[idx]);
         }
 
         /* perform any end-of-script processing */
         scriptDone();
     }
 
     /* carry out one script event */
     doScriptEvent(evt)
     {
         /* check what kind of event we have */
         switch (dataTypeXlat(evt))
         {
         case TypeSString:
             /* it's a string - display it */
             say(evt);
             break;
             
         case TypeObject:
             /* it must be a Script object - invoke its doScript() method */
             evt.doScript();
             break;
             
         case TypeFuncPtr:
             /* it's a function pointer - invoke it */
             (evt)();
             break;
             
         case TypeProp:
             /* it's a property of self - invoke it */
             self.(evt)();
             break;
             
         default:
             /* do nothing in other cases */
             break;
         }
     }
 
     /*
      *   Perform any end-of-script processing.  By default, we advance the
      *   script to the next state.
      *   
      *   Some scripts might want to override this.  For example, a script
      *   could be driven entirely by some external timing; the state of a
      *   script could vary once per turn, for example, or could change each
      *   time an actor pushes a button.  In these cases, invoking the
      *   script wouldn't affect the state of the event list, so the
      *   subclass would override scriptDone() so that it does nothing at
      *   all.  
      */
     scriptDone()
     {
         /* advance to the next state */
         advanceState();
     }
 ;
 
 /*
  *   An "external" event list is one whose state is driven externally to
  *   the script.  Specifically, the state is *not* advanced by invoking the
  *   script; the state is advanced exclusively by some external process
  *   (for example, by a daemon that invokes the event list's advanceState()
  *   method).  
  */
 class ExternalEventList: EventList
     scriptDone() { }
 ;
 
 /*
  *   A cyclical event list - this runs through the event list in order,
  *   returning to the first element when we pass the last element.  
  */
 class CyclicEventList: EventList
     advanceState()
     {
         /* go to the next state */
         ++curScriptState;
 
         /* if we've passed the end of the list, loop back to the start */
         if (curScriptState > eventList.length())
             curScriptState = 1;
     }
 ;
 
 /*
  *   A stopping event list - this runs through the event list in order,
  *   then stops at the last item and repeats it each time the script is
  *   subsequently invoked. 
  *   
  *   This is often useful for things like ASK ABOUT topics, where we reveal
  *   more information when asked repeatedly about a topic, but eventually
  *   reach a point where we've said everything:
  *   
  *.  >ask bob about black book
  *.  "What makes you think I know anything about it?" he says, his
  *   voice shaking.
  *   
  *   >again
  *.  "No! You can't make me tell you!"
  *   
  *   >again
  *.  "All right, I'll tell you what you want to know!  But I warn you,
  *   these are things mortal men were never meant to know.  Your life, your
  *   very soul will be in danger from the moment you hear these dark secrets!"
  *   
  *   >again
  *.  [scene missing]
  *   
  *   >again
  *.  "I've already told you all I know."
  *   
  *   >again
  *.  "I've already told you all I know."
  */
 class StopEventList: EventList
     advanceState()
     {
         /* if we haven't yet reached the last state, go to the next one */
         if (curScriptState < eventList.length())
             ++curScriptState;
     }
 ;
 
 /*
  *   A synchronized event list.  This is an event list that takes its
  *   actions from a separate event list object.  We get our current state
  *   from the other list, and advancing our state advances the other list's
  *   state in lock step.  Set 'masterObject' to refer to the master list
  *   whose state we synchronize with.  
  *   
  *   This can be useful, for example, when we have messages that reflect
  *   two different points of view on the same events: the messages for each
  *   point of view can be kept in a separate list, but the one list can be
  *   a slave of the other to ensure that the two lists are based on a
  *   common state.  
  */
 class SyncEventList: EventList
     /* my master event list object */
     masterObject = nil
 
     /* my state is simply the master list's state */
     getScriptState() { return masterObject.getScriptState(); }
 
     /* to advance my state, advance the master list's state */
     advanceState() { masterObject.advanceState(); }
 
     /* let the master list take care of finishing a script step */
     scriptDone() { masterObject.scriptDone(); }
 ;
 
 /*
  *   Randomized event list.  This is similar to a regular event list, but
  *   chooses an event at random each time it's invoked.
  */
 class RandomEventList: RandomFiringScript, EventList
     /* process the next step of the script */
     doScript()
     {
         local idx;
 
         /* check the odds to see if we want to fire an event at all */
         if (!checkEventOdds())
             return;
         
         /* get our next random number */
         idx = getNextRandom();
         
         /* run the event, if the index is valid */
         if (idx >= 1 && idx <= eventList.length())
             doScriptEvent(eventList[idx]);
     }
     
     /*
      *   Get the next random state.  By default, we simply return a number
      *   from 1 to the number of entries in our event list.  This is a
      *   separate method to allow subclasses to customize the way the
      *   random number is selected.  
      */
     getNextRandom()
     {
         /*   
          *   Note that rand(n) returns a number from 0 to n-1 inclusive;
          *   since list indices run from 1 to list.length, add one to the
          *   result of rand(list.length) to get a value in the proper range
          *   for a list index.  
          */
         return rand(eventList.length()) + 1;
     }
 ;
 
 /*
  *   Shuffled event list.  This is similar to a random event list, except
  *   that we fire our events in a "shuffled" order rather than an
  *   independently random order.  "Shuffled order" means that we fire the
  *   events in random order, but we don't re-fire an event until we've run
  *   through all of the other events.  The effect is as though we were
  *   dealing from a deck of cards.
  *   
  *   For the first time through the main list, we normally shuffle the
  *   strings immediately at startup, but this is optional.  If shuffleFirst
  *   is set to nil, we will NOT shuffle the list the first time through -
  *   we'll run through it once in the given order, then shuffle for the
  *   next time through, then shuffle again for the next, and so on.  So, if
  *   you want a specific order for the first time through, just define the
  *   list in the desired order and set shuffleFirst to nil.
  *   
  *   You can optionally specify a separate list of one-time-only sequential
  *   strings in the property firstEvents.  We'll run through these strings
  *   once.  When we've exhausted them, we'll switch to the main eventList
  *   list, showing it one time through in its given order, then shuffling
  *   it and running through it again, and so on.  The firstEvents list is
  *   never shuffled - it's always shown in exactly the order given.  
  */
 class ShuffledEventList: RandomFiringScript, EventList
     /* 
      *   a list of events to go through sequentially, in the exact order
      *   specified, before firing any events from the main list
      */
     firstEvents = []
 
     /*
      *   Flag: shuffle the eventList list before we show it for the first
      *   time.  By default, this is set to true, so that the behavior is
      *   random on each independent run of the game.  However, it might be
      *   desirable in some cases to always use the original ordering of the
      *   eventList list the first time through the list.  If this is set to
      *   nil, we won't shuffle the list the first time through.  
      */
     shuffleFirst = true
 
     /* process the next step of the script */
     doScript()
     {
         local evt;
         local firstLen = firstEvents.length();
         local eventLen = eventList.length();
 
         /* process the script step only if the event odds allow it */
         if (!checkEventOdds())
             return;
 
         /* 
          *   States 1..N, where N is the number of elements in the
          *   firstEvents list, simply show the firstEvents elements in
          *   order.
          *   
          *   If we're set to shuffle the main eventList list initially, all
          *   states above N simply show elements from the eventList list in
          *   shuffled order.
          *   
          *   If we're NOT set to shuffle the main eventList list initially,
          *   the following apply:
          *   
          *   States N+1..N+M, where M is the number of elements in the
          *   eventList list, show the eventList elements in order.
          *   
          *   States above N+M show elements from the eventList list in
          *   shuffled order.  
          */
         if (curScriptState <= firstLen)
         {
             /* simply fetch the next string from firstEvents */
             evt = firstEvents[curScriptState++];
         }
         else if (!shuffleFirst && curScriptState <= firstLen + eventLen)
         {
             /* fetch the next string from eventList */
             evt = eventList[curScriptState++ - firstLen];
         }
         else
         {
             /* we're showing shuffled strings from the eventList list */
             evt = eventList[getNextRandom()];
         }
 
         /* execute the event */
         doScriptEvent(evt);
     }
 
 
     /*
      *   Get the next random event.  We'll pick an event from our list of
      *   events using a ShuffledIntegerList to ensure we pick each value
      *   once before re-using any values.  
      */
     getNextRandom()
     {
         /* if we haven't created our shuffled list yet, do so now */
         if (shuffledList_ == nil)
         {
             /* 
              *   create a shuffled integer list - we'll use these shuffled
              *   integers as indices into our event list 
              */
             shuffledList_ = new ShuffledIntegerList(1, eventList.length());
 
             /* 
              *   tell the list to avoid re-using the same element twice in
              *   a row on shuffling boundaries 
              */
             shuffledList_.suppressRepeats = true;
         }
 
         /* ask the shuffled list to pick an element */
         return shuffledList_.getNextValue();
     }
 
     /* our ShuffledList - we'll initialize this on demand */
     shuffledList_ = nil
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Shuffled List - this class keeps a list of values that can be returned
  *   in random order, but with the constraint that we never repeat a value
  *   until we've handed out every value.  Think of a shuffled deck of
  *   cards: the order of the cards handed out is random, but once a card is
  *   dealt, it can't be dealt again until we put everything back into the
  *   deck and reshuffle.  
  */
 class ShuffledList: object
     /* 
      *   the list of values we want to shuffle - initialize this in each
      *   instance to the set of values we want to return in random order 
      */
     valueList = []
 
     /*
      *   Flag: suppress repeated values.  We mostly suppress repeats by
      *   our very design, since we run through the entire list before
      *   repeating anything in the list.  However, there's one situation
      *   (in a list with more than one element) where a repeat can occur:
      *   immediately after a shuffle, we could select the last element
      *   from the previous shuffle as the first element of the new
      *   shuffle.  If this flag is set, we'll suppress this type of repeat
      *   by choosing again any time we're about to choose a repeat.  
      */
     suppressRepeats = nil
 
     /* create from a given list */
     construct(lst)
     {
         /* remember our list of values */
         valueList = lst;
     }
 
     /* 
      *   Get a random value.  This will return a randomly-selected element
      *   from 'valueList', but we'll return every element of 'valueList'
      *   once before repeating any element.
      *   
      *   If we've returned every value on the current round, we'll
      *   automatically shuffle the values and start a new round.  
      */
     getNextValue()
     {
         local i;
         local ret;
         local justReshuffled = nil;
 
         /* if we haven't initialized our vector, do so now */
         if (valuesVec == nil)
         {
             /* create the vector */
             valuesVec = new Vector(valueList.length(), valueList);
 
             /* all values are initially available */
             valuesAvail = valuesVec.length();
         }
 
         /* if we've exhausted our values on this round, start over */
         if (valuesAvail == 0)
         {
             /* shuffle the elements */
             reshuffle();
 
             /* note that we just did a shuffle */
             justReshuffled = true;
         }
 
         /* pick a random element from the 'available' partition */
         i = rand(valuesAvail) + 1;
 
         /*
          *   If we just reshuffled, and we're configured to suppress a
          *   repeat immediately after a reshuffle, and we chose the first
          *   element of the vector, and we have at least two elements,
          *   choose a different element.  The first element in the vector
          *   is always the last element we return from each run-through,
          *   since the 'available' partition is at the start of the list
          *   and thus shrinks down until it contains only the first
          *   element. 
          */
         if (justReshuffled && suppressRepeats && valuesAvail > 1)
         {
             /* 
              *   we don't want repeats, so choose anything besides the
              *   first element; keep choosing until we get another element 
              */
             while (i == 1)
                 i = rand(valuesAvail) + 1;
         }
 
         /* remember the element we're returning */
         ret = valuesVec[i];
 
         /*
          *   Move the value at the top of the 'available' partition down
          *   into the hole we're creating at 'i', since we're about to
          *   reduce the size of the 'available' partition to reflect the
          *   use of one more value; that would leave the element at the top
          *   of the partition homeless, so we need somewhere to put it.
          *   Luckily, we also need to delete element 'i', since we're using
          *   this element.  Solve both problems at once by moving element
          *   we're rendering homeless into the hole we're creating.  
          */
         valuesVec[i] = valuesVec[valuesAvail];
 
         /* move the value we're returning into the top slot */
         valuesVec[valuesAvail] = ret;
 
         /* reduce the 'available' partition by one */
         --valuesAvail;
 
         /* return the result */
         return ret;
     }
 
     /*
      *   Shuffle the values.  This puts all of the values back into the
      *   deck (as it were) for a new round.  It's never required to call
      *   this, because getNextValue() automatically shuffles the deck and
      *   starts over each time it runs through the entire deck.  This is
      *   provided in case the caller has a reason to want to put all the
      *   values back into play immediately, before every value has been
      *   dealt on the current round.  
      */
     reshuffle()
     {
         /* 
          *   Simply reset the counter of available values.  Go with the
          *   original source list's length, in case we haven't initialized
          *   our internal vector yet. 
          */
         valuesAvail = valueList.length();
     }
 
     /*
      *   Internal vector of available/used values.  Elements from 1 to
      *   'valuesAvail', inclusive, are still available for use on this
      *   round.  Elements above 'valuesAvail' have already been used.  
      */
     valuesVec = nil
     
     /* number of values still available on this round */ 
     valuesAvail = 0
 ;
 
 /*
  *   A Shuffled Integer List is a special kind of Shuffled List that
  *   returns integers in a given range.  Like an ordinary Shuffled List,
  *   we'll return integers in the given range in random order, but we'll
  *   only return each integer once during a given round; when we exhaust
  *   the supply, we'll reshuffle the set of integers and start over.  
  */
 class ShuffledIntegerList: ShuffledList
     /* 
      *   The minimum and maximum values for our range.  Instances should
      *   define these to the range desired. 
      */
     rangeMin = 1
     rangeMax = 10
 
     /* initialize the value list on demand */
     valueList = nil
 
     /* construct with the given range */
     construct(rmin, rmax)
     {
         rangeMin = rmin;
         rangeMax = rmax;
     }
 
     /* get the next value */
     getNextValue()
     {
         /* if we haven't set up our value list yet, do so now */
         if (valueList == nil)
         {
             local cnt;
             local i;
             
             /* 
              *   Set up a vector with the required number of elements.  We
              *   have to add one to the difference between our max and min
              *   values, because we want the set to be inclusive of the
              *   endpoints. 
              */
             cnt = rangeMax - rangeMin + 1;
             valueList = new Vector(cnt);
 
             /* put a nil value in each slot */
             valueList.fillValue(nil, 1, cnt);
 
             /* now populate the slots with the integers from our range */
             i = rangeMin;
             valueList.applyAll({x: i++});
         }
 
         /* use the inherited handling to select from our value list */
         return inherited();
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Library global variables 
  */
 libGlobal: object
     /*
      *   The current library messages object.  This is the source object
      *   for messages that don't logically relate to the actor carrying out
      *   the comamand.  It's mostly used for meta-command replies, and for
      *   text fragments that are used to construct descriptions.
      *   
      *   This message object isn't generally used for parser messages or
      *   action replies - most of those come from the objects given by the
      *   current actor's getParserMessageObj() or getActionMessageObj(),
      *   respectively.
      *   
      *   By default, this is set to libMessages.  The library never changes
      *   this itself, but a game can change this if it wants to switch to a
      *   new set of messages during a game.  (If you don't need to change
      *   messages during a game, but simply want to customize some of the
      *   default messages, you don't need to set this variable - you can
      *   simply use 'modify libMessages' instead.  This variable is
      *   designed for cases where you want to *dynamically* change the
      *   standard messages during the game.)  
      */
     libMessageObj = libMessages
     
     /* 
      *   Sense cache - we keep SenseInfo lists here, keyed by [pov,sense];
      *   we normally discard the cached information at the start of each
      *   turn, and disable caching entirely at the start of the "action"
      *   phase of each turn.  We leave caching disabled during each turn's
      *   action phase because this is the phase where simulation state
      *   changes are typically made, and hence it would be difficult to
      *   keep the cache coherent during this phase.
      *   
      *   When this is nil, it indicates that caching is disabled.  We only
      *   allow caching during certain phases of execution, when game state
      *   is not conventionally altered, so that we don't have to do a lot
      *   of work to keep the cache up to date.  
      */
     senseCache = nil
 
     /*
      *   Can-Touch cache - we keep CanTouchInfo entires here, keyed by
      *   [from,to].  This cache is the touch-path equivalent of the sense
      *   cache, and is enabled and disabled 
      */
     canTouchCache = nil
 
     /* 
      *   Connection list cache - this is a cache of all of the objects
      *   connected by containment to a given object. 
      */
     connectionCache = nil
 
     /* 
      *   Actor visual ambient cache - this keeps track of the ambient light
      *   level at the given actor. 
      */
     actorVisualAmbientCache = nil
 
     /* enable the cache, clearing any old cached information */
     enableSenseCache()
     {
         /* create a new, empty lookup table for the sense cache */
         senseCache = new LookupTable(32, 64);
 
         /* create the can-touch cache */
         canTouchCache = new LookupTable(32, 64);
 
         /* create the actor visual ambient cache */
         actorVisualAmbientCache = new LookupTable(32, 64);
 
         /* create a connection list cache */
         connectionCache = new LookupTable(32, 64);
     }
 
     /* disable the cache */
     disableSenseCache()
     {
         /* forget the cache tables */
         senseCache = nil;
         canTouchCache = nil;
         actorVisualAmbientCache = nil;
         connectionCache = nil;
     }
 
     /* 
      *   Invalidate the sense cache.  This can be called if something
      *   happens during noun resolution or verification that causes any
      *   cached sense information to become out of date.  For example, if
      *   you have to create a new game-world object during noun-phrase
      *   resolution, this should be called to ensure that the new object's
      *   visibility is properly calculated and incorporated into the cached
      *   information.  
      */
     invalSenseCache()
     {
         /* remember whether or not caching is currently enabled */
         local wasEnabled = (senseCache != nil);
 
         /* clear the cache by disabling it */
         disableSenseCache();
 
         /* if the cache was previously enabled, re-enable it */
         if (wasEnabled)
             enableSenseCache();
     }
 
     /*
      *   List of all of the senses.  The library pre-initializer will load
      *   this list with a reference to each instance of class Sense. 
      */
     allSenses = []
 
     /*
      *   The current player character 
      */
     playerChar = nil
 
     /* 
      *   The current perspective actor.  This is the actor who's performing
      *   the action (LOOK AROUND, EXAMINE, SMELL, etc) that's generating
      *   the current description. 
      */
     pointOfViewActor = nil
 
     /*
      *   The current perspective object.  This is *usually* the actor
      *   performing the current command, but can be a different object when
      *   the actor is viewing the location being described via an
      *   intermediary, such as through a closed-circuit TV camera.  
      */
     pointOfView = nil
 
     /*
      *   The stack of point of view objects.  The last element of the
      *   vector is the most recent point of view after the current point
      *   of view.  
      */
     povStack = static new Vector(32)
 
     /* 
      *   The global score object.  We use a global for this, rather than
      *   referencing libScore directly, to allow the score module to be
      *   left out entirely if the game doesn't make use of scoring.  The
      *   score module should set this during pre-initialization.  
      */
     scoreObj = nil
 
     /* 
      *   The global Footnote class object.  We use a global for this,
      *   rather than referencing Footnote directly, to allow the footnote
      *   module to be left out entirely if the game doesn't make use of
      *   footnotes.  The footnote class should set this during
      *   pre-initialization.  
      */
     footnoteClass = nil
 
     /* the total number of turns so far */
     totalTurns = 0
 
     /* 
      *   flag: the parser is in 'debug' mode, in which it displays the
      *   parse tree for each command entered 
      */
     parserDebugMode = nil
 
     /*
      *   Most recent command, for 'undo' purposes.  This is the last
      *   command the player character performed, or the last initial
      *   command a player directed to an NPC.
      *   
      *   Note that if the player directed a series of commands to an NPC
      *   with a single command line, only the first command on such a
      *   command line is retained here, because it is only the first such
      *   command that counts as a player's turn in terms of the game
      *   clock.  Subsequent commands are executed by the NPC's on the
      *   NPC's own time, and do not count against the PC's game clock
      *   time.  The first command counts against the PC's clock because of
      *   the time it takes the PC to give the command to the NPC.  
      */
     lastCommandForUndo = ''
 
     /* 
      *   Most recent target actor phrase; this goes with
      *   lastCommandForUndo.  This is nil if the last command did not
      *   specify an actor (i.e., was implicitly for the player character),
      *   otherwise is the string the player typed specifying a target
      *   actor.  
      */
     lastActorForUndo = ''
 
     /*
      *   Current command information.  We keep track of the current
      *   command's actor and action here, as well as the verification
      *   result list and the command report list.  
      */
     curActor = nil
     curIssuingActor = nil
     curAction = nil
     curVerifyResults = nil
 
     /* the exitLister object, if included in the build */
     exitListerObj = nil
 
     /* the hint manager, if included in the build */
     hintManagerObj = nil
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   FinishType objects are used in finishGameMsg() to indicate what kind
  *   of game-over message to display.  We provide a couple of standard
  *   objects for the most common cases. 
  */
 class FinishType: object
     /* the finishing message, as a string or library message property */
     finishMsg = nil
 ;
 
 /* 'death' - the game has ended due to the player character's demise */
 ftDeath: FinishType finishMsg = &finishDeathMsg;
 
 /* 'victory' - the player has won the game */
 ftVictory: FinishType finishMsg = &finishVictoryMsg;
 
 /* 'failure' - the game has ended in failure (but not necessarily death) */
 ftFailure: FinishType finishMsg = &finishFailureMsg;
 
 /* 'game over' - the game has simply ended */
 ftGameOver: FinishType finishMsg = &finishGameOverMsg;
 
 /*
  *   Finish the game, showing a message explaining why the game has ended.
  *   This can be called when an event occurs that ends the game, such as
  *   the player character's death, winning, or any other endpoint in the
  *   story.
  *   
  *   We'll show a message defined by 'msg', using a standard format.  The
  *   format depends on the language, but in English, it's usually the
  *   message surrounded by asterisks: "*** You have won! ***".  'msg' can
  *   be:
  *   
  *.    - nil, in which case we display nothing
  *.    - a string, which we'll display as the message
  *.    - a FinishType object, from which we'll get the message
  *   
  *   After showing the message (if any), we'll prompt the user with
  *   options for how to proceed.  We'll always show the QUIT, RESTART, and
  *   RESTORE options; other options can be offered by listing one or more
  *   FinishOption objects in the 'extra' parameter, which is given as a
  *   list of FinishOption objects.  The library defines a few non-default
  *   finish options, such as finishOptionUndo and finishOptionCredits; in
  *   addition, the game can subclass FinishOption to create its own custom
  *   options, as desired.  
  */
 finishGameMsg(msg, extra)
 {
     local lst;
 
     /*
      *   Adjust the turn counter to take into account the action currently
      *   in progress, if any, and to reflect any turns that the player
      *   character has already completed and which aren't yet reflected in
      *   the turn counter.  If we're processing a daemon, the PC's next
      *   schedulable run time will already reflect the last turn the PC
      *   completed, but the global turn counter won't be there yet, since
      *   we're still scheduling daemons that were ready to run on the same
      *   turn as the player's last action.  
      */
     libGlobal.totalTurns = gPlayerChar.nextRunTime + gAction.actionTime;
 
     /* translate the message, if specified */
     if (dataType(msg) == TypeObject)
     {
         /* it's a FinishType object - get its message property or string */
         msg = msg.finishMsg;
 
         /* if it's a library message property, look it up */
         if (dataType(msg) == TypeProp)
             msg = gLibMessages.(msg);
     }
 
     /* if we have a message, display it */
     if (msg != nil)
         gLibMessages.showFinishMsg(msg);
 
     /* if the extra options include a scoring option, show the score */
     if (extra != nil && extra.indexWhich({x: x.showScoreInFinish}) != nil)
     {
         "<.p>";
         libGlobal.scoreObj.showScore();
         "<.p>";
     }
 
     /*
      *   Since we need to interact directly with the player, any sense
      *   context currently in effect is now irrelevant.  Reset the sense
      *   context by setting the 'source' object to nil to indicate that we
      *   don't need any sense blocking at all.  We can just set the context
      *   directly, since this routine will never return into the
      *   surrounding command processing - we always either terminate the
      *   program or proceed to a different game context (via undo, restore,
      *   restart, etc).  By the same token, the actor we're talking to now
      *   is the player character.  
      */
     senseContext.setSenseContext(nil, sight);
     gActor = gPlayerChar;
 
     /* start with the standard options */
     lst = [finishOptionRestore, finishOptionRestart];
 
     /* add any additional options in the 'extra' parameter */
     if (extra != nil)
         lst += extra;
 
     /* always add 'quit' as the last option */
     lst += finishOptionQuit;
 
     /* process the options */
     processOptions(lst);
 }
 
 /* finish the game, offering the given extra options but no message */
 finishGame(extra)
 {
     finishGameMsg(nil, extra);
 }
 
 /*
  *   Show failed startup restore options.  If a restore operation fails at
  *   startup, we won't just proceed with the game, but ask the user what
  *   they want to do; we'll offer the options of restoring another game,
  *   quitting, or starting the game from the beginning.  
  */
 failedRestoreOptions()
 {
     /* process our set of options */
     processOptions([restoreOptionRestoreAnother, restoreOptionStartOver,
                     finishOptionQuit]);
 }
 
 /*
  *   Process a list of finishing options.  We'll loop, showing prompts and
  *   reading responses, until we get a response that terminates the loop.  
  */
 processOptions(lst)
 {
     /* keep going until we get a valid response */
 promptLoop:
     for (;;)
     {
         local resp;
         
         /* show the options */
         finishOptionsLister.showListAll(lst, 0, 0);
 
         /* switch to before-command mode for reading the interactive input */
         "<.commandbefore>";
 
         /* 
          *   update the status line, in case the score or turn counter has
          *   changed (this is especially likely when we first enter this
          *   loop, since we might have just finished the game with our
          *   previous action, and that action might well have awarded us
          *   some points) 
          */
         statusLine.showStatusLine();
 
         /* read a response */
         resp = inputManager.getInputLine(nil, nil);
 
         /* switch to command-after mode */
         "<.commandafter>";
 
         /* check for a match to each of the options in our list */
         foreach (local cur in lst)
         {
             /* if this one matches, process the option */
             if (cur.responseMatches(resp))
             {
                 /* it matches - carry out the option */
                 if (cur.doOption())
                 {
                     /* 
                      *   they returned true - they want to continue asking
                      *   for more options
                      */
                     continue promptLoop;
                 }
                 else
                 {
                     /* 
                      *   they returned nil - they want us to stop asking
                      *   for options and return to our caller 
                      */
                     return;
                 }
             }
         }
 
         /*
          *   If we got this far, it means that we didn't get a valid
          *   option.  Display our "invalid option" message, and continue
          *   looping so that we show the prompt again and read a new
          *   option.  
          */
         gLibMessages.invalidFinishOption(resp);
     }
 }
 
 /*
  *   Finish Option class.  This is the base class for the abstract objects
  *   representing options offered by finishGame.  
  */
 class FinishOption: object
     /* 
      *   The description, as displayed in the list of options.  For the
      *   default English messages, this is expected to be a verb phrase in
      *   infinitive form, and should show the keyword accepted as a
      *   response in all capitals: "RESTART", "see some AMUSING things to
      *   do", "show CREDITS". 
      */
     desc = ""
 
     /* 
      *   By default, the item is listed.  If you want to create an
      *   invisible option that's accepted but which isn't listed in the
      *   prompt, just set this to nil.  Invisible options are sometimes
      *   useful when the output of one option mentions another option; for
      *   example, the CREDITS message might mention a LICENSE command for
      *   displaying the license, so you want to make that command available
      *   without cluttering the prompt with it.  
      */
     isListed = true
 
     /* our response keyword */
     responseKeyword = ''
 
     /* 
      *   a single character we accept as an alternative to our full
      *   response keyword, or nil if we don't accept a single-character
      *   response 
      */
     responseChar = nil
     
     /* 
      *   Match a response string to this option.  Returns true if the
      *   string matches our response, nil otherwise.  By default, we'll
      *   return true if the string exactly matches responseKeyword or
      *   exactly matches our responseChar (if that's non-nil), but this
      *   can be overridden to match other strings if desired.  By default,
      *   we'll match the response without regard to case.
      */
     responseMatches(response)
     {
         /* do all of our work in lower-case */
         response = response.toLower();
 
         /* 
          *   check for a match the full response keyword or to the single
          *   response character 
          */
         return (response == responseKeyword.toLower()
                 || (responseChar != nil
                     && response == responseChar.toLower()));
     }
 
     /*
      *   Carry out the option.  This is called when the player enters a
      *   response that matches this option.  This routine must perform the
      *   action of the option, then return true to indicate that we should
      *   ask for another option, or nil to indicate that the finishGame()
      *   routine should simply return.  
      */
     doOption()
     {
         /* tell finishGame() to ask for another option */
         return true;
     }
 
     /* 
      *   Flag: show the score with the end-of-game announcement.  If any
      *   option in the list of finishing options has this flag set, we'll
      *   show the score using the same message that the SCORE command
      *   uses. 
      */
     showScoreInFinish = nil
 ;
 
 /*
  *   QUIT option for finishGame.  The language-specific code should modify
  *   this to specify the description and response keywords.  
  */
 finishOptionQuit: FinishOption
     doOption()
     {
         /* 
          *   carry out the Quit action - this will signal a
          *   QuittingException, so this call will never return 
          */
         QuitAction.terminateGame();
     }
 ;
 
 /*
  *   RESTORE option for finishGame. 
  */
 finishOptionRestore: FinishOption
     doOption()
     {
         /* 
          *   Try restoring.  If this succeeds (i.e., it returns true), tell
          *   the caller to stop looping and to proceed with the game by
          *   returning nil.  If this fails, tell the caller to keep looping
          *   by returning true.
          */
         if (RestoreAction.askAndRestore())
         {
             /* 
              *   we succeeded, so we're now restored to some prior game
              *   state - terminate any remaining processing in the command
              *   that triggered the end-of-game options
              */
             throw new TerminateCommandException();
         }
         else
         {
             /* it failed - tell the caller to keep looping */
             return true;
         }
     }
 ;
 
 /*
  *   RESTART option for finishGame 
  */
 finishOptionRestart: FinishOption
     doOption()
     {
         /* 
          *   carry out the restart - this will not return, since we'll
          *   reset the game state and re-enter the game at the restart
          *   entrypoint 
          */
         RestartAction.doRestartGame();
     }
 ;
 
 /*
  *   START FROM BEGINNING option for failed startup restore.  This is just
  *   like finishOptionRestart, but shows a different option name.  
  */
 restoreOptionStartOver: finishOptionRestart
 ;
 
 /* 
  *   RESTORE ANOTHER GAME option for failed startup restore.  This is just
  *   like finishOptionRestore, but shows a different option name. 
  */
 restoreOptionRestoreAnother: finishOptionRestore
 ;
 
 /*
  *   UNDO option for finishGame 
  */
 finishOptionUndo: FinishOption
     doOption()
     {
         /* try performing the undo */
         if (UndoAction.performUndo(nil))
         {
             /* act as though UNDO were the last actual command, for AGAIN */
             AgainAction.saveForAgain(gPlayerChar, gPlayerChar,
                                      nil, UndoAction);
             
             /* 
              *   Success - terminate the current command with no further
              *   processing.
              */
             throw new TerminateCommandException();
         }
         else
         {
             /* 
              *   failure - show a blank line and tell the caller to ask
              *   for another option, since we couldn't carry out this
              *   option 
              */
             "<.p>";
             return true;
         }
     }
 ;
 
 /*
  *   FULL SCORE option for finishGame
  */
 finishOptionFullScore: FinishOption
     doOption()
     {
         /* show a blank line before the score display */
         "\b";
 
         /* run the Full Score action */
         FullScoreAction.showFullScore();
 
         /* show a paragraph break after the score display */
         "<.p>";
 
         /* 
          *   this option has now had its full effect, so tell the caller
          *   to go back and ask for a new option 
          */
         return true;
     }
 
     /* 
      *   by default, show the score with the end-of-game announcement when
      *   this option is included 
      */
     showScoreInFinish = true
 ;
 
 /*
  *   Option to show the score in finishGame.  This doesn't create a listed
  *   option in the set of offered options, but rather is simply a flag to
  *   finishGame() that the score should be announced along with the
  *   end-of-game announcement message. 
  */
 finishOptionScore: FinishOption
     /* show the score in the end-of-game announcement */
     showScoreInFinish = true
 
     /* this is not a listed option */
     isListed = nil
 
     /* this option isn't selectable, so it has no effect */
     doOption() { }
 ;
 
 /*
  *   CREDITS option for finishGame 
  */
 finishOptionCredits: FinishOption
     doOption()
     {
         /* show a blank line before the credits */
         "\b";
 
         /* run the Credits action */
         CreditsAction.execSystemAction();
 
         /* show a paragraph break after the credits */
         "<.p>";
 
         /* 
          *   this option has now had its full effect, so tell the caller
          *   to go back and ask for a new option 
          */
         return true;
     }
 ;
 
 /*
  *   AMUSING option for finishGame 
  */
 finishOptionAmusing: FinishOption
     /*
      *   The game must modify this object to define a doOption method.  We
      *   have no built-in way to show a list of amusing things to try, so
      *   if a game wants to offer this option, it must provide a suitable
      *   definition here.  (We never offer this option by default, so a
      *   game need not provide a definition if the game doesn't explicitly
      *   offer this option via the 'extra' argument to finishGame()).  
      */
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   The settings user interface.  This is a subclass of the Settings
  *   Manager that adds a command-line user interface, particularly to allow
  *   the user to view, save, and load the default settings.  
  *   
  *   Our user interface consists mainly of a pair of special commands: SAVE
  *   DEFAULTS and RESTORE DEFAULTS.  The SAVE DEFAULTS command tells the
  *   library to write out all of the current settings (at least, all of
  *   those that participate in this framework) to a file.  RESTORE DEFAULTS
  *   explicitly reads that same file and puts the stored settings into
  *   effect.  Finally, we'll also read the file and activate its stored
  *   settings when we start (or RESTART) the game.
  *   
  */
 settingsUI: settingsManager
     /* display all of the current settings */
     showAll()
     {
         local first = true;
 
         /* loop over all SettingsItem instances */
         forEachInstance(SettingsItem, new function(item)
         {
             /* add a separator if this isn't the first one */
             if (!first)
                 gLibMessages.settingsItemSeparator;
             
             /* show this item's description */
             item.settingDesc;
             
             /* it's no longer the first */
             first = nil;
         });
     }
 
     /* 
      *   Save settings, and display an acknowledgment message (or an error
      *   message, if necessary) for the user's edification.
      */
     saveSettingsMsg()
     {
         /* catch any errors */
         try
         {
             /* save the settings */
             saveSettings();
 
             /* if we got this far, declare success */
             gLibMessages.savedDefaults();
         }
         catch (FileCreationException fce)
         {
             /* we couldn't open the file */
             gLibMessages.defaultsFileWriteError;
         }
     }
 
     /*
      *   Restore settings, and display an acknowledgment or error message,
      *   as appropriate.  
      */
     restoreSettingsMsg()
     {
         /* catch any errors */
         try
         {
             /* restore the settings */
             restoreSettings();
 
             /* if we got this far, declare success */
             gLibMessages.restoredDefaults();
         }
         catch (SettingsNotSupportedException sns)
         {
             /* this interpreter doesn't support the settings file */
             gLibMessages.defaultsFileNotSupported;
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Utility functions 
  */
 
 /*
  *   nilToList - convert a 'nil' value to an empty list.  This can be
  *   useful for mix-in classes that will be used in different inheritance
  *   contexts, since the classes might or might not inherit a base class
  *   definition for list-valued methods such as preconditions.  This
  *   provides a usable default for list-valued methods that return nothing
  *   from superclasses. 
  */
 nilToList(val)
 {
     return (val != nil ? val : []);
 }
 
 /* 
  *   partitionList - partition a list into a pair of two lists, the first
  *   containing items that match the predicate 'fn', the second containing
  *   items that don't match 'fn'.  'fn' is a function pointer (usually an
  *   anonymous function) that takes a single argument - a list element -
  *   and returns true or nil.
  *   
  *   The return value is a list with two elements.  The first element is a
  *   list giving the elements of the original list for which 'fn' returns
  *   true, the second element is a list giving the elements for which 'fn'
  *   returns nil.
  *   
  *   (Contributed by Tommy Nordgren.)  
  */
 partitionList(lst, fn)
 {
     local lst1 = lst.subset(fn);
     local lst2 = lst.subset({x : !fn(x)});
     
     return [lst1, lst2];
 }
 
 /*
  *   Determine if list a is a subset of list b.  a is a subset of b if
  *   every element of a is in b.  
  */
 isListSubset(a, b)
 {
     /* a can't be a subset if it has more elements than b */
     if (a.length() > b.length())
         return nil;
     
     /* check each element of a to see if it's also in b */
     foreach (local cur in a)
     {
         /* if this element of a is not in b, a is not a subset of b */
         if (b.indexOf(cur) == nil)
             return nil;
     }
 
     /* 
      *   we didn't find any elements of a that are not also in b, so a is a
      *   subset of b 
      */
     return true;
 }
 
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11