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; /* ------------------------------------------------------------------------ */ /* * We refer to some properties defined primarily in score.t - that's an * optional module, though, so make sure the compiler has heard of these. */ property calcMaxScore, runScoreNotifier; /* ------------------------------------------------------------------------ */ /* * 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() { /* * 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 */ } /* * Build a saved game metadata table. This returns a LookupTable * containing string key/value pairs that are stored in saved game * files, providing descriptive information that can be displayed to * the user when browsing a collection of save files. This is called * each time we execute a SAVE command, so that we store the current * context of the game. * * Some interpreters display information from this table when * presenting the user with a list of files for RESTORE. The * contents of the table are intentionally open-ended to allow for * future extensions, but at the moment, the following keys are * specifically defined (note that capitalization must be exact): * * UserDesc - descriptive text entered by the user (this should * simply be the contents of the 'userDesc' parameter). This is * treated as ordinary plain text (i.e., no HTML or other markups are * interpreted in this text). * * AutoDesc - descriptive text generated by the game to describe the * saved position. This text can contain the simple HTML markups * <b>..</b>, <i>..</i>, and <br> for formatting. * * Return nil if you don't want to save any metadata information. * * 'userDesc' is an optional string entered by the user via the Save * Game dialog. Some interpreters let the user enter a description * for a saved game via the file selector dialog; the descriptive * text is separate from the filename, and is intended to let the * user enter a more free-form description than would be allowed in a * filename. This text, if any, is passed to use via the 'userDesc' * parameter. */ getSaveDesc(userDesc) { /* create the lookup table */ local t = new LookupTable(); /* store the user description, if provided */ if (userDesc != nil) t['UserDesc'] = userDesc; /* start our auto description with the current room name */ desc = gPlayerChar.getLookAroundName() + '; '; /* if we're keeping score, include the score */ if (libGlobal.scoreObj != nil) desc += toString(libGlobal.scoreObj.totalScore) + ' points in '; /* add the number of turns so far */ desc += toString(libGlobal.totalTurns) + ' moves'; /* add the auto description */ t['AutoDesc'] = desc; /* return the table */ return t; } /* * 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 default, so that the player can refer to the player * character in either the first or second person, regardless of how * the game refers to the PC. * * 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 * define "correct" in the case of first or second person as the * complement of what the game uses: if the game calls the PC "me", * the player must say "you", and vice versa. In a third-person * game, the player must also refer to the PC in the third person. * * We set the default to allow using "you" and "me" interchangeably * because (a) this will create no confusion in most games, and (b) * many players would be annoyed otherwise. For one thing, most * experienced IF players will be rather set in their ways; they'll * be accustomed to using either "me" or "you" (but usually "me") to * refer to the PC, and will tend out of habit to do so even in games * that don't use the traditional second-person narration format. * For another thing, different players have different ideas about * whether the PC is "you" or "me" in input, even in a conventional * second-person game. Some players think in terms of a conversation * with the narrator, in which case the narrator's "you" is the * player's "me", and vice versa; other players are rather more * literal-minded, assuming that if the game talks about "you" then * so should the player. * * Even in games that use first-person or third-person narration, it * seems unlikely that there will be a separate second-person element * to the narration, and as long as that's true, it should cause no * confusion for the game to accept "you" and "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 set, announcement messages that * list objects by name will add distinguishing details to indicate * specifically which objects are being referred to. This applies to * messages announcing default objects, vaguely matched objects, and * multiple objects. * * A distinguisher is a parser object that tells two objects apart by * some feature that's different in the two objects. The key thing * is that the difference has some natural language phrasing * associated with it, both on the input side and the output side. * For example, the locational distinguisher can tell two objects * apart if they have different containers, since it can describe the * objects by adding containment phrases like "in the box" or "on the * table". * * When this flag is turned on, the parser will try to pick a * distinguisher that can tell apart the specific objects mentioned * in the announcement, from one another or from other objects in * scope, depending on the context. The point is to help make it * clearer to the player the exact objects being referred to. * * Even when this flag is set, the parser tries to minimize the use * of additional distinguishing detail. The goal is to be natural in * the usage, adding detail only when a human speaker would, which is * when the phrasing would otherwise be ambiguous. * * Setting this flag to nil makes the parser simply use the basic * name of each object in an announcement. You can use this setting * if you find that the distinguisher mode generates too much fussy * detail for your taste. */ useDistinguishersInAnnouncements = true /* * How should we handle object announcements when an object is * automatically disambiguated? This controls how an action is * described when the parser uses the logicalness rules to narrow * down the object for a noun phrase when the noun phrase could refer * to multiple in-scope objects. There are three options: * * AnnounceUnclear - Make a parenthetical announcement only when the * choice is *not* clear (as described below). This is the original * library behavior, from before this option was added. * * AnnounceClear - Make a parenthetical announcement (for example, * "(the red door)") for all disambiguated objects, whether clear or * unclear. We don't make an announcement when there's only one * in-scope object matching the noun phrase - the announcement is * only when multiple objects match the words. * * DescribeClear - For *unclear* disambiguation, make a parenthetical * announcement, to emphasize that the parser had to make a choice. * For *clear* disambiguation, skip the announcement, but *do* use a * verbose version of the library message in place of one of the * terse default replies. For example, for >TAKE BOX, instead of * "Taken", we would reply "You take the green box." The longer * reply in these cases always mentions the involved object by name, * to make it clear exactly which object we chose to use. * * The default setting is DescribeClear. * * This only applies when the disambiguation choice is clear - that * is, when there's exactly one in-scope object that passes the * logicalness tests. For example, if the current location contains * a red door that's open and a green door that's closed, CLOSE DOOR * clearly refers to the red door because the other one is already * closed - it's not logical. There are other cases where the * disambiguation is a best guess rather than a clear choice, such as * when there are multiple logical objects but there's one that's * more likely than the others due to the logicalRank results. In * those best-guess situations, the parser always announces its * decision, because it's entirely plausible that the player meant * one of the other logical, but less likely, choices. */ ambigAnnounceMode = DescribeClear /* * Should the "before" notifications (beforeAction, roomBeforeAction, * and actorAction) run before or after the "check" phase? * * The library traditionally ran the "before" notifiers first, so * this is the default. However, in many ways it's more logical and * useful to run "check" first. That way, you can consider the * action to be more or less committed by the time the "before" * notifiers are invoked. Of course, a command is never *truly* * committed until it's actually been executed, since a "before" * handler could always cancel it. But this is relatively rare - * "before" handlers usually carry out side effects, so it's very * useful to be able to know that the command has already passed all * of its own internal checks by the time "before" is invoked - that * way, you can invoke side effects without worrying that the command * will subsequently fail. */ beforeRunsBeforeCheck = true ; /* * 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 */ aioClearScreen(); } /* ------------------------------------------------------------------------ */ /* * 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) { libGlobal.commandLineArgs = args; mainCommon(&newGame); } /* * 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) { libGlobal.commandLineArgs = args; mainCommon(&restoreAndRunGame, restoreFile); } /* * Common main entrypoint - this handles starting a new game or restoring * an existing saved state. */ mainCommon(prop, [args]) { try { /* restore the global default settings */ settingsManager.restoreSettings(); } catch (Exception exc) { /* * ignore any errors restoring defaults - it's not critical that * we restore this file automatically */ } try { /* at the start of the session, set up the UI subsystem */ if (mainGlobal.restartID == 0) { /* initialize the UI */ initUI(); /* * tell the system library to call our UI shutdown function * at program exit */ mainAtExit.addHandler(terminateUI); } /* initialize the display */ initDisplay(); /* call the appropriate gameMain method */ gameMain.(prop)(args...); } catch (QuittingException q) { /* * This exception is 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, ¬eSenseChanges, 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 = [] /* cached length of the event list */ eventListLen = (eventList.length()) /* 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() { /* get our current event state */ local idx = getScriptState(); /* get the list (evaluate it once to avoid repeated side effects) */ local lst = eventList; /* cache the length */ eventListLen = lst.length(); /* if it's a valid index in our list, fire the event */ if (idx >= 1 && idx <= eventListLen) { /* carry out the event */ doScriptEvent(lst[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 > eventListLen) 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 < eventListLen) ++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() { /* check the odds to see if we want to fire an event at all */ if (!checkEventOdds()) return; /* get our next random number */ local idx = getNextRandom(); /* cache the list and its length, to avoid repeated side effects */ local lst = eventList; eventListLen = lst.length(); /* run the event, if the index is valid */ if (idx >= 1 && idx <= eventListLen) doScriptEvent(lst[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(eventListLen) + 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 /* * Flag: suppress repeats in the shuffle. If this is true, it * prevents a given event from showing up twice in a row, which could * otherwise happen right after a shuffle. This is ignored for lists * with one or two events: it's impossible to prevent repeats in a * one-element list, and doing so in a two-element list would produce * a predictable A-B-A-B... pattern. * * You might want to set this to nil for lists of three or four * elements, since such short lists can result in fairly * un-random-looking sequences when repeats are suppressed, because * the available number of permutations drops significantly. */ suppressRepeats = true /* process the next step of the script */ doScript() { /* cache the lists to avoid repeated side effects */ local firstLst = firstEvents; local firstLen = firstLst.length(); local lst = eventList; eventListLen = lst.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. */ local evt; if (curScriptState <= firstLen) { /* simply fetch the next string from firstEvents */ evt = firstEvents[curScriptState++]; } else if (!shuffleFirst && curScriptState <= firstLen + eventListLen) { /* fetch the next string from eventList */ evt = lst[curScriptState++ - firstLen]; } else { /* we're showing shuffled strings from the eventList list */ evt = lst[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, eventListLen); /* apply our suppressRepeats option to the shuffled list */ shuffledList_.suppressRepeats = suppressRepeats; } /* 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. * * Note that we ignore this for a list of one element, since it's * obviously impossible to avoid repeats in this case. We also * ignore it for a two-element list, since this would produce the * predictable pattern A-B-A-B..., defeating the purpose of the * shuffle. */ 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 three 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 we have one element, there's obviously no point in trying to * suppress repeats. If we have two elements, we *still* don't * want to suppress repeats, because in this case we'd generate a * predicatable A-B-A-B pattern (because we could never have two * A's or two B's in a row). */ if (justReshuffled && suppressRepeats && valuesAvail > 2) { /* * 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. This is * simply a list of integers from rangeMin to rangeMax. */ if (valueList == nil) { local ele = rangeMin; valueList = List.generate({i: ele++}, rangeMax - rangeMin + 1); } /* 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 entries 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 /* * The game's IFID, as defined in the game's main module ID object. * If the game has multiple IFIDs in the module list, this will store * only the first IFID in the list. NOTE: the library initializes * this automatically during preinit; don't set this manually. */ IFID = nil /* * Command line arguments. The library sets this to a list of * strings containing the arguments passed to the program on the * command line. This list contains the command line arguments * parsed according to the local conventions for the operating system * and C++ library. The standard parsing procedure used by most * systems is to break the line into tokens delimited by space * characters. Many systems also allow space characters to be * embedded in tokens by quoting the tokens. The first argument is * always the name of the .t3 file currently executing. */ commandLineArgs = [] /* * Retrieve a "switch" from the command line. Switches are options * specifies with the conventional Unix "-xxx" notation. This * searches for a command option that equals the given string or * starts with the given substring. If we find it, we return the * part of the option after the given substring - this is * conventionally the value of the switch. For example, the command * line might look like this: * *. t3run mygame.t3 -name=MyGame -user=Bob * * Searching for '-name=' would return 'MyGame', and searching for * '-user=' would return' Bob'. * * If the switch is found but has no value attached, the return value * is an empty string. If the switch isn't found at all, the return * value is nil. */ getCommandSwitch(s) { /* search from argument 2 to the last switch argument */ local args = commandLineArgs; for (local i in 2..args.length()) { /* * if this isn't a switch, or is the special "-" last switch * marker, we're done */ local a = args[i]; if (!a.startsWith('-') || a == '-') return nil; /* check for a match */ if (a.startsWith(s)) return a.substr(s.length() + 1); } /* didn't find it */ return 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; /* * Explicitly run any final score notification now. This will ensure * that any points awarded in the course of the final command that * brought us to this point will generate the usual notification, and * that the notification will appear at a reasonable place, just * before the termination message. */ if (libGlobal.scoreObj != nil) libGlobal.scoreObj.runScoreNotifier(); /* 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, function(item) { /* include only items that want to participate in the listing */ if (item.includeInListing) { /* 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 (Exception exc) { /* 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; } catch (Exception exc) { /* display other errors */ gLibMessages.defaultsFileReadError(exc); } } ; /* ------------------------------------------------------------------------ */ /* * 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 5/16/2013 from TADS version 3.1.3
Generated on 5/16/2013 from TADS version 3.1.3