actor.t | documentation |
#charset "us-ascii" /* * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. * * TADS 3 Library - actors * * This module provides definitions related to actors, which represent * characters in the game. */ /* include the library header */ #include "adv3.h" /* ------------------------------------------------------------------------ */ /* * Implied command modes */ enum ModePlayer, ModeNPC; /* ------------------------------------------------------------------------ */ /* * A Topic is an object representing some piece of knowledge in the * story. Actors can use Topic objects in commands such as "ask" and * "tell". * * A physical simulation object can be a Topic through multiple * inheritance. In addition, a game can define Topic objects for * abstract conversation topics that don't correspond to simulation * objects; for example, a topic could be created for "the meaning of * life" to allow a command such as "ask guru about meaning of life." * * The key distinction between Topic objects and regular objects is that * a Topic can represent an abstract, non-physical concept that isn't * connected to any "physical" object in the simulation. */ class Topic: VocabObject /* * Is the topic known? If this is true, the topic is in scope for * actions that operate on topics, such as "ask about" and "tell * about." If this is nil, the topic isn't known. * * By default, we mark all topics as known to begin with, which * allows discussion of any topic at any time. Some authors prefer * to keep track of which topics the player character actually has * reason to know about within the context of the game, making topics * available for conversation only after they become known for some * good reason, such as another character mentioning them in * conversation. * * Note that, as with Thing.isKnown, this is only the DEFAULT 'known' * property. Each actor can have its own separate 'known' property * by defining the actor's 'knownProp' to a different property name. */ isKnown = true /* * Topics are abstract objects, so they can't be sensed with any of * the physical senses, even if they're ever included as part of a * containment hierarchy (which might be convenient in some cases * for purposes of associating a topic with a physical object, for * example). */ canBeSensed(sense, trans, ambient) { return nil; } /* a topic cannot by default be used to resolve a possessive phrase */ canResolvePossessive = nil ; /* ------------------------------------------------------------------------ */ /* * FollowInfo - this is an object that tracks an actor's knowledge of * the objects that the actor can follow, which are objects that actor * has witnessed leaving the current location. We keep track of each * followable object and the direction we saw it depart. */ class FollowInfo: object /* the object we can follow */ obj = nil /* the TravelConnector the object traversed to leave */ connector = nil /* * The source location - this is the location we saw the object * depart. We keep track of this because an actor can follow an * object only if the actor is starting from the same location where * the actor saw the object depart. */ sourceLocation = nil ; /* ------------------------------------------------------------------------ */ /* * Postures. A posture describes how an actor is internally positioned: * standing, lying, sitting. We represent postures with objects of * class Posture to make it easier to add new game-specific postures. */ class Posture: object /* * Try getting the current actor into this posture within the given * location, by running an appropriate implied command. */ tryMakingPosture(loc) { } /* put the actor into our posture via a nested action */ setActorToPosture(actor, loc) { } ; /* * Standing posture - this is the default posture, which an actor * normally uses for travel. Actors are generally in this posture any * time they are not sitting on something, lying on something, or * similar. */ standing: Posture tryMakingPosture(loc) { return tryImplicitAction(StandOn, loc); } setActorToPosture(actor, loc) { nestedActorAction(actor, StandOn, loc); } ; /* * Sitting posture. */ sitting: Posture tryMakingPosture(loc) { return tryImplicitAction(SitOn, loc); } setActorToPosture(actor, loc) { nestedActorAction(actor, SitOn, loc); } ; /* * Lying posture. */ lying: Posture tryMakingPosture(loc) { return tryImplicitAction(LieOn, loc); } setActorToPosture(actor, loc) { nestedActorAction(actor, LieOn, loc); } ; /* ------------------------------------------------------------------------ */ /* * Conversation manager output filter. We look for special tags in the * output stream: * * <.reveal key> - add 'key' to the knowledge token lookup table. The * 'key' is an arbitrary string, which we can look up in the table to * determine if the key has even been revealed. This can be used to make * a response conditional on another response having been displayed, * because the key will only be added to the table when the text * containing the <.reveal key> sequence is displayed. * * <.convnode name> - switch the current responding actor to conversation * node 'name'. * * <.convstay> - keep the responding actor in the same conversation node * as it was in at the start of the current response * * <.topics> - schedule a topic inventory for the end of the turn (just * before the next command prompt) */ conversationManager: OutputFilter, PreinitObject /* * Custom extended tags. Games and library extensions can add their * own tag processing as needed, by using 'modify' to extend this * object. There are two things you have to do to add your own tags: * * First, add a 'customTags' property that defines a regular * expression for your added tags. This will be incorporated into * the main pattern we use to look for tags. Simply specify a * string that lists your tags separated by "|" characters, like * this: * * customTags = 'foo|bar' * * Second, define a doCustomTag() method to process the tags. The * filter routine will call your doCustomTag() method whenever it * finds one of your custom tags in the output stream. */ customTags = nil doCustomTag(tag, arg) { /* do nothing by default */ } /* filter text written to the output stream */ filterText(ostr, txt) { local start; /* scan for our special tags */ for (start = 1 ; ; ) { local match; local arg; local actor; local sp; local tag; local nxtOfs; /* scan for the next tag */ match = rexSearch(tagPat, txt, start); /* if we didn't find it, we're done */ if (match == nil) break; /* note the next offset */ nxtOfs = match[1] + match[2]; /* get the argument (the third group from the match) */ arg = rexGroup(3); if (arg != nil) arg = arg[3]; /* pick out the tag */ tag = rexGroup(1)[3].toLower(); /* check which tag we have */ switch (tag) { case 'reveal': /* reveal the key by adding it to our database */ setRevealed(arg); break; case 'convbegin': /* * Internal tag - starting a conversational response for * an actor, identified by an index in our idToActor * vector. Get the actor. */ actor = idToActor[toInteger(arg)]; /* * since we're just starting a response, clear the flag * in the actor indicating that a ConvNode has been set * in the course of this response */ actor.responseSetConvNode = nil; /* remember the new responding actor */ respondingActor = actor; /* done */ break; case 'convend': /* * Ending a conversational response for a given actor, * identified by the first argument, which is an index in * our idToActor vector. */ sp = arg.find(' '); actor = idToActor[toInteger(arg.substr(1, sp - 1))]; /* the rest of the argument is the default new ConvNode */ arg = arg.substr(sp + 1); /* if the new ConvNode is empty, it means no ConvNode */ if (arg == '') arg = nil; /* * if we didn't explicitly set a new ConvNode in the * course of this response, apply the default */ if (!actor.responseSetConvNode) actor.setConvNodeReason(arg, 'convend'); /* * Since we've just finished showing a message that * specifically refers to this actor, the player should * be able to refer to this actor using a pronoun on the * next command. Set the responding actor as the * antecedent for the appropriate singular pronouns for * the player character. Note that we do this at the end * of the response, so that the antecedent is the last * one if we have more than one. */ gPlayerChar.setPronounObj(actor); /* done */ break; case 'convnode': /* * If there's a current responding actor, set its current * conversation node. */ if (respondingActor != nil) { /* * Set the new node. While we're working, capture * any output that occurs so that we can insert it * into the output stream just after the <.convnode> * tag, so that any text displayed within the * ConvNode's activation method (noteActive) is * displayed in the proper order. */ local ctxt = mainOutputStream.captureOutput( {: respondingActor.setConvNodeReason(arg, 'convnode') }); /* re-insert any text we captured */ txt = txt.substr(1, nxtOfs - 1) + ctxt + txt.substr(nxtOfs); } break; case 'convstay': /* * leave the responding actor in the old conversation * node - we don't need to change the ConvNode, but we do * need to note that we've explicitly set it */ if (respondingActor != nil) respondingActor.responseSetConvNode = true; break; case 'topics': /* schedule a topic inventory listing */ scheduleTopicInventory(); break; default: /* check for an extended tag */ doCustomTag(tag, arg); break; } /* continue the search after this match */ start = nxtOfs; } /* * remove the tags from the text by replacing every occurrence * with an empty string, and return the result */ return rexReplace(tagPat, txt, '', ReplaceAll); } /* regular expression pattern for our tags */ tagPat = static new RexPattern( '<nocase><langle><dot>' + '(reveal|convbegin|convend|convnode|convstay|topics' + (customTags != nil ? '|' + customTags : '') + ')' + '(<space>+(<^rangle>+))?' + '<rangle>') /* * Schedule a topic inventory request. Game code can call this at * any time to request that the player character's topic inventory * be shown automatically just before the next command prompt. In * most cases, game code won't call this directly, but will request * the same effect using the <.topics> tag in topic response text. */ scheduleTopicInventory() { /* note that we have a request for a prompt-time topic inventory */ pendingTopicInventory = true; } /* * Show or schedule a topic inventory request. If the current * action has a non-default command report, schedule it; otherwise, * show it now. * * If there's a non-default report, don't suggest the topics now; * instead, schedule a topic inventory for the end of the turn. * When we have a non-default report, the report could change the * ConvNode for the actor, so we don't want to show the topic * inventory until we've had a chance to process all of the reports. */ showOrScheduleTopicInventory(actor, otherActor) { /* check for a non-default command report in the current action */ if (gTranscript.currentActionHasReport( {x: x.ofKind(MainCommandReport)})) { /* we have a non-default report - defer the topic inventory */ scheduleTopicInventory(); } else { /* we have only a default report, so show the inventory now */ actor.suggestTopicsFor(otherActor, nil); } } /* * Note that an actor is about to give a response through a * TopicEntry object. We'll remember the actor so that we'll know * which actor is involved in a <.convnode> operation. */ beginResponse(actor) { /* if the actor doesn't have an ID yet, assign one */ if (actor.convMgrID == nil) { /* add the actor to our vector of actors */ idToActor.append(actor); /* the ID is simply the index in this vector */ actor.convMgrID = idToActor.length(); } /* output a <.convbegin> for the actor */ gTranscript.addReport(new ConvBeginReport(actor.convMgrID)); } /* * Finish the response - call this after we finish handling the * response. There must be a subsequent matching call to this * routine whenever beginResponse() is called. * * 'node' is the default new ConvNode the actor for the responding * actor. If another ConvNode was explicitly set in the course of * handling the response, this is ignored, since the explicit * setting overrides this default. */ finishResponse(actor, node) { local prv; local oldNode; /* if the node is a ConvNode object, use its name */ if (node != nil && node.ofKind(ConvNode)) node = node.name; /* * if the previous report was our ConvBeginReport, the * conversation display was empty, so ignore the whole thing */ if ((prv = gTranscript.getLastReport()) != nil && prv.ofKind(ConvBeginReport) && prv.actorID == actor.convMgrID) { /* remove the <.convbegin> report - we're canceling it out */ gTranscript.deleteLastReport(); /* we're done - do not generate the <.convend> */ return; } /* * if the actor has a current ConvNode, and our default next * node is nil, and the current node is marked as "sticky," stay * in the current node rather than switching to a nil default */ if (node == nil && (oldNode = actor.curConvNode) != nil && oldNode.isSticky) { /* it's sticky, so stay at this node */ node = oldNode.name; } /* output a <.convend> for the actor */ gTranscript.addReport(new ConvEndReport(actor.convMgrID, node)); } /* * The current responding actor. Actors should set this when they're * about to show a response to an ASK, TELL, etc. */ respondingActor = nil /* * Mark a tag as revealed. This adds an entry for the tag to the * revealedNameTab table. We simply set the table entry to 'true'; * the presence of the tag in the table constitutes the indication * that the tag has been revealed. * * (Games and library extensions can use 'modify' to override this * and store more information in the table entry. For example, you * could store the time when the information was first revealed, or * the location where it was learned. If you do override this, just * be sure to set the revealedNameTab entry for the tag to a non-nil * and non-zero value, so that any code testing the presence of the * table entry will see that the slot is indeed set.) */ setRevealed(tag) { revealedNameTab[tag] = true; } /* * The global lookup table of all revealed keys. This table is keyed * by the string naming the revelation; the value associated with * each key is not used (we always just set it to true). */ revealedNameTab = static new LookupTable(32, 32) /* a vector of actors, indexed by their convMgrID values */ idToActor = static new Vector(32) /* preinitialize */ execute() { /* add every ConvNode object to our master table */ forEachInstance(ConvNode, { obj: obj.getActor().convNodeTab[obj.name] = obj }); /* * set up the prompt daemon that makes automatic topic inventory * suggestions when appropriate */ new PromptDaemon(self, &topicInventoryDaemon); } /* * Prompt daemon: show topic inventory when appropriate. When a * response explicitly asks us to show a topic inventory using the * <.topics> tag, or when other game code asks us to show topic * inventory by calling scheduleTopicInventory(), we'll show the * inventory just before the command input prompt. */ topicInventoryDaemon() { /* if we have a topic inventory scheduled, show it now */ if (pendingTopicInventory) { /* * Show the player character's topic inventory. This is not * an explicit inventory request, since the player didn't ask * for it. */ gPlayerChar.suggestTopics(nil); /* we no longer have a pending inventory request */ pendingTopicInventory = nil; } } /* flag: we have a pending prompt-time topic inventory request */ pendingTopicInventory = nil ; /* ------------------------------------------------------------------------ */ /* * A plug-in topic database. The topic database is a set of TopicEntry * objects that specify the responses to queries on particular topics. * The exact nature of the queries that a particular topic database * handles is up to the database subclass to define; we just provide the * abstract mechanism for finding and displaying responses. * * This is a "plug-in" database in that it's meant to be added into other * classes using multiple inheritance. This isn't meant to be used as a * stand-alone abstract topic entry container. */ class TopicDatabase: object /* * Is the topic group active? A TopicEntry always checks with its * container to see if the children of the container are active. By * default, everything in the database is active. */ topicGroupActive = true /* * Get the score adjustment for all topic entries contained within. * The default adjustment is zero; TopicGroup objects can use this to * adjust the score for their nested entries. */ topicGroupScoreAdjustment = 0 /* * Handle a topic. Look up the topic in our topic list for the * given conversational action type. If we find a match, we'll * invoke the matching topic list entry to handle it. We'll return * true if we find a match, nil if not. */ handleTopic(fromActor, topic, convType, path) { local resp; /* find the best response */ resp = findTopicResponse(fromActor, topic, convType, path); /* if we found a match, let it handle the topic */ if (resp != nil) { /* show the response */ showTopicResponse(fromActor, topic, resp); /* tell the caller we handled it */ return true; } else { /* tell the caller we didn't handle it */ return nil; } } /* show the response we found for a topic */ showTopicResponse(fromActor, topic, resp) { /* let the response object handle it */ resp.handleTopic(fromActor, topic); } /* * find the best response (a TopicEntry object) for the given topic * (a ResolvedTopic object) */ findTopicResponse(fromActor, topic, convType, path) { local topicList; local best, bestScore; /* * Get the list of possible topics for this conversation type. * The topic list is contained in one of our properties; exactly * which property is determined by the conversation type. */ topicList = self.(convType.topicListProp); /* if the topic list is nil, we obviously won't find the topic */ if (topicList == nil) return nil; /* scan our topic list for the best match(es) */ best = new Vector(); bestScore = nil; foreach (local cur in topicList) { /* get this item's score */ local score = cur.adjustScore(cur.matchTopic(fromActor, topic)); /* * If this item has a score at all, and the topic entry is * marked as active, and it's best (or only) score so far, * note it. Ignore topics marked as not active, since * they're in the topic database only provisionally. */ if (score != nil && cur.checkIsActive() && (bestScore == nil || score >= bestScore)) { /* clear the vector if we've found a better score */ if (bestScore != nil && score > bestScore) best = new Vector(); /* add this match to the list of ties for this score */ best.append(cur); /* note the new best score */ bestScore = score; } } /* * If the best-match list is empty, we have no matches. If * there's just one match, we have a winner. If we found more * than one match tied for first place, we need to pick one * winner. */ if (best.length() == 0) { /* no matches at all */ best = nil; } else if (best.length() == 1) { /* exactly one match - it's easy to pick the winner */ best = best[1]; } else { /* * We have multiple topics tied for first place. Run through * the topic list and ask each topic to propose the winner. */ local toks = topic.topicProd.getOrigTokenList().mapAll( {x: getTokVal(x)}); local winner = nil; foreach (local t in best) { /* ask this topic what it thinks the winner should be */ winner = t.breakTopicTie(best, topic, fromActor, toks); /* if the topic had an opinion, we can stop searching */ if (winner != nil) break; } /* * If no one had an opinion, run through the list again and * try to pick by vocabulary match strength. This is only * possible when all of the topics are associated with * simulation objects; if any topics have pattern matches, we * can't use this method. */ if (winner == nil) { local rWinner = nil; foreach (local t in best) { /* get this topic's match object(s) */ local m = t.matchObj; if (m == nil) { /* * there's no match object - it's not comparable * to others in terms of match strength, so we * can't use this method to break the tie */ winner = nil; break; } /* * If it's a list, search for an element with a * ResolveInfo entry in the topic match, using the * strongest match if we find more than one. * Otherwise, just use the strength of this match. */ local ri; if (m.ofKind(Collection)) { /* search for a ResolveInfo object */ foreach (local mm in m) { /* get this topic */ local riCur = topic.getResolveInfo(mm); /* if this is the best match so far, keep it */ if (compareVocabMatch(riCur, ri) > 0) ri = riCur; } } else { /* get the ResolveInfo object */ ri = topic.getResolveInfo(m); } /* * if we didn't find a match, we can't use this * method to break the tie */ if (ri == nil) { winner = nil; break; } /* * if this is the best match so far, elect it as the * tentative winner */ if (compareVocabMatch(ri, rWinner) > 0) { rWinner = ri; winner = t; } } } /* * if there's a tie-breaking winner, use it; otherwise just * arbitrarily pick the first item in the list of ties */ best = (winner != nil ? winner : best[1]); } /* * If there's a hierarchical search path, AND this topic entry * defines a deferToEntry() method, look for matches in the * inferior databases on the path and check to see if we want to * defer to one of them. */ if (best != nil && path != nil && best.propDefined(&deferToEntry)) { /* look for a match in each inferior database */ for (local i = 1, local len = path.length() ; i <= len ; ++i) { local inf; /* * Look up an entry in this inferior database. Pass in * the remainder of the path, so that the inferior * database can consider further deferral to its own * inferior databases. */ inf = path[i].findTopicResponse(fromActor, topic, convType, path.sublist(i + 1)); /* * if we found an entry in this inferior database, and * our entry defers to the inferior entry, then ignore * the match in our own database */ if (inf != nil && best.deferToEntry(inf)) return nil; } } /* return the best matching response object, if any */ return best; } /* * Compare the vocabulary match strengths of two ResolveInfo objects, * for the purposes of breaking ties in topic matching. Uses the * usual comparison/sorting return value conventions: -1 means that a * is weaker than b, 0 means they're equivalent, 1 means a is * stronger than b. */ compareVocabMatch(a, b) { /* * If both are nil, they're equivalent. If one or the other is * nil, the non-nil item is stronger. */ if (a == nil && b == nil) return 0; if (a == nil) return -1; if (b == nil) return 1; /* * Both are valid objects, so compare based on the vocabulary * match flags. */ local fa = a.flags_, fb = b.flags_; /* check plural truncations - no plural truncation is better */ if ((fa & PluralTruncated) && !(fb & PluralTruncated)) return -1; if (!(fa & PluralTruncated) && (fb & PluralTruncated)) return 1; /* check any truncation - no truncation is better */ if ((fa & VocabTruncated) && !(fb & VocabTruncated)) return -1; if (!(fa & VocabTruncated) && (fb & VocabTruncated)) return 1; /* we can't find any reason to prefer one over the other */ return 0; } /* show our suggested topic list */ showSuggestedTopicList(lst, asker, askee, explicit) { /* get the asking actor's scope list for use later */ scopeList = asker.scopeList(); /* remove items that have redundant list groups and full names */ for (local i = 1, local len = lst.length() ; i <= len ; ++i) { local a = lst[i]; /* check for redundant elements */ for (local j = i + 1 ; j <= len ; ++j) { local b = lst[j]; /* * If item 'a' matches item 'b', and both are active, * remove item 'b'. We only need to remove redundant items if they're both active, since inactive items */ if (a.suggestionGroup == b.suggestionGroup && a.fullName == b.fullName && a.isSuggestionActive(asker, scopeList) && b.isSuggestionActive(asker, scopeList)) { /* delete item 'b' from the list */ lst.removeElementAt(j); /* adjust our indices for the deletion */ --j; --len; } } } /* show our list */ new SuggestedTopicLister(asker, askee, explicit) .showList(asker, nil, lst, 0, 0, nil, nil); } /* * Flag: this database level should limit topic suggestions (for the * TOPICS and TALK TO commands) to its own topics, excluding any * topics inherited from the "broader" context. If this property is * set to true, then we won't include suggestions from any lower * level of the database hierarchy. If this property is nil, we'll * also include any topic suggestions from the broader context. * * Topic databases are arranged into a fixed hierarchy for an actor. * At the top level is the current ConvNode object; at the next level * is the ActorState; and at the bottom level is the Actor itself. * So, if the ConvNode's limitSuggestions property is set to true, * then the suggestions for the actor will include ONLY the ConvNode. * If the ConvNode has the property set to nil, but the ActorState * has it set to true, then we'll include the ConvNode and the * ActorState suggestions. * * By default, we set this to nil. This should usually be set to * true for any ConvNode or ActorState where the NPC won't allow the * player to stray from the subject. For example, if a ConvNode only * accepts a YES or NO response to a question, then this property * should probably be set to true in the ConvNode, since other * suggested topics won't be accepted as conversation topics as long * as the ConvNode is active. */ limitSuggestions = nil /* * Add a topic to our topic database. We'll add it to the * appropriate list or lists as indicated in the topic itself. * 'topic' is a TopicEntry object. */ addTopic(topic) { /* add the topic to each list indicated in the topic */ foreach (local cur in topic.includeInList) addTopicToList(topic, cur); } /* remove a topic from our topic database */ removeTopic(topic) { /* remove the topic from each of its lists */ foreach (local cur in topic.includeInList) removeTopicFromList(topic, cur); } /* add a suggested topic */ addSuggestedTopic(topic) { /* add the topic to our suggestion list */ addTopicToList(topic, &suggestedTopics); } /* remove a suggested topic */ removeSuggestedTopic(topic) { /* add the topic to our suggestion list */ removeTopicFromList(topic, &suggestedTopics); } /* * Add a topic to the given topic list. The topic list is given as a * property point; for example, we'd specify &askTopics to add the * topic to our ASK list. */ addTopicToList(topic, listProp) { /* if we haven't created this topic list vector yet, create it now */ if (self.(listProp) == nil) self.(listProp) = new Vector(8); /* add the topic */ self.(listProp).append(topic); } /* remove a topic from the given topic list */ removeTopicFromList(topic, listProp) { /* if the list exists, remove the topic from it */ if (self.(listProp) != nil) self.(listProp).removeElement(topic); } /* * Our list of suggested topics. These are SuggestedTopic objects * that describe things that another actor wants to ask or tell this * actor about. */ suggestedTopics = nil /* * Get the "owner" of the topics in this database. The meaning of * "owner" varies according to the topic database type; for actor * topic databases, for example, this is the actor. Generally, the * owner is the object being queried about the topic, from the * player's perspective. Each type of database should define this * method to return the appropriate object. */ getTopicOwner() { return nil; } ; /* * A TopicDatabase for an Actor. This is used not only directly for an * Actor but also for an actor's sub-databases, in ActorState and * ConvNode. * * Actor topic databases field queries for the various types of * topic-based interactions an actor can participate in: ASK, TELL, SHOW, * GIVE, and so on. * * Each actor has its own topic database, which means each actor can have * its own set of responses. Actor states can also have their own * separate topic databases; this makes it easy to make an actor's * response to a particular question vary according to the actor's state. * Conversation nodes can also have their own separate databases, which * allows for things like threaded conversations. */ class ActorTopicDatabase: TopicDatabase /* * Initiate conversation on the given simulation object. If we can * find an InitiateTopic matching the given object, we'll show its * topic response and return true; if we can't find a topic to * initiate, we'll simply return nil. */ initiateTopic(obj) { /* find an initiate topic for the given object */ if (handleTopic(gPlayerChar, obj, initiateConvType, nil)) { /* * we handled the topic, so note that we're in conversation * with the player character now */ getTopicOwner().noteConversation(gPlayerChar); /* indicate that we found a topic to initiate */ return true; } /* we didn't find a topic to initiate */ return nil; } /* show a topic response */ showTopicResponse(fromActor, topic, resp) { local actor = getTopicOwner(); local newNode; /* * note whether the response is conversational - we need to do * this ahead of time, since invoking the response can sometimes * have the side effect of changing the response's status */ local isConv = resp.isConversational; /* tell the conversation manager we're starting a response */ conversationManager.beginResponse(actor); /* let the response object handle it */ resp.handleTopic(fromActor, topic); /* * By default, after showing a response, we want to leave the * conversation node tree entirely if we didn't explicitly set * the next node in the course of the response. So, set the * default new node to 'nil'. However, if the topic is * non-conversational, it shouldn't affect the conversation * thread at all, so leave the current node unchanged. */ if (isConv) newNode = nil; else newNode = actor.curConvNode; /* tell the conversation manager we're done with the response */ conversationManager.finishResponse(actor, newNode); } /* * Our 'ask about', 'ask for', 'tell about', 'give', 'show', * miscellaneous, command, and self-initiated topic databases - these * are vectors we initialize as needed. Since every actor and every * actor state has its own separate topic database, it's likely that * the bulk of these databases will be empty, so we don't bother even * creating a vector for a topic list until the first topic is added. * This means we have to be able to cope with these being nil * anywhere we use them. */ askTopics = nil askForTopics = nil tellTopics = nil showTopics = nil giveTopics = nil miscTopics = nil commandTopics = nil initiateTopics = nil /* our special command database */ specialTopics = nil ; /* ------------------------------------------------------------------------ */ /* * A "suggested" topic. These provide suggestions for things the player * might want to ASK or TELL another actor about. At certain times * (specifically, when starting a conversation with HELLO or TALK TO, or * when the player enters a TOPICS command to explicitly ask for a list * of topic suggestions), we'll look for these objects in the actor or * actor state for the actor to whom we're talking. We'll show a list * of each currently active suggestion we find. This gives the player * some guidance of what to talk about. For example: * * >talk to bob *. "Excuse me," you say. * * Bob looks up from his newspaper. "Yes? Oh, you again." * * (You'd like to ask him about the black book, the candle, and the * bell, and tell him about the crypt.) * * Topic suggestions are entirely optional. Some authors don't like the * idea, since they think it's too much like a menu system, and just * gives away the solution to the game. If you don't want to have * anything to do with topic suggestions, we won't force you - simply * don't define any SuggestedTopic objects, and the library will never * offer suggestions and will even disable the TOPICS command. * * If you do want to use topic suggestions, the easiest way to use this * class is to combine it using multiple inheritance with a TopicEntry * object. You just have to add SuggestedTopic to the superclass list * for your topic entry object, and give the suggested topic a name * string (using a property and format defined by the language-specific * library) to display in suggestions lists. Doing this, the suggestion * will automatically be enabled whenever the topic entry is available, * and will automatically be removed from the suggestions when the topic * is invoked in conversation (in other words, we'll only suggest asking * about the topic until it's been asked about once). * * Topic suggestions can be associated with an actor or an actor state; * these are topics that a given character would like to talk to the * associated actor about. The association is a bit tricky: suggested * topic objects are stored with the actor being *talked to*. For * example, if we want to suggest topics that the player character might * want to ASK BILL ABOUT, we store these suggestions with *Bill*. We * do NOT store the suggestions with the player character. This might * seem backwards at first glance, since fundamentally the suggestions * belong in the player character's "brain" - they are, after all, * things the player character wants to talk about. In practice, * though, there are two things that make it easier to keep the * information with the character being asked. First, in most games, * there's just one player character, so one of the two actors in each * association will always be the player character; by storing the * objects with the NPC, we can just let the PC be assumed as the other * actor as a default, saving us some typing that would be necessary if * we had to specify each object in the other direction. Second, we * keep the *response* objects associated with the character being asked * - that association is intuitive, at least. The thing is, we can * usually combine the suggestion and response into a single object, * saving another bunch of typing; if we didn't keep the suggestion with * the character being asked, we couldn't combine the suggestions and * responses this way, since they'd have to be associated with different * actors. */ class SuggestedTopic: object /* * The name of the suggestion. The rules for setting this vary by * language; in the English version, we'll display the fullName when * we show a stand-alone item, and the groupName when we appear in a * list group (such as a group of ASK ABOUT or TELL ABOUT * suggestions). * * In English, the fullName should be suitable for use after * 'could': "You could <fullName>, <fullName>, or <fullName>". * * In English, the phrasing where the 'name' property is used * depends on the specific subclass, but it should usually be a * qualified noun phrase (that is, it should include a qualifier * such as "a" or "the" or a possessive). For ASK and TELL, for * example, the 'name' should be suitable for use after ABOUT: "You * could ask him about <the lighthouse>, <Bob's black book>, or <the * weather>." * * By default, we'll walk up our 'location' tree looking for another * suggested topic; if we find one, we'll use its corresponding name * values. */ fullName = (fromEnclosingSuggestedTopic(&fullName, '')) name = (fromEnclosingSuggestedTopic(&name, '')) /* * Our associated topic. In most cases, this will be initialized * automatically: if this suggested topic object is also a * TopicEntry object (using multiple inheritance), we'll set this * during start-up to 'self', or if our location is a TopicEntry, * we'll set this to our location. This only needs to be * initialized manually if neither of those conditions is true. */ associatedTopic = nil /* * Set the location to the actor to ask or tell about this topic. * This is the target of the ASK ABOUT or TELL ABOUT command, NOT * the actor who's doing the asking. This can also be set to a * TopicEntry object, in which case we'll be associated with the * actor with which the topic entry is associated, and we'll also * automatically tie the topic entry to this suggestion. * * Because we're using the location property, you can use the '+' * notation to add a suggested topic to the target actor, state * objects, or topic entry. */ location = nil /* * The actor who *wants* to ask or tell about this topic. Our * location property gives the actor to be asked or told, because * we're associated with the target actor - the same actor who has * the TopicEntry information for the topic. This property, in * contrast, gives the actor who's doing the asking. * * By default, we return the player character; in most cases, you * won't have to override this. In most games, only the player * character uses the suggested topic mechanism, because there's no * reason to suggest topics for NPC's - they're just automata, after * all, so if we want them to ask something, we can just program * them to ask it directly. Also, most games have only one player * character. Games that meet these criteria won't ever have to * override this. If you do have multiple player characters, you'll * probably want to override this for each suggested topic to * indicate which character wants to ask about the topic, as the * different player characters might have different things they'd * want to talk about. */ suggestTo = (gPlayerChar) /* the ListGroup with which we're to list this suggestion */ suggestionGroup = [] /* find the nearest enclosing SuggestedTopic parent */ findEnclosingSuggestedTopic() { /* walk up our location list */ for (local loc = location ; loc != nil ; loc = loc.location) { /* if this is a suggested topic, it's what we're looking for */ if (loc.ofKind(SuggestedTopic)) return loc; } /* didn't find anything */ return nil; } /* find the outermost enclosing SuggestedTopic parent */ findOuterSuggestedTopic() { local outer; /* walk up our location list */ for (local loc = self, outer = nil ; loc != nil ; loc = loc.location) { /* if this is a suggested topic, it's the outermost so far */ if (loc.ofKind(SuggestedTopic)) outer = loc; } /* return the outermost suggested topic we found */ return outer; } /* * get a property from the nearest enclosing SuggestedTopic, or * return the given default value if there is no enclosing * SuggestedTopic */ fromEnclosingSuggestedTopic(prop, defaultVal) { /* look for the nearest enclosing suggested topic */ local enc = findEnclosingSuggestedTopic(); /* * return the desired property from the enclosing suggested * topic object if we found one, or the default if there is no * enclosing object */ return (enc != nil ? enc.(prop) : defaultVal); } /* * Should we suggest this topic to the given actor? We'll return * true if the actor is the same actor for which this suggestion is * intended, and the associated topic entry is currently active, and * we haven't already satisfied our curiosity about the topic. */ isSuggestionActive(actor, scopeList) { /* * Check to see if this is our target actor; that the associated * topic itself is active; that our curiosity hasn't already been * satisfied; and that it's at least possible to match the * associated topic right now. If all of these conditions are * met, we can make this suggestion. */ return (actor == suggestTo && associatedTopicIsActive() && associatedTopicCanMatch(actor, scopeList) && !curiositySatisfied); } /* * The number of times to suggest asking about our topic. When * we've asked about our associated topic this many times, we'll * have satisfied our curiosity. In most cases, we'll only want to * suggest a topic until it's asked about once, since most topics * only have a single meaningful response, so we'll use 1 as the * default. This should be overridden in cases where a topic will * reveal more information when asked several times. If this is * nil, it means that there's no limit to the number of times to * suggest asking about this. */ timesToSuggest = 1 /* * Have we satisfied our curiosity about this topic? Returns true * if so, nil if not. We'll never suggest a topic when this returns * true, because this means that the player no longer feels the need * to ask about the topic. */ curiositySatisfied = (timesToSuggest != nil && associatedTopicTalkCount() >= timesToSuggest) /* initialize - this is called automatically during pre-initialization */ initializeSuggestedTopic() { /* if we have a location, link up with our location */ if (location != nil) location.addSuggestedTopic(self); /* * if we're also a TopicEntry (using multiple inheritance), then * we are our own associated topic object */ if (ofKind(TopicEntry)) associatedTopic = self; } /* * Methods that rely on the associated topic. We isolate these in a * few methods here so that the rest of class doesn't depend on the * exact nature of our topic association. In particular, this allows * for subclasses that don't have an associated topic at all, or that * have multiple associated topics. Subclasses with specialized * topic relationships can simply override these methods to define * these methods appropriately. */ /* is the associated topic active? */ associatedTopicIsActive() { return associatedTopic.checkIsActive(); } /* get the number of previous invocations of the associated topic */ associatedTopicTalkCount() { return associatedTopic.talkCount; } /* is it possible to match the associated topic? */ associatedTopicCanMatch(actor, scopeList) { return associatedTopic.isMatchPossible(actor, scopeList); } /* * Note that we're being shown in a topic inventory listing. By * default, we don't do anything here, but subclasses can use this to * do any extra work they want to do on being listed. */ noteSuggestion() { } ; /* * A suggested topic that applies to an entire AltTopic group. * * Normally, a suggestion is tied to an individual TopicEntry. This * means that when a topic has several AltTopic alternatives, each * AltTopic can be its own separate, independent suggestion. A * particular alternative can be a suggestion or not, independently of * the other alternatives for the same TopicEntry. Since each AltTopic * is a separate suggestion, asking about one of the alternatives won't * have any effect on the "curiosity" about the other alternatives - in * other words, the other alternatives will be separately suggested when * they become active. * * In many cases, it's better for an entire set of alternatives to be * treated as a single suggested topic. That is, we want to suggest the * topic when ANY of the alternatives is active, and asking about any one * of the alternatives will satisfy the PC's curiosity for ALL of the * alternatives. This sort of arrangement is usually better for cases * where the conditions that trigger the different alternatives aren't * things that ought to make the PC think to ask the same question again. * * Use this class by associating it with the *root* TopicEntry of the * group of alternatives. You can do this most simply by mixing this * class into the superclass list of the root TopicEntry: * *. + AskTellTopic, SuggestedTopicTree, SuggestedAskTopic *. // ... *. ; * ++ AltTopic ... ; * ++ AltTopic ... ; * * This makes the entire group of AltTopics part of the same suggestion. * Note that you must *also* include SuggestedAsk, SuggestedTellTopic, or * one of the other specialized types among the superclass, to indicate * which kind of suggestion this is. */ class SuggestedTopicTree: SuggestedTopic /* is the associated topic active? */ associatedTopicIsActive() { /* the topic is active if anything in the AltTopic group is active */ return associatedTopic.anyAltIsActive; } /* get the number of previous invocations of the associated topic */ associatedTopicTalkCount() { /* return the number of invocations of any alternative */ return associatedTopic.altTalkCount; } ; /* * A suggested ASK ABOUT topic. We'll list ASK ABOUT topics together in * a subgroup ("you'd like to ask him about the book, the candle, and * the bell..."). */ class SuggestedAskTopic: SuggestedTopic suggestionGroup = [suggestionAskGroup] ; /* * A suggested TELL ABOUT topic. We'll list TELL ABOUT topics together * in a subgroup. */ class SuggestedTellTopic: SuggestedTopic suggestionGroup = [suggestionTellGroup] ; /* * A suggested ASK FOR topic. We'll list ASK FOR topics together as a * group. */ class SuggestedAskForTopic: SuggestedTopic suggestionGroup = [suggestionAskForGroup] ; /* * A suggested GIVE TO topic. */ class SuggestedGiveTopic: SuggestedTopic suggestionGroup = [suggestionGiveGroup] ; /* * A suggested SHOW TO topic. */ class SuggestedShowTopic: SuggestedTopic suggestionGroup = [suggestionShowGroup] ; /* * A suggested YES/NO topic */ class SuggestedYesTopic: SuggestedTopic suggestionGroup = [suggestionYesNoGroup] ; class SuggestedNoTopic: SuggestedTopic suggestionGroup = [suggestionYesNoGroup] ; /* ------------------------------------------------------------------------ */ /* * A conversation node. Conversation nodes are supplemental topic * databases that represent a point in time in a conversation - a * particular context that arises from what came immediately before in * the conversation. A conversation node is used to set up a group of * special responses that make sense only in a momentary context within a * conversation. * * A ConvNode object must be nested (via the 'location' property) within * an actor or an ActorState. This is how we associate the ConvNode with * its actor. Note that putting a ConvNode inside an ActorState doesn't * do anything different from putting the node directly inside the * ActorState's actor - we allow it only for convenience, to allow * greater flexibility arranging source code. */ class ConvNode: ActorTopicDatabase /* * Every ConvNode must have a name property. This is a string * identifying the object. Use this name string instead of a regular * object name (so ConvNode instances can essentially always be * anonymous, as far as the compiler is concerned). This string is * used to find the ConvNode in the master ConvNode database * maintained in the conversationManager object. * * A ConvNode name should be unique with respect to all other * ConvNode objects - no two ConvNode objects should have the same * name string. Other than this, the name strings are arbitrary. * (However, they shouldn't contain any '>' characters, because this * would prevent them from being used in <.convnode> tags, which is * the main place ConvNode's are usually used.) */ name = '' /* * Is this node "sticky"? If so, we'll stick to this node if we * show a response that doesn't set a new node. By default, we're * not sticky, so if we show a response that doesn't set a new node * and doesn't use a <.convstay> tag, we'll simply forget the node * and set the actor to no current ConvNode. * * Sticky nodes are useful when you want the actor to stay * on-subject even when the player digresses to talk about other * things. This is useful when the actor has a particular thread * they want to drive the conversation along. */ isSticky = nil /* * Show our NPC-initiated greeting. This is invoked when our actor's * initiateConversation() method is called to cause our actor to * initiate a conversation with the player character. This method * should show what our actor says to initiate the conversation. By * default, we'll invoke our npcGreetingList's script, if the * property is non-nil. * * A greeting should always be defined for any ConvNode that's used * in an initiateConversation() call. * * To define a greeting when defining a ConvNode, you can override * this method with a simple double-quoted string message, or you can * define an npcGreetingList property as an EventList of some kind. */ npcGreetingMsg() { /* if we have an npcGreetingList property, invoke the script */ if (npcGreetingList != nil) npcGreetingList.doScript(); } /* an optional EventList containing our NPC-initiated greetings */ npcGreetingList = nil /* * Our NPC-initiated conversation continuation message. This is * invoked on each turn (during the NPC's takeTurn() daemon * processing) that we're in this conversation node and the player * character doesn't do anything conversational. This allows the NPC * to carry on the conversation of its own volition. Define this as * a double-quoted string if you want the NPC to say something to * continue the conversation. */ npcContinueMsg = nil /* * An optional EventList containing NPC-initiated continuation * messages. You can define an EventList here instead of defining * npcContinueMsg, if you want more than one continuation message. */ npcContinueList = nil /* * Flag: automatically show a topic inventory on activating this * conversation node. Some conversation nodes have sufficiently * obscure entries that it's desirable to show a topic inventory * automatically when the node becomes active. * * By default, we automatically show a topic inventory if the node * contains an active SpecialTopic entry. Since special topics are * inherently obscure, in that they use non-standard commands, we * always want to show topics when one of these becomes active. */ autoShowTopics() { /* if we have an active special topic, show the topic inventory */ return (specialTopics != nil && specialTopics.indexWhich({x: x.checkIsActive()}) != nil); } /* our NPC is initiating a conversation starting with this node */ npcInitiateConversation() { local actor = getActor(); /* tell the conversation manager we're the actor who's talking */ conversationManager.beginResponse(actor); /* note that we're in conversation with the player character now */ getActor().noteConversation(gPlayerChar); /* show our NPC greeting */ npcGreetingMsg(); /* look for an ActorHelloTopic within the node */ handleTopic(gPlayerChar, actorHelloTopicObj, helloConvType, nil); /* end the response, staying in the current ConvNode by default */ conversationManager.finishResponse(actor, self); } /* * Continue the conversation of the NPC's own volition. Returns * true if we displayed anything, nil if not. */ npcContinueConversation() { local actor = getActor(); local disp; /* tell the conversation manager we're starting a response */ conversationManager.beginResponse(actor); /* show our text, watching to see if we generate any output */ disp = outputManager.curOutputStream.watchForOutput(function() { /* * if we have a continuation list, invoke it; otherwise if we * have a continuation message, show it; otherwise, just * return nil to let the caller know we have nothing to add */ if (npcContinueList != nil) npcContinueList.doScript(); else npcContinueMsg; }); /* end the response, staying in the current ConvNode by default */ conversationManager.finishResponse(actor, self); /* * if we actually said anything, note that we're in conversation * with the player character */ if (disp) getActor().noteConversation(gPlayerChar); /* return the display indication */ return disp; } /* our actor is our location, or our location's actor */ getActor() { /* if our location is an actor state, return the state's actor */ if (location.ofKind(ActorState)) return location.getActor(); /* otherwise, our location must be our actor */ return location; } /* our actor is the "owner" of our topics */ getTopicOwner() { return getActor(); } /* * Handle a conversation topic. The actor state object will call * this to give the ConvNode the first crack at handling a * conversation command. We'll return true if we handle the command, * nil if not. Our default handling is to look up the topic in the * given database list property, and handle it through the TopicEntry * we find there, if any. */ handleConversation(otherActor, topic, convType, path) { /* try handling it, returning the handled/not-handled result */ return handleTopic(otherActor, topic, convType, path); } /* * Can we end the conversation? If so, return true; our caller will * invoke our endConversation() to let us know that the conversation * is over. * * To prevent the conversation from ending, simply return nil. * * In most cases, you won't want to force the conversation to keep * going without any comment. Instead, you'll want to display some * message to let the player know what's going on - something like * "Hey! We're not through here!" If you do display a message, then * rather than returning nil, return the special value blockEndConv - * this tells the caller that the actor said something, so the caller * will call noteConvAction() to prevent further generated * conversation output on this same turn. * * 'reason' gives the reason the conversation is ending, as an * endConvXxx enum code. */ canEndConversation(actor, reason) { return true; } /* * Receive notification that our actor is ending a stateful * conversation. This is called before the normal * InConversationState disengagement operations. 'reason' is one of * the endConvXxx enums, indicating why the conversation is ending. * * Instances can override this for special behavior on terminating a * conversation. For example, an actor who just asked a question * could say something to indicate that the other actor is being * rude. By default, we do nothing. * * Note that there's no way to block the ending of the conversation * here. If you want to prevent the conversation from ending, use * canEndConversation() instead. */ endConversation(actor, reason) { } /* * Process a special command. Check the given command line string * against all of our topics, and see if we have a match to any topic * that takes a special command syntax. If we find a matching * special topic, we'll note the match, and turn the command into our * secret internal pseudo-command "XSPCLTOPIC". That command will * then go through the parser, which will recognize it and process it * using the normal conversational mechanisms, which will find the * SpecialTopic we noted earlier (in this method) and display its * response. * * 'str' is the original input string, exactly as entered by the * player, and 'procStr' is the "processed" version of the input * string. The nature of the processing varies by language, but * generally this involves things like removing punctuation marks and * any "noise words" that don't usually change the meaning of the * input, at least for the purposes of matching a special topic. */ processSpecialCmd(str, procStr) { local match; local cnt; /* we don't have an active special topic yet */ activeSpecialTopic = nil; /* * if we have no special topics, there's definitely no special * processing we need to do */ if (specialTopics == nil) return str; /* scan our special topics for a match */ cnt = 0; foreach (local cur in specialTopics) { /* if this one is active, and it matches the string, note it */ if (cur.checkIsActive() && cur.matchPreParse(str, procStr)) { /* remember it as the last match */ match = cur; /* count the match */ ++cnt; } } /* * If we found exactly one match, then activate it. If we found * zero or more than one, ignore any special topics and proceed * on the assumption that this is a normal command. (We ignore * ambiguous matches because this probably means that the entire * command is some very common word that happens to be acceptable * as a keyword in one or more of our matches. In these cases, * the common word was probably meant as an ordinary command, * since the player would likely have been more specific if a * special topic were really desired.) */ if (cnt == 1) { /* * remember the active SpecialTopic - we'll use this memory * to find it again when we get through the full command * processing */ activeSpecialTopic = match; /* * Change the command to our special internal pseudo-command * that triggers the active special topic. Include the * original string as a literal phrase, enclosed in double * quotes and specially coded to ensure that the tokenizer * doesn't become confused by any embedded quotes. */ return 'xspcltopic "' + SpecialTopicAction.encodeOrig(str) + '"'; } else { /* proceed, treating the original input as an ordinary command */ return str; } } patWhitespace = static new RexPattern('<space>+') patDelim = static new RexPattern('<punct|space>') /* * Handle an XSPCLTOPIC command from the given actor. This is part * two of the two-phase processing of SpecialTopic matches. Our * pre-parser checks each SpecialTopic's custom syntax for a match * to the player's text input, and if it finds a match, it sets our * activeSpecialTopic property to the matching SpecialTopic, and * changes the user's command to XSPCLTOPIC for processing by the * regular parser. The regular parser sees the XSPCLTOPIC command, * which is a valid verb that calls the issuing actor's * saySpecialTopic() routine, which in turn forwards the request to * the issuing actor's interlocutor's current conversation node - * which is to say, 'self'. We complete the two-step procedure by * going back to the active special topic object that we previously * noted and showing its response. */ saySpecialTopic(fromActor) { /* make sure we have an active special topic object */ if (activeSpecialTopic != nil) { local actor = getTopicOwner(); /* tell the conversation manager we're starting a response */ conversationManager.beginResponse(actor); /* let the SpecialTopic handle the response */ activeSpecialTopic.handleTopic(fromActor, nil); /* * Tell the conversation manager we're done. By default, we * want to leave the conversation tree entirely, so set the * new default node to 'nil'. */ conversationManager.finishResponse(actor, nil); /* that wraps things up for the active special topic */ activeSpecialTopic = nil; } else { /* * There is no active special topic, so the player must have * typed in the XSPCLTOPIC command explicitly - if we got * here through the normal two-step procedure then this * property would not be nil. Politely decline the command, * since it's not for the player's direct use. */ gLibMessages.commandNotPresent; } } /* * The active special topic. This is the SpecialTopic object that * we matched during pre-parsing, so it's the one whose response we * wish to show while processing the command we pre-parsed. */ activeSpecialTopic = nil /* * Note that we're becoming active, with a reason code. Our actor * will call this method when we're becoming active, as long as we * weren't already active. * * 'reason' is a string giving a reason code for why we're being * called. For calls from the library, this will be one of these * codes: * * 'convnode' - processing a <.convnode> tag * * 'convend' - processing a <.convend> tag * * 'initiateConversation' - a call to Actor.initiateConversation() * * 'endConversation' - a call to Actor.endConversation() * * The reason code is provided so that the node can adapt its action * for different trigger conditions, if desired. By default, we * ignore the reason code and just call the basic noteActive() * method. */ noteActiveReason(reason) { noteActive(); } /* * Note that we're becoming active, with a reason code. Our actor * will call this method when we're becoming active, as long as we * weren't already active. * * Note that if you want to adapt the method's behavior according to * why the node was activated, you can override noteActiveReason() * instead of this method. */ noteActive() { /* if desired, schedule a topic inventory whenever we're activated */ if (autoShowTopics()) conversationManager.scheduleTopicInventory(); } /* * Note that we're leaving this conversation node. This doesn't do * anything by default, but individual instances might find the * notification useful for triggering side effects. */ noteLeaving() { } ; /* ------------------------------------------------------------------------ */ /* * Pre-parser for special ConvNode-specific commands. When the player * character is talking to another character, and the NPC's current * ConvNode includes topics with their own commands, we'll check the * player's input to see if it matches any of these topics. */ specialTopicPreParser: StringPreParser doParsing(str, which) { local actor; local node; /* * don't handle this on requests for missing literals - these * responses are always interpreted as literal text, so there's * no way this could be a special ConvNode command */ if (which == rmcAskLiteral) return str; /* * if the player character isn't currently in conversation, or * the actor with whom the player character is conversing doesn't * have a current conversation node, there's nothing to do */ if ((actor = gPlayerChar.getCurrentInterlocutor()) == nil || (node = actor.curConvNode) == nil) return str; /* ask the conversation node to process the string */ return node.processSpecialCmd(str, processInputStr(str)); } /* * Process the input string, as desired, for special-topic parsing. * This method is for the language module's use; by default, we do * nothing. * * Language modules should override this to remove punctuation marks * and to do any other language-dependent processing to make the * string parsable. */ processInputStr(str) { return str; } ; /* ------------------------------------------------------------------------ */ /* * A conversational action type descriptor. This descriptor is used in * handleConversation() in Actor and ActorState to describe the type of * conversational action we're performing. The type descriptor object * encapsulates a set of information that tells us how to handle the * action. */ class ConvType: object /* * The unknown interlocutor message property. This is used when we * try this conversational action without knowing whom we're talking * to. For example, if we just say HELLO, and there's no one around * to talk to, we'll use this as the default response. This can be a * library message property, or simply a single-quoted string to * display. */ unknownMsg = nil /* * The TopicDatabase topic-list property. This is the property of * the TopicDatabase object that we evaluate to get this list of * topic entries to search for a match to the topic. */ topicListProp = nil /* the default response property for this action */ defaultResponseProp = nil /* * Call the default response property on the given topic database. * This invokes the property given by defaultResponseProp(). We have * both the property and the method to call the property because this * allows us to test for the existence of the property and to call it * with the appropriate argument list. */ defaultResponse(db, otherActor, topic) { } /* * Perform any special follow-up action for this type of * conversational action. */ afterResponse(actor, otherActor) { } ; helloConvType: ConvType unknownMsg = &sayHelloMsg topicListProp = &miscTopics defaultResponseProp = &defaultGreetingResponse defaultResponse(db, other, topic) { db.defaultGreetingResponse(other); } /* after an explicit HELLO, show any suggested topics */ afterResponse(actor, otherActor) { /* show or schedule a topic inventory, as appropriate */ conversationManager.showOrScheduleTopicInventory(actor, otherActor); } ; byeConvType: ConvType unknownMsg = &sayGoodbyeMsg topicListProp = &miscTopics defaultResponseProp = &defaultGoodbyeResponse defaultResponse(db, other, topic) { db.defaultGoodbyeResponse(other); } ; yesConvType: ConvType unknownMsg = &sayYesMsg topicListProp = &miscTopics defaultResponseProp = &defaultYesResponse defaultResponse(db, other, topic) { db.defaultYesResponse(other); } ; noConvType: ConvType unknownMsg = &sayNoMsg topicListProp = &miscTopics defaultResponseProp = &defaultNoResponse defaultResponse(db, other, topic) { db.defaultNoResponse(other); } ; askAboutConvType: ConvType topicListProp = &askTopics defaultResponseProp = &defaultAskResponse defaultResponse(db, other, topic) { db.defaultAskResponse(other, topic); } ; askForConvType: ConvType topicListProp = &askForTopics defaultResponseProp = &defaultAskForResponse defaultResponse(db, other, topic) { db.defaultAskForResponse(other, topic); } ; tellAboutConvType: ConvType topicListProp = &tellTopics defaultResponseProp = &defaultTellResponse defaultResponse(db, other, topic) { db.defaultTellResponse(other, topic); } ; giveConvType: ConvType topicListProp = &giveTopics defaultResponseProp = &defaultGiveResponse defaultResponse(db, other, topic) { db.defaultGiveResponse(other, topic); } ; showConvType: ConvType topicListProp = &showTopics defaultResponseProp = &defaultShowResponse defaultResponse(db, other, topic) { db.defaultShowResponse(other, topic); } ; commandConvType: ConvType topicListProp = &commandTopics defaultResponseProp = &defaultCommandResponse defaultResponse(db, other, topic) { db.defaultCommandResponse(other, topic); } ; /* * This type is for NPC-initiated conversations. It's not a normal * conversational action, since it doesn't involve handling a player * command, but is usually instead triggered by an agenda item, * takeTurn(), or other background activity. */ initiateConvType: ConvType topicListProp = &initiateTopics ; /* * CONSULT ABOUT isn't a true conversational action, since it's applied * to inanimate objects (such as books); but it's handled through the * conversation system, so it needs a conversation type object */ consultConvType: ConvType topicListProp = &consultTopics ; /* ------------------------------------------------------------------------ */ /* * A topic database entry. Actors and actor state objects store topic * databases; a topic database is essentially a set of these entries. * * A TopicEntry can go directly inside an Actor, in which case it's part * of the actor's global set of topics; or, it can go inside an * ActorState, in which case it's part of the state's database and is * only active when the state is active; or, it can go inside a * TopicGroup, which is a set of topics with a common controlling * condition; or, it can go inside a ConvNode, in which case it's in * effect only when the conversation node is active. * * Each entry is a relationship between a topic, which is something that * can come up in an ASK or TELL action, and a handling for the topic. * In addition, each entry determines what kind or kinds of actions it * responds to. * * Note that TopicEntry objects are *not* simulation objects. Rather, * these are abstract objects; they can be associated with simulation * objects via the matching mechanism, but these are separate from the * actual simulation objects. The reason for this separation is that a * given simulation object might have many different response - the * response could vary according to who's being asked the question, who's * asking, and what else is happening in the game. * * An entry decides for itself if it matches a topic. By default, an * entry can match based on either a simulation object, which we'll match * to anything in the topic's "in scope" or "likely" match lists, or * based on a regular expression string, which we'll match to the actual * topic text entered in the player's command. * * An entry can decide how strongly it matches a topic. The database * will choose the strongest match when multiple entries match the same * topic. The strength of the match is given by a numeric score; the * higher the score, the stronger the match. The match strength makes it * easy to specify a hierarchy of topics from specific to general, so * that we provide general responses to general topic areas, but can * still respond to particular topics areas more specifically. For * example, we might want to provide a specific match to the FROBNOZ * SPELL object, talking about that particular magic spell, but provide a * generic '.* spell' pattern to response to questions about any old * spell. We'd give the generic pattern a lower score, so that the * specific FROBNOZ SPELL response would win when it matches, but we'd * fall back on the generic pattern in other cases. */ class TopicEntry: object /* * My matching simulation object or objects. This can be either a * single object or a list of objects. */ matchObj = nil /* * Is this topic active? This can be used to control how an actor * can respond without have to worry about adding and removing topics * manually at key events, or storing the topics in state objects. * Sometimes, it's easier to just put a topic entry in the actor's * database from the start, and test some condition dynamically when * the topic is actually queried. To do this, override this method * to test the condition that determines when the topic entry should * become active. We'll never show the topic's response when * isActive returns nil. By default, we simply return true to * indicate that the topic entry is active. */ isActive = true /* * Flag: we are a "conversational" topic. This is true by default. * When this is set to nil, a ConversationReadyState will NOT show * its greeting and will not enter its InConversationState to show * this topic entry's response. * * This should be set to nil when the topic entry's response is * non-conversational, in which case a greeting would be * undesirable. This is appropriate for responses like "You don't * think he'd want to talk about that", where the response indicates * that the player character didn't even ask a question (or * whatever). */ isConversational = true /* * Do we imply a greeting? By default, all conversational topics * imply a greeting. We separate this out so that the implied * greeting can be controlled independently of whether or not we're * actually conversational, if desired. */ impliesGreeting = (isConversational) /* * Get the actor associated with the topic, if any. By default, * we'll return our enclosing database's topic owner, if it's an * actor - in almost all cases, if there's any actor associated with * a topic, it's simply the owner of the database containing the * topic. */ getActor() { local owner; /* * if we have an owner, and it's an actor, then it's our * associated actor; otherwise, we don't have any associated * actor */ if ((owner = location.getTopicOwner()) != nil && owner.ofKind(Actor)) return owner; else return nil; } /* * Determine if this topic is active. This checks the isActive * property, and also takes into account our relationship to * alternative entries for the topic. Generally, you should *define* * (override) isActive, and *call* this method. */ checkIsActive() { /* * if our isActive property indicates we're not active, we're * definitely not active, so there's no need to check for an * overriding alternative */ if (!isActive) return nil; /* if we have an active nested alternative, it overrides us */ if (altTopicList.indexWhich({x: x.isActive}) != nil) return nil; /* ask our container if its topics are active */ return location.topicGroupActive(); } /* * Check to see if any alternative in the alternative group is * active. This returns true if we're active or if any of our nested * AltTopics is active. */ anyAltIsActive() { /* * if all topics within our container are inactive, then there's * definitely no active alternative */ if (!location.topicGroupActive()) return nil; /* * if we're active, or any of our nested AltTopics is active, our * alternative group is active */ if (isActive || altTopicList.indexWhich({x: x.isActive}) != nil) return true; /* we didn't find any active alternatives in the entire group */ return nil; } /* * Adjust my score value for any hierarchical adjustments. We'll add * the score adjustment for each enclosing object. */ adjustScore(score) { /* the score is nil, it means there's no match, so don't adjust it */ if (score == nil) return score; /* add in the cumulative adjustment from my containers */ return score + location.topicGroupScoreAdjustment; } /* * Check to see if we want to defer to the given topic from an * inferior topic database. By default, we never defer to a topic * from an inferior database: we choose a matching topic from the top * database in the hierarchy where we find a match. * * The database hierarchy, for most purposes, starts with the * ConvNode at the highest level, then the ActorState, then the * Actor. We search those databases, in that order, and we take the * first match we find. By default, if there's another match in a * lower-level database, it doesn't matter what its matchScore is: we * always pick the one from the highest-level database where we find * a match. You can override this method to change this behavior. * * We don't actually define this method here, because the presence of * the method is significant. If the method isn't defined at all, we * won't bother looking for a possible deferral, saving the trouble * of searching the other databases in the hierarchy. */ // deferToEntry(other) { return nil; } /* * Our match strength score. By default, we'll use a score of 100, * which is just an arbitrary base score. */ matchScore = 100 /* * The set of database lists we're part of. This is a list of * property pointers, giving the TopicDatabase properties of the * lists we participate in. */ includeInList = [] /* * Our response. This is displayed when we're the topic entry * selected to handle an ASK or TELL. Each topic entry must override * this to show our response text (or, alternatively, an entry can * override handleTopic so that it doesn't call this property). */ topicResponse = "" /* * The number of times this topic has invoked by the player. Each * time the player asks/tells/etc about this topic, we'll increment * this count. */ talkCount = 0 /* * The number of times this topic or any nested AltTopic has been * invoked by the player. Each time the player asks/tells/etc about * this topic OR any of its AltTopic children, we'll increment this * count. */ altTalkCount = 0 /* * the owner of any AltTopic nested within me is the same as my own * topic owner, which we take from our location */ getTopicOwner() { if (location != nil) return location.getTopicOwner(); else return nil; } /* * Initialize. If we have a location property, we'll assume that the * location is a topic database object, and we'll add ourselves to * that database. */ initializeTopicEntry() { /* if we have a location, add ourselves to its topic database */ if (location != nil) location.addTopic(self); /* sort our list of AltTopic children */ altTopicList = altTopicList.sort( SortAsc, {a, b: a.altTopicOrder - b.altTopicOrder}); } /* add a topic nested within us */ addTopic(entry) { /* if we have a location, add the entry to its topic database */ if (location != nil) location.addTopic(entry); } /* * Add an AltTopic entry. This is called by our AltTopic children * during initialization; we'll simply add the entry to our list of * AltTopic children. */ addAltTopic(entry) { /* add the entry to our list of alternatives */ altTopicList += entry; } /* get the topic group score adjustment (for AltTopics nested within) */ topicGroupScoreAdjustment = (location.topicGroupScoreAdjustment) /* check the group isActive status (for AltTopics nested within) */ topicGroupActive = (location.topicGroupActive) /* our list of AltTopic children */ altTopicList = [] /* * Match a topic. This is abstract in this base class; it must be * defined by each concrete subclass. This returns nil if there's no * match, or an integer value if there's a match. The higher the * number's value, the stronger the match. * * This is abstract in the base class because the meaning of 'topic' * varies by subclass, according to which type of command it's used * with. For example, in ASK and TELL commands, 'topic' is a * ResolvedTopic describing the topic in the player's command; for * GIVE and SHOW commands, it's the resolved simulation object. */ // matchTopic(fromActor, topic) { return nil; } /* * Check to see if a match to this topic entry is *possible* right * now for the given actor. For most subclasses, this is inherently * imprecise, because the 'match' function simply isn't reversible in * general: to know if we can be matched, we'd have to determine if * there's a non-empty set of possible inputs that can match us. * This method is complementary to matchTopic(), so subclasses must * override with a corresponding implementation. * * 'actor' is the actor to whom we're making the suggestion. * 'scopeList' is the list of objects that are in scope for the * actor. * * The library only uses this to determine if a suggestion should be * offered. So, specialized topic instances with non-standard match * rules don't have to worry about this unless they're used as * suggestions, or unless the game itself needs this information for * some other reason. */ // isMatchPossible(actor, scopeList) { return true; } /* * Break a tie among matching topics entries. The topic database * searcher calls this on each matching topic entry when it finds * multiple entries tied for first place, based on their match * scores. This gives the entries a chance to figure out which one * is actually the best match for the input, given the other entries * that also matched. * * This method returns a TopicEntry object - one of the objects from * the match list - if it has an opinion as to which one should take * precedence. It returns nil if it doesn't know or doesn't care. * Returning nil gives the other topics in the match list a chance to * make the selection. If all of the objects in the list return nil, * the topic database searcher simply picks one of the topic matches * arbitrarily. * * 'matchList' is the list of tied TopicEntry objects. 'topic' is * the ResolvedTopic object from the parser, representing the * player's input phrase that we're matching. 'fromActor' is the * actor performing the command. 'toks' is a list of strings giving * the word tokens of the noun phrase. * * The topic database searcher calls this method for each matching * topic entry in the case of a tie, and simply accepts the opinion * of the first one that expresses an opinion by returning a non-nil * value. There's no voting; whoever happens to get *and use* the * first say also gets the last word. We expect that this won't be a * problem in practice: when this comes up at all, it's because there * are a couple of closely related topic entries that are active in a * particular context, and you need a special bit of tweaking to pick * the right one for a given input phrase. Simply pick one of the * involved entries and define this method there. */ breakTopicTie(matchList, topic, fromActor, toks) { /* * we don't have an opinion - defer to the next object in the * list, or allow an arbitrary selection */ return nil; } /* * Set pronouns for the topic, if possible. If the topic corresponds * to a game-world object, then we should set the pronoun antecedent * to the game object. This must be handled per subclass because of * the range of possible meanings of 'topic'. */ setTopicPronouns(fromActor, topic) { } /* * Handle the topic. This is called when we find that this is the * best topic entry for the current topic. * * By default, we'll do one of two things: * * - If 'self' inherits from Script, then we'll simply invoke our * doScript() method. This makes it especially easy to set up a * topic entry that shows a series of responses: just add EventList * or one of its subclasses to the base class list when defining the * topic, and define the eventList property as a list of string * responses. For example: * *. + TopicEntry, StopEventList @blackBook *. ['<q>What makes you think I know anything about it?</q> *. he says, his voice shaking. ', *. '<q>No! You can\'t make me tell you!</q> he wails. ', *. '<q>All right, fine! I\'ll tell you, but I warn you, *. this is knowledge mortal men were never meant to know.</q> ', *. // and so on *. ] *. ; * * - Otherwise, we'll call our topicResponse property, which should * simply be a double-quoted string to display. This is the simplest * way to define a topic with just one response. * * Note that 'topic' will vary by subclass, depending on the type of * command used with the topic type. For example, for ASK and TELL * commands, 'topic' is a ResolvedTopic object; for GIVE and SHOW, * it's a simulation object (i.e., generally a Thing subclass). */ handleTopic(fromActor, topic) { /* note the invocation */ noteInvocation(fromActor); /* set pronoun antecedents if possible */ setTopicPronouns(fromActor, topic); /* check to see if we're a Script */ if (ofKind(Script)) { /* we're a Script - invoke our script */ doScript(); } else { /* show our simple response string */ topicResponse; } } /* note that we've been invoked */ noteInvocation(fromActor) { /* * we count as one of the alternatives in our alternative group, * so note the invocation of the group */ noteAltInvocation(fromActor, self); /* count the invocation */ ++talkCount; } /* * Note that something in our entire alternative group has been * invoked. We count as a member of our own group, so this is * invoked when we're invoked; this is also invoked when any AltTopic * child of ours is invoked. */ noteAltInvocation(fromActor, alt) { local owner; /* notify our owner of the topic invocation */ if ((owner = location.getTopicOwner()) != nil) owner.notifyTopicResponse(fromActor, alt); /* count the alternative invocation */ ++altTalkCount; } /* * Add a suggested topic. A suggested topic can be nested within a * topic entry; doing this associates the suggested topic with the * topic entry, and automatically associates the suggested topic * with the entry's actor or actor state. */ addSuggestedTopic(t) { /* * If the SuggestedTopic is *directly* within us, we're the * SuggestedTopic object's associated TopicEntry. The nesting * could be deeper, if we have alternative topics nested within * us; in these cases, we're not directly associated with the * suggested topic. */ if (t.location == self) t.associatedTopic = self; /* add the suggestion to our location's topic database */ if (location != nil) location.addSuggestedTopic(t); } ; /* * A TopicGroup is an abstract container for a set of TopicEntry objects. * The purpose of the group object is to apply a common "is active" * condition to all of the topics within the group. * * The isActive condition of the TopicGroup is effectively AND'ed with * any other conditions on the nested TopicEntry's. In other words, a * TopicEntry within the TopicGroup is active if the TopicEntry would * otherwise be acive AND the TopicGroup is active. * * TopicEntry objects are associated with the group via the 'location' * property - set the location of the TopicEntry to point to the * containing TopicGroup. * * You can put a TopicGroup anywhere a TopicEntry could go - directly * inside an Actor, inside an ActorState, or within another TopicGroup. * The topic entries within a topic group act as though they were * directly in the topic group's container. */ class TopicGroup: object /* * The group "active" condition - each instance should override this * to specify the condition that applies to all of the TopicEntry * objects within the group. */ isActive = true /* * The *adjustment* to the match score for topic entries contained * within this group. This is usually a positive number, so that it * boosts the match strength of the child topics. */ matchScoreAdjustment = 0 /* * the topic owner for any topic entries within the group is the * topic owner taken from the group's own location */ getTopicOwner() { return location.getTopicOwner(); } /* are TopicEntry objects within the group active? */ topicGroupActive() { /* * our TopicEntry objects are active if the group condition is * true and our container's contents are active */ return isActive && location.topicGroupActive(); } /* * Get my score adjustment. We'll return our own basic score * adjustment plus the cumulative adjustment for our containers. */ topicGroupScoreAdjustment = (matchScoreAdjustment + location.topicGroupScoreAdjustment) /* add a topic - we'll simply add the topic directly to our container */ addTopic(topic) { location.addTopic(topic); } /* add a suggested topic - we'll pass this up to our container */ addSuggestedTopic(topic) { location.addSuggestedTopic(topic); } ; /* * An alternative topic entry. This makes it easy to define different * responses to a topic according to the game state; for example, we * might want to provide a different response for a topic after some * event has occurred, so that we can reflect knowledge of the event in * the response. * * A set of alternative topics is sort of like an inverted if-then-else. * You start by defining a normal TopicEntry (an AskTopic, or an * AskTellTopic, or whatever) for the basic response. Then, you add a * nested AltTopic located within the base topic; you can add another * AltTopic nested within the base topic, and another after that, and so * on. When we need to choose one of the topics, we'll choose the last * one that indicates it's active. So, the order of appearance is * essentially an override order: the first AltTopic overrides its parent * TopicEntry, and each subsequent AltTopic overrides its previous * AltTopic. * * + AskTellTopic @lighthouse "It's very tall."; *. ++ AltTopic "Not really..." isActive=(...); *. ++ AltTopic "Well, maybe..." isActive=(...); *. ++ AltTopic "One more thing..." isActive=(...); * * In this example, the response we'll show for ASK ABOUT LIGHTHOUSE will * always be the LAST entry of the group that's active. For example, if * all of the responses are active except for the very last one, then * we'll show the "Well, maybe" response, because it's the last active * response. If the main AskTellTopic is active, but none of the * AltTopics are active, we'll show the "It's very tall" main response, * because it's the last element of the group that's active. * * Note that an AltTopic takes its matching information from its parent, * so you don't need to specify a matchObj or any other matching * information in an AltTopic. You merely need to provide the response * text and the isActive test. */ class AltTopic: TopicEntry /* we match if our parent matches, and with the same score */ matchTopic(fromActor, topic) { return location.matchTopic(fromActor, topic); } /* we can match if our parent can match */ isMatchPossible(actor, scopeList) { return location.isMatchPossible(actor, scopeList); } /* we can match a pre-parse string if our parent can */ matchPreParse(str, pstr) { return location.matchPreParse(str, pstr); } /* set pronouns for the topic */ setTopicPronouns(fromActor, topic) { location.setTopicPronouns(fromActor, topic); } /* include in the same lists as our parent */ includeInList = (location.includeInList) /* AltTopic initialization */ initializeAltTopic() { /* add myself to our parent's child list */ if (location != nil) location.addAltTopic(self); } /* * Determine if this topic is active. An AltTopic is active if its * own isActive indicates true, AND none of its subsequent siblings * are active. */ checkIsActive() { /* we can't be active if our own isActive says we're not */ if (!isActive) return nil; /* * Check for any active element after us in the parent's list. * To do this, scan from the end of the parent list backwards, * and look for an element that's active. If we reach our own * entry, then we'll know that there are no active entries * following us in the list. Note that we already know we're * active, or we wouldn't have gotten this far, so we can simply * look for the rightmost active element in the list. */ if (location != nil && location.altTopicList.lastValWhich({x: x.isActive}) != self) { /* * we found an active element after ourself, so it overrides * us - we're therefore not active */ return nil; } /* ask our container if its topics are active */ return location.topicGroupActive(); } /* take our implied-greeting status from our parent */ impliesGreeting = (location.impliesGreeting) /* take our conversational status from our parent */ isConversational = (location.isConversational) /* * Our relative order within our parent's list of alternatives. By * default, we simply return the source file ordering, which ensures * that static AltTopic objects (i.e., those defined directly in * source files, not dynamically created with 'new') will be ordered * just as they're laid out in the source file. */ altTopicOrder = (sourceTextOrder) /* note invocation */ noteInvocation(fromActor) { /* count our own invocation */ ++talkCount; /* let our container know its AltTopic child is being invoked */ if (location != nil) location.noteAltInvocation(fromActor, self); } /* our AltTopic counter is the AltTopic counter for the enclosing topic */ altTalkCount = (location != nil ? location.altTalkCount : talkCount) ; /* * A "topic match" topic entry. This is a topic entry that matches topic * phrases in the grammar. * * Handling topic phrases is a bit tricky, because they can't be resolved * to definitive game-world objects the way ordinary noun phrases can. * Topic phrases can refer to things that aren't physically present, but * which are known to the actor performing the command; they can refer to * abstract Topic objects, that have no physical existence in the game * world at all; and they can ever be arbitrary text that doesn't match * any vocabulary defined by the game. * * Our strategy in matching topics is to first narrow the list down to * the physical and abstract game objects that both match the vocabulary * used in the command and are part of the memory of the actor performing * the command. That much is handled by the normal topic phrase * resolution rules, and gives us a list of possible matches. Then, * given this narrowed list of possibilities, we look through the list of * objects that we're associated with; we effectively intersect the two * lists, and if the result is non-empty, we consider it a match. * Finally, we also consider any regular expression that we're associated * with; if we have one, and the topic phrase text in the command matches * the input, we'll consider it a match. */ class TopicMatchTopic: TopicEntry /* * A regular expression pattern that we'll match to the actual topic * text as entered in the command. If 'matchExactCase' is true, * we'll match the exact text in its original upper/lower case * rendering; otherwise, we'll convert the player input to lower-case * before matching it against the pattern. In most cases, we'll want * to match the input no matter what combination of upper and lower * case the player entered, so matchExactCase is nil by default. * * Note that both the object (or object list) and the regular * expression pattern can be included for a single topic entry * object. This allows a topic entry to match several different ways * of entering the topic name, or to match several different topics * with the same response. */ matchPattern = nil matchExactCase = nil /* * Match the topic. By default, we'll match to either the simulation * object or objects in matchObj, or the pattern in matchPattern. * Note that we always try both ways of matching, so a single * AskTellTopic can define both a pattern and an object list. * * 'topic' is a ResolvedTopic object describing the player's text * input and the list of objects that the parser matched to the text. * * Subclasses can override this as desired to use other ways of * matching. */ matchTopic(fromActor, topic) { /* * if we have one or more match objects, try matching to the * topic's best simulation object match */ if (matchObj != nil) { /* * we have a match object or match object list - if it's a * collection, check each element, otherwise just match the * single object */ if (matchObj.ofKind(Collection)) { /* try matching each object in the list */ if (matchObj.indexWhich({x: findMatchObj(x, topic)}) != nil) return matchScore; } else { /* match the single object */ if (findMatchObj(matchObj, topic)) return matchScore; } } /* * check for a match to the regular expression pattern, if we * have a pattern AND the resolved topic allows literal matches */ if (matchPattern != nil && topic.canMatchLiterally()) { local txt; /* * There's no match object; try matching our regular * expression to the actual topic text. Get the actual text. */ txt = topic.getTopicText(); /* * if they don't want an exact case match, convert the * original topic text to lower case */ if (!matchExactCase) txt = txt.toLower(); /* if the regular expression matches, we match */ if (rexMatch(matchPattern, txt) != nil) return matchScore; } /* we didn't find a match - indicate this with a nil score */ return nil; } /* * Match an individual item from our match list to the given * ResolvedTopic object. We'll check each object in the resolved * topic's "in scope" and "likely" lists. */ findMatchObj(obj, rt) { /* check the "in scope" list */ if (rt.inScopeList.indexOf(obj) != nil) return true; /* check the "likely" list */ return (rt.likelyList.indexOf(obj) != nil); } /* * It's possible for us to match if any of our matchObj objects are * known to the actor. If we have no matchObj objects, we must be * matching on a regular expression or on a custom condition, so we * can't speculate on matchability; we'll simply return true in those * cases. */ isMatchPossible(actor, scopeList) { /* check what we have in our matchObj */ if (matchObj == nil) { /* * we have no match object, so we must match on a regular * expression or a custom condition; we can't speculate on * our matchability, so just return true as a default */ return true; } else if (matchObj.ofKind(Collection)) { /* * we have a list of match objects - return true if any of * them are known or are currently in scope */ return (matchObj.indexWhich( {x: actor.knowsAbout(x) || scopeList.indexOf(x)}) != nil); } else { /* * we have a single match object - return true if it's known * or it's in scope */ return (actor.knowsAbout(matchObj) || scopeList.indexOf(matchObj) != nil); } } /* set the topic pronouns */ setTopicPronouns(fromActor, topic) { /* check to see what kind of match object we have */ if (matchObj == nil) { /* * no match object, so we must match a regular expression * pattern; this gives us no clue what game object we might * match, so there's nothing we can do here */ } else if (matchObj.ofKind(Collection)) { local lst; /* * We match a list of objects. Get the subset of the * in-scope list from the topic that we match. Consider only * the in-scope items for now, and consider only game-world * objects (Things). */ lst = matchObj.subset( {x: x.ofKind(Thing) && topic.inScopeList.indexOf(x) != nil}); /* if that didn't turn up anything, consider the likelies, too */ if (lst.length() == 0) lst = matchObj.subset( {x: (x.ofKind(Thing) && topic.likelyList.indexOf(x) != nil)}); /* * if that narrows it down to one match, make it the pronoun * antecedent */ if (lst.length() == 1) fromActor.setPronounObj(lst[1]); } else { /* * we match a single object; if it's a game-world object (a * Thing), use it as the pronoun antecedent */ if (matchObj.ofKind(Thing)) fromActor.setPronounObj(matchObj); } } ; /* * A dual ASK/TELL topic database entry. This type of topic is included * in both the ASK ABOUT and TELL ABOUT lists. * * Many authors have chosen to treat ASK and TELL as equivalent, or at * least, equivalent for most topics. Since these verbs only very weakly * suggest what the player character is actually saying, it's frequently * the case that a given topic response makes just as much sense coming * from TELL as from ASK, or vice versa. In these cases, it's best to * enter the topic under both ASK and TELL; which one the player tries * might simply depend on the player's frame of mind, and they might feel * cheated if one works and the other doesn't in cases where both are * equally valid. */ class AskTellTopic: TopicMatchTopic /* include me in both the ASK and TELL lists */ includeInList = [&askTopics, &tellTopics] ; /* * An ASK ABOUT topic database entry. This type of topic is included in * the ASK ABOUT list only. */ class AskTopic: AskTellTopic includeInList = [&askTopics] ; /* * A TELL ABOUT topic database entry. This type of topic entry is * included in the TELL ABOUT list only. */ class TellTopic: AskTellTopic includeInList = [&tellTopics] ; /* * An ASK FOR topic database entry. This type of topic entry is * included in the ASK FOR list only. */ class AskForTopic: AskTellTopic includeInList = [&askForTopics] ; /* * A combination ASK ABOUT and ASK FOR topic. */ class AskAboutForTopic: AskTellTopic includeInList = [&askTopics, &askForTopics] ; /* * A combination ASK ABOUT, TELL ABOUT, and ASK FOR topic. */ class AskTellAboutForTopic: AskTellTopic includeInList = [&askTopics, &tellTopics, &askForTopics] ; /* * A base class for topic entries that match simple simulation objects. */ class ThingMatchTopic: TopicEntry /* * Match the topic. We'll match the simulation object in 'obj' to * our matchObj object or list. */ matchTopic(fromActor, obj) { /* * if matchObj is a collection, check each element, otherwise * just match the single object */ if (matchObj.ofKind(Collection)) { /* try matching each object in the list */ if (matchObj.indexOf(obj) != nil) return matchScore; } else { /* match the single object */ if (matchObj == obj) return matchScore; } /* didn't find a match - indicate this by returning a nil score */ return nil; } /* * It's possible for us to match if any of our matchObj objects are * in scope. */ isMatchPossible(actor, scopeList) { /* check to see what kind of match object we have */ if (matchObj.ofKind(Collection)) { /* we can match if any of our match objects are in scope */ return (matchObj.indexWhich({x: scopeList.indexOf(x)}) != nil); } else { /* we can match if our single match object is in scope */ return scopeList.indexOf(matchObj); } } /* set the topic pronouns */ setTopicPronouns(fromActor, topic) { /* * the 'topic' is just an ordinary game object; as long as it's a * Thing, set it as the antecedent */ if (topic.ofKind(Thing)) fromActor.setPronounObj(topic); } ; /* * A GIVE/SHOW topic database entry. * * Note that this base class is usable for any command that refers to a * simulation object. It's NOT suitable for ASK/TELL lists, or for other * commands that refer to topics, since we expect our 'topic' to be a * resolved simulation object. */ class GiveShowTopic: ThingMatchTopic /* include me in both the GIVE and SHOW lists */ includeInList = [&giveTopics, &showTopics] ; /* * A GIVE TO topic database entry. This type of topic entry is included * in the GIVE TO list only. */ class GiveTopic: GiveShowTopic includeInList = [&giveTopics] ; /* * A SHOW TO topic database entry. This type of topic entry is included * in the SHOW TO list only. */ class ShowTopic: GiveShowTopic includeInList = [&showTopics] ; /* * A TopicEntry that can match a Thing or a Topic. This can be used to * combine ASK/TELL-type responses and GIVE/SHOW-type responses in a * single topic entry. * * When this kind of topic is used as a suggested topic, note that you * should name the suggestion according to the least restrictive verb. * This is important because the suggestion will be active if any of the * verbs would allow it; to ensure that we suggest a verb that will * actually work, we should thus use the least restrictive verb. In * practice, this means you should use ASK or TELL as the suggestion * name, because an object merely has to be known to be used as a topic; * it might be possible to ASK/TELL about an object but not GIVE/SHOW the * object, because the object is known but not currently in scope. */ class TopicOrThingMatchTopic: ThingMatchTopic, TopicMatchTopic matchTopic(fromActor, obj) { /* * if we're being asked to match a ResolvedTopic, use the * inherited TopicMatchTopic handling; otherwise, use the * inherited ThingMatchTopic handling */ if (obj.ofKind(ResolvedTopic)) return inherited TopicMatchTopic(fromActor, obj); else return inherited ThingMatchTopic(fromActor, obj); } isMatchPossible(actor, scopeList) { /* if a match is possible from either subclass, allow it */ return (inherited TopicMatchTopic(actor, scopeList) || inherited ThingMatchTopic(actor, scopeList)); } setTopicPronouns(fromActor, obj) { /* * if the object is a ResolvedTopic, use the inherited * TopicMatchTopic handling, otherwise use the ThingMatchTopic * handling */ if (obj.ofKind(ResolvedTopic)) return inherited TopicMatchTopic(fromActor, obj); else return inherited ThingMatchTopic(fromActor, obj); } ; /* * A combined ASK/TELL/SHOW topic. Players will sometimes want to point * something out when it's visible, rather than asking about it; this * allows SHOW TO to be used as a synonym for ASK ABOUT for these cases. */ class AskTellShowTopic: TopicOrThingMatchTopic includeInList = [&askTopics, &tellTopics, &showTopics] ; /* * A combined ASK/TELL/GIVE/SHOW topic. */ class AskTellGiveShowTopic: TopicOrThingMatchTopic includeInList = [&askTopics, &tellTopics, &giveTopics, &showTopics] ; /* * A command topic. This is used to respond to orders given to an NPC, * as in "BOB, GO EAST." The match object for this kind of topic entry * is an Action class; for example, to create a response to "BOB, LOOK", * we'd create a CommandTopic that matches LookAction. * * If you're designing a CommandTopic for a command can be accepted from * a remote location, such as by telephone, you should be aware that the * command will be running in the NPC's visual sense context. This means * that if the player character can't see the NPC, the topic result * message will be hidden - the NPC's visual sense context hides all * messages generated while it's in effect if the PC can't see the NPC. * This is usually desirable, since most messages relay visual * information that wouldn't be visible to the player character if the PC * can't see the subject of the message. However, if you've specifically * designed your CommandTopic to work remotely, this isn't at all what * you want, since you've already taken the remoteness into account in * the message and thus want the message to be displayed after all. The * way to handle this is to wrap the message in a callWithSenseContext() * with a nil sense context. For example: * * topicResponse() *. { callWithSenseContext(nil, nil, {: "Here's my message!" }); } */ class CommandTopic: TopicEntry /* we go in the command topics list */ includeInList = [&commandTopics] /* match the topic */ matchTopic(fromActor, obj) { /* * Check the collection or the single object, as needed. Note * that our match object is an Action base class, so we must * match if 'obj' is of the match object class. */ if (matchObj.ofKind(Collection)) { /* check each entry for a match */ if (matchObj.indexWhich({x: obj.ofKind(x)}) != nil) return matchScore; } else { /* check our single object */ if (obj.ofKind(matchObj)) return matchScore; } /* didn't find a match */ return nil; } /* * we can always match, since the player can always type in any * possible action */ isMatchPossible(actor, scopeList) { return true; } /* we have no pronouns to set */ setTopicPronouns(fromActor, topic) { } ; /* * A base class for simple miscellaneous topics. These handle things * like YES, NO, HELLO, and GOODBYE, where the topic is entirely * contained in the verb, and there's no separate noun phrase needed to * indicate the topic. */ class MiscTopic: TopicEntry matchTopic(fromActor, obj) { /* * if it's one of our matching topics, return our match score, * otherwise return a nil score to indicate failure */ return (matchList.indexOf(obj) != nil) ? matchScore : nil; } /* * a match is always possible for simple verb topics (since the * player could always type the verb) */ isMatchPossible(actor, scopeList) { return true; } ; /* * A greeting topic - this handles a HELLO or TALK TO command, as well * as implied greetings (the kind of greeting generated when we jump * directly into a conversation with an actor that uses stateful * conversations, by typing a command like ASK ABOUT or TELL ABOUT * without first saying HELLO explicitly). */ class HelloTopic: MiscTopic includeInList = [&miscTopics] matchList = [helloTopicObj, impHelloTopicObj] /* * this is an explicit greeting, so it obviously shouldn't trigger * an implied greeting, regardless of how conversational we are */ impliesGreeting = nil /* * if we use this as a greeting upon entering a ConvNode, we'll want * to stay in the node afterward */ noteInvocation(fromActor) { inherited(fromActor); "<.convstay>"; } ; /* * An implied greeting topic. This handles ONLY implied greetings. * * Note that we have a higher-than-normal score by default. This makes * it easy to program two common cases for conversational states. * First, the more common case, where you want a single message for both * implied and explicit greetings: just create a HelloTopic, since that * responds to both kinds. Second, the less common case, where we want * to differentiate, writing separate responses for implied and explicit * greetings: create a HelloTopic for the explicit kind, and ALSO create * an ImpHelloTopic for the implied kind. Since the ImpHelloTopic has a * higher score, it'll overshadow the HelloTopic object when it matches * an implied greeting; but since ImpHelloTopic doesn't match an * explicit greeting, we'll fall back on the HelloTopic for that. */ class ImpHelloTopic: MiscTopic includeInList = [&miscTopics] matchList = [impHelloTopicObj] matchScore = 200 /* * this is itself a greeting, so we obviously don't want to trigger * another greeting to greet the greeting */ impliesGreeting = nil /* * if we use this as a greeting upon entering a ConvNode, we'll want * to stay in the node afterward */ noteInvocation(fromActor) { inherited(fromActor); "<.convstay>"; } ; /* * Actor Hello topic - this handles greetings when an NPC initiates the * conversation. */ class ActorHelloTopic: MiscTopic includeInList = [&miscTopics] matchList = [actorHelloTopicObj] matchScore = 200 /* this is a greeting, so we don't want to trigger another greeting */ impliesGreeting = nil /* * if we use this as a greeting upon entering a ConvNode, we'll want * to stay in the node afterward */ noteInvocation(fromActor) { inherited(fromActor); "<.convstay>"; } ; /* * A goodbye topic - this handles both explicit GOODBYE commands and * implied goodbyes. Implied goodbyes happen when a conversation ends * without an explicit GOODBYE command, such as when the player character * walks away from the NPC, or the NPC gets bored and wanders off, or the * NPC terminates the conversation of its own volition. */ class ByeTopic: MiscTopic includeInList = [&miscTopics] matchList = [byeTopicObj, leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj] /* * If we're not already in a conversation when we say GOODBYE, don't * bother saying HELLO implicitly - if the player is saying GOODBYE * explicitly, she probably has the impression that there's some kind * of interaction already going on with the NPC. If we didn't * override this, you'd get an automatic HELLO followed by the * explicit GOODBYE when not already in conversation, which is a * little weird. */ impliesGreeting = nil ; /* * An implied goodbye topic. This handles ONLY automatic (implied) * conversation endings, which happen when we walk away from an actor * we're talking to, or the other actor ends the conversation after being * ignored for too long, or the other actor ends the conversation of its * own volition via npc.endConversation(). * * We use a higher-than-default matchScore so that any time we have both * a ByeTopic and an ImpByeTopic that are both active, we'll choose the * more specific ImpByeTopic. */ class ImpByeTopic: MiscTopic includeInList = [&miscTopics] matchList = [leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj] matchScore = 200 ; /* * A "bored" goodbye topic. This handles ONLY goodbyes that happen when * the actor we're talking terminates the conversation out of boredom * (i.e., after a period of inactivity in the conversation). * * Note that this is a subset of ImpByeTopic - ImpByeTopic handles * "bored" and "leaving" goodbyes, while this one handles only the * "bored" goodbyes. You can use this kind of topic if you want to * differentiate the responses to "bored" and "leaving" conversation * endings. */ class BoredByeTopic: MiscTopic includeInList = [&miscTopics] matchList = [boredByeTopicObj] matchScore = 300 ; /* * A "leaving" goodbye topic. This handles ONLY goodbyes that happen * when the PC walks away from the actor they're talking to. * * Note that this is a subset of ImpByeTopic - ImpByeTopic handles * "bored" and "leaving" goodbyes, while this one handles only the * "leaving" goodbyes. You can use this kind of topic if you want to * differentiate the responses to "bored" and "leaving" conversation * endings. */ class LeaveByeTopic: MiscTopic includeInList = [&miscTopics] matchList = [leaveByeTopicObj] matchScore = 300 ; /* * An "actor" goodbye topic. This handles ONLY goodbyes that happen when * the NPC terminates the conversation of its own volition via * npc.endConversation(). */ class ActorByeTopic: MiscTopic includeInList = [&miscTopics] matchList = [actorByeTopicObj] matchScore = 300 ; /* a topic for both HELLO and GOODBYE */ class HelloGoodbyeTopic: MiscTopic includeInList = [&miscTopics] matchList = [helloTopicObj, impHelloTopicObj, byeTopicObj, boredByeTopicObj, leaveByeTopicObj, actorByeTopicObj] /* * since we handle greetings, we don't want to trigger a separate * implied greeting */ impliesGreeting = nil ; /* * Topic singletons representing HELLO and GOODBYE topics. These are * used as the parameter to matchTopic() when we're looking for the * response to the corresponding verbs. */ helloTopicObj: object; byeTopicObj: object; /* * a topic singleton for implied greetings (the kind of greeting that * happens when we jump right into a conversation with a command like * ASK ABOUT or TELL ABOUT, rather than explicitly saying HELLO first) */ impHelloTopicObj: object; /* * a topic singleton for an NPC-initiated hello (this is the kind of * greeting that happens when the NPC is the one who initiates the * conversation, via actor.initiateConversation()) */ actorHelloTopicObj: object; /* * topic singletons for the two kinds of automatic goodbyes (the kind of * conversation ending that happens when we simply walk away from an * actor we're in conversation with, or when we ignore the other actor * for enough turns that the actor gets bored and ends the conversation * of its own volition) */ boredByeTopicObj: object; leaveByeTopicObj: object; /* * a topic singleton for an NPC-initiated goodbye (this is the kind of * goodbye that happens when the NPC is the one who breaks off the * conversation, via npc.endConversation()) */ actorByeTopicObj: object; /* * A YES/NO topic. These handle YES and/or NO, which are normally used * as responses to questions posed by the NPC. YesNoTopic is the base * class, and can be used to create a single response for both YES and * NO; YesTopic provides a response just for YES; and NoTopic provides a * response just for NO. The only thing an instance of these classes * should normally need to specify is the response text (or a list of * response strings, by multiply inheriting from an EventList subclass as * usual). */ class YesNoTopic: MiscTopic includeInList = [&miscTopics] /* * our list of matching topic objects - we'll only ever be asked to * match 'yesTopicObj' (for YES inputs) or 'noTopicObj' (for NO * inputs) */ matchList = [yesTopicObj, noTopicObj] ; class YesTopic: YesNoTopic matchList = [yesTopicObj] ; class NoTopic: YesNoTopic matchList = [noTopicObj] ; /* * Topic singletons representing the "topic" of YES and NO commands. We * use these as the parameter to matchTopic() in the TopicEntry objects * when we're looking for a response to a YES or NO command. */ yesTopicObj: object; noTopicObj: object; /* * A default topic entry. This is an easy way to create an entry that * will be used as a last resort, if no other entry is found. This kind * of entry will match *any* topic, but with the lowest possible score, * so it will only be used if there's no other match for the topic. * * It's a good idea to provide some variety in a character's default * responses, because it seems that in every real game session, the * player will at some point spend a while peppering an NPC with * questions on every topic that comes to mind. Usually, the player will * think of many things that the author didn't anticipate. The more * things the author covers, the better, but it's unrealistic to think * that an author can reasonably anticipate every topic, or even most * topics, that players will think of. So, we'll have a whole bunch of * ASK, ASK, ASK commands all at once, and much of the time we'll get a * bunch of default responses in a row. It gets tedious in these cases * when the NPC repeats the same default response over and over. * * A simple but effective trick is to provide three or four random * variations on "I don't know that," customized for the character. This * makes the NPC seem less like a totally predictable robot, and it can * also be a convenient place to flesh out the character a bit. An easy * way to do this is to add ShuffledEventList to the superclass list of * the default topic entry, and provide a eventList list with the various * random responses. For example: * * + DefaultAskTellTopic, ShuffledEventList *. ['Bob mutters something unintelligible and keeps fiddling with *. the radio. ', *. 'Bob looks up from the radio for a second, but then goes back *. to adjusting the knobs. ', *. 'Bob just keeps adjusting the radio, completely ignoring you. '] *. ; * * It's important to be rather generic in default responses; in * particular, it's a bad idea to suggest that the NPC doesn't know about * the topic. From the author's perspective, it's easy to make the * mistake of thinking "this is a default response, so it'll only be used * for topics that are completely off in left field." Wrong! Sometimes * the player will indeed ask about completely random stuff, but in * *most* cases, the player is only asking because they think it's a * reasonable thing to ask about. Defaults that say things like "I don't * know anything about that" or "What a crazy thing to ask about" or "You * must be stupid if you think I know about that!" can make a game look * poorly implemented, because these will inevitably be shown in response * to questions that the NPC really ought to know about: * *. >ask bob about his mother *. "I don't know anything about that!" *. *. >ask bob about his father *. "You'd have to be a moron to think I'd know about that!" * * It's better to use responses that suggest that the NPC is * uninterested, or is hostile, or is preoccupied with something else, or * doesn't understand the question, or something else appropriate to the * character. If you can manage to make the response about the * *character*, rather than the topic, it'll reduce the chances that the * response is jarringly illogical. */ class DefaultTopic: TopicEntry /* * A list of objects to exclude from the default match. This can be * used to create a default topic that matches everything EXCEPT a * few specific topics that are handled in enclosing topic databases. * For example, if you want to create a catch-all in a ConvNode's * list of topics, but you want a particular topic to escape the * catch-all and be sent instead to the Actor's topic database, you * can put that topic in the exclude list for the catch-all, making * it a catch-almost-all. */ excludeMatch = [] /* match anything except topics in our exclude list */ matchTopic(fromActor, topic) { /* * If the topic matches anything in the exclusion list, do NOT * match the topic. If 'topic' is a ResolvedTopic, search its * in-scope and 'likely' lists; otherwise search for 'topic' * directly in the exclusion list. */ if (topic.ofKind(ResolvedTopic)) { /* it's a resolved topic, so search the in-scope/likely lists */ if (topic.inScopeList.intersect(excludeMatch).length() != 0 || topic.likelyList.intersect(excludeMatch).length() != 0) return nil; } else if (excludeMatch.indexOf(topic) != nil) return nil; /* match anything else with our score */ return matchScore; } /* use a low default matching score */ matchScore = 1 /* a match is always possible for a default topic */ isMatchPossible(actor, scopeList) { return true; } /* set the topic pronoun */ setTopicPronouns(fromActor, topic) { /* * We're not matching anything, so we can get no guidance from * the match object. Instead, look at the topic itself. If it's * a Thing, set the Thing as the antecedent. If it's a * ResolvedTopic, and there's only one Thing match in scope, or * only one Thing match in the likely list, set that. Otherwise, * we have no grounds for guessing. */ if (topic != nil) { if (topic.ofKind(Thing)) { /* we have a Thing - use it as the antecedent */ fromActor.setPronounObj(topic); } else if (topic.ofKind(ResolvedTopic)) { local lst; /* * if there's only one Thing in scope, or only one Thing * in the 'likely' list, use it */ lst = topic.inScopeList.subset({x: x.ofKind(Thing)}); if (lst.length() == 0) lst = topic.likelyList.subset({x: x.ofKind(Thing)}); /* if we got exactly one object, it's the antecedent */ if (lst.length() == 1) fromActor.setPronounObj(lst[1]); } } } ; /* * Default topic entries for different uses. We'll use a hierarchy of * low match scores, in descending order of specificity: 3 for * single-type defaults (ASK only, for example), 2 for multi-type * defaults (ASK/TELL), and 1 for the ANY default. */ class DefaultCommandTopic: DefaultTopic includeInList = [&commandTopics] matchScore = 3 ; class DefaultAskTopic: DefaultTopic includeInList = [&askTopics] matchScore = 3 ; class DefaultTellTopic: DefaultTopic includeInList = [&tellTopics] matchScore = 3 ; class DefaultAskTellTopic: DefaultTopic includeInList = [&askTopics, &tellTopics] matchScore = 2 ; class DefaultGiveTopic: DefaultTopic includeInList = [&giveTopics] matchScore = 3 ; class DefaultShowTopic: DefaultTopic includeInList = [&showTopics] matchScore = 3 ; class DefaultGiveShowTopic: DefaultTopic includeInList = [&giveTopics, &showTopics] matchScore = 2 ; class DefaultAskForTopic: DefaultTopic includeInList = [&askForTopics] matchScore = 3 ; class DefaultAnyTopic: DefaultTopic includeInList = [&askTopics, &tellTopics, &showTopics, &giveTopics, &askForTopics, &miscTopics, &commandTopics] /* * exclude these from actor-initiated hellos & goodbyes - those * should only match topics explicitly */ excludeMatch = [actorHelloTopicObj, actorByeTopicObj] matchScore = 1 ; /* * A "special" topic. This is a topic that responds to its own unique, * custom command input. In other words, rather than responding to a * normal command like ASK ABOUT or SHOW TO, we'll respond to a command * for which we define our own syntax. Our special syntax doesn't have * to follow any of the ordinary parsing conventions, because whenever * our ConvNode is active, we get a shot at parsing player input before * the regular parser gets to see it. * * A special topic MUST be part of a ConvNode, because these are * inherently meaningful only in context. A special topic is active * only when its conversation node is active. * * Special topics are automatically Suggested Topics as well as Topic * Entries. Because special topics use their own custom grammar, it's * unreasonable to expect a player to guess at the custom grammar, so we * should always provide a topic inventory suggestion for every special * topic. */ class SpecialTopic: TopicEntry, SuggestedTopicTree /* * Our keyword list. Each special topic instance must define a list * of strings giving the keywords we match. The special topic will * match user input if the user input consists exclusively of words * from this keyword list. The user input doesn't have to include * all of the words defined here, but all of the words in the user's * input have to appear here to match. * * Alternatively, an instance can specifically define its own custom * regular expression pattern instead of using the keyword list; the * regular expression allows the instance to include punctuation in * the syntax, or apply more restrictive criteria than simply * matching the keywords. */ keywordList = [] /* * Initialize the special topic. This runs during * pre-initialization, to give us a chance to do pre-game set-up. * * This routine adds the topic's keywords to the global dictionary, * under the 'special' token type. Since a special topic's keywords * are accepted when the special topic is active, it would be wrong * for the parser to claim that the words are unknown when the * special topic isn't active. By adding the keywords to the * dictionary, we let the parser know that they're valid words, so * that it won't claim that they're unknown. */ initializeSpecialTopic() { /* add each keyword */ foreach (local cur in keywordList) { /* * Add the keyword. Since we don't actually need the * word-to-object association that the dictionary stores, * simply associate the word with the SpecialTopic class * rather than with this particular special topic instance. * The dictionary only stores a given word-obj-prop * association once, even if it's entered repeatedly, so * tying all of the special topic keywords to the * SpecialTopic class ensures that we won't store redundant * entries if the same keyword is used in multiple special * topics. */ cmdDict.addWord(SpecialTopic, cur, &specialTopicWord); } } /* * our regular expression pattern - we'll build this automatically * from the keyword list if this isn't otherwise defined */ matchPat = nil /* our suggestion (topic inventory) base name */ name = '' /* * our suggestion (topic inventory) full name is usually the same as * the base name; special topics usually aren't grouped in topic * suggestion listings, since each topic usually has its own unique, * custom syntax */ fullName = (name) /* on being suggested, update the special topic history */ noteSuggestion() { specialTopicHistory.noteListing(self); } /* include in the specialTopics list of our parent topic database */ includeInList = [&specialTopics] /* * By default, don't limit the number of times we'll suggest this * topic. Since a special topic is valid only in a particular * ConvNode context, we normally want all of the topics in that * context to be available, even if they've been used before. */ timesToSuggest = nil /* check for a match */ matchTopic(fromActor, topic) { /* * We match if and only if we're the current active topic for * our conversation node, as designated during our pre-parsing. * Because we're activated exclusively by our special syntax, * the only way we can ever match is by matching our special * syntax in pre-parsing; when that happens, the pre-parser * notes the matching SpecialTopic and sends a pseudo-command to * the parser to let it know to invoke the special topic's * response. We take this circuitous route to showing the * response because we do our actual matching in the pre-parse * step, but we want to do the actual command processing * normally; we can only accomplish both needs using this * two-step process, with the two steps tied together via our * memory of the topic selected in pre-parse. */ if (getConvNode().activeSpecialTopic == self) return matchScore; else return nil; } /* * a special topic is always matchable, since we match on literal * text */ isMatchPossible(actor, scopeList) { return true; } /* * Match a string during pre-parsing. By default, we'll match the * string if all of its words (as defined by the regular expression * parser) match our keywords. */ matchPreParse(str, procStr) { /* build the regular expression pattern if there isn't one */ if (matchPat == nil) { local pat; /* start with the base pattern string */ pat = '<nocase><space>*(%<'; /* add the keywords */ for (local i = 1, local len = keywordList.length() ; i <= len ; ++i) { /* add this keyword to the pattern */ pat += keywordList[i]; /* add the separator or terminator, as appropriate */ if (i == len) pat += '%><space>*)+'; else pat += '%><space>*|%<'; } /* create the pattern object */ matchPat = new RexPattern(pat); } /* we have a match if the pattern matches the processed input */ return rexMatch(matchPat, procStr) == procStr.length(); } /* find our enclosing ConvNode object */ getConvNode() { /* scan up the containment tree for a ConvNode */ for (local loc = location ; loc != nil ; loc = loc.location) { /* if this is a ConvNode, it's what we're looking for */ if (loc.ofKind(ConvNode)) return loc; } /* not found */ return nil; } ; /* * A history of special topics listed in topic inventories. This keeps * track of special topics that we've recently offered, so that we can * provide better feedback if the player tries to use a recently-listed * special topic after it's gone out of context. * * When the player types a command that the parser doesn't recognize, the * parser will check the special topic history to see if the command * matches a special topic that was suggested recently. If so, we'll * explain that the command isn't usable right now, rather than claiming * that the command is completely invalid. A player might justifiably * find it confusing to have the game suggest a command one minute, and * then claim that the very same command is invalid a minute later. * * Ideally, we'd search *every* special topic for a match each time the * player enters an invalid command, but that could take a long time in a * conversation-heavy game with a large number of special topics. As a * compromise, we keep track of the last few special commands that were * actually suggested, so that we can scan those. The reasoning is that * a player is more likely to try a recently-offered special command; the * player will probably eventually forget older suggestions, and in any * case it's much more jarring to see a "command not understood" response * to a suggestion that's still fresh in the player's memory. * * This is a transient object because we're interested in the special * topics that have been offered in the current session, irrespective of * things like 'undo' and 'restore'. From the player's perspective, the * recency of a special topic suggestion is a function of the transcript, * not of the internal story timeline. For example, if the game suggests * a special topic, then the player types UNDO, the player might still * think to try the special topic on the next turn simply because it's * right there on the screen a few lines up. */ transient specialTopicHistory: object /* * Maximum number of topics to keep in our inventory. When the * history exceeds this number, we'll throw away the oldest entry * each time we need to add a new entry - thus, we'll always have the * N most recent suggestions. * * This can be configured as desired. The default setting tries to * strike a balance between speed and good feedback - we try to keep * track of enough entries that most players wouldn't think to try * anything that's aged out of the list, but not so many that it * takes a long time to scan them all. * * If you set this to nil, we won't keep a history at all, but * instead simply scan every special topic in the entire game when we * need to look for a match to an entered command - in a game with a * small number of special topics (on the order of, say, 30 or 40), * there should be no problem using this approach. Note that this * changes the behavior in one important way: when there's no history * limit, we can topics that *haven't even been offered yet*. In * some ways this is more desirable than only scanning past * suggestions, since it avoids weird situations where the game * claims that a command is unrecognized at one point, but later * suggests and then accepts the exact same command. It's * conceivably less desirable in that it could accidentally give away * information to the player, by letting them know that a randomly * typed command will be meaningful at some point in the game - but * the odds of this even happening seem minuscule, and the * possibility that it would give away meaningful information even if * it did happen seems very remote. */ maxEntries = 20 /* note that a special topic 't' is being listed in a topic inventory */ noteListing(t) { /* * If t's already in the list, delete it from its current * position, so that we can add it back at the end of the list, * reflecting its status as the most recent entry. */ historyList.removeElement(t); /* * if the list is already at capacity, remove the oldest entry, * which is the first entry in the list */ if (maxEntries != nil && historyList.length() >= maxEntries) historyList.removeElementAt(1); /* add the new entry at the end of the list */ historyList.append(t); } /* * Scan the history list (or, if there's no limit to the history, * scan all of the special topics in the entire game) for a match to * an unrecognized command. Returns true if we find a match, nil if * not. */ checkHistory(toks) { local str, procStr; /* get the original and processed version of the input string */ str = cmdTokenizer.buildOrigText(toks); procStr = specialTopicPreParser.processInputStr(str); /* * scan each special topic in the history - or, if the history is * unlimited, scan every special topic */ if (maxEntries != nil) { /* scan each entry in our history list */ for (local l = historyList, local i = 1, local len = l.length() ; i <= len ; ++i) { /* check this entry */ if (l[i].matchPreParse(str, procStr)) return true; } } else { /* no history limit - scan every special topic in the game */ for (local o = firstObj(SpecialTopic) ; o != nil ; o = nextObj(o, SpecialTopic)) { /* check this entry */ if (o.matchPreParse(str, procStr)) return true; } } /* we didn't find a match */ return nil; } /* * The list of entries. Create it when we first need it, which * perInstance does for us. */ historyList = perInstance(new transient Vector(maxEntries)) ; /* * An "initiate" topic entry. This is a rather different kind of topic * entry from the ones we've defined so far; an initiate topic is for * cases where the NPC itself wants to initiate a conversation in * response to something in the environment. * * One way to use initiate topics is to use the current location as the * topic key. This lets the NPC say something appropriate to the current * room, and can be coded simply as * *. actor.initiateTopic(location); */ class InitiateTopic: ThingMatchTopic /* include in the initiateTopics list */ includeInList = [&initiateTopics] /* * since this kind of topic is triggered by internal calculations in * the game, and not on anything the player is doing, there's no * reason that our match object should be a pronoun antecedent */ setTopicPronouns(fromActor, topic) { } ; /* a catch-all default initiate topic */ class DefaultInitiateTopic: DefaultTopic includeInList = [&initiateTopics] ; /* ------------------------------------------------------------------------ */ /* * An ActorState represents the current state of an Actor. * * The main thing that makes actors special is that they're supposed to * be living, breathing people or creatures. That substantially * complicates the programming of one of these objects, because in order * to create the appearance of animation, many things about an actor have * to change over time. * * The ActorState is designed to make it easier to program this * variability that's needed to make an actor seem life-like. The idea * is to separate the parts of an actor that tend to change according to * what the actor is doing, moving all of those out of the Actor object * and into an ActorState object instead. Each ActorState object * represents one state of an actor (i.e., one thing the actor can be * doing). The Actor object becomes easier to program, because we've * reduced the Actor object to the character's constant, unchanging * features. The stateful part is also easier to program, because we * don't have to make it conditional on anything; we simply define all of * the stateful parts in an ActorState, and we define separate ActorState * objects for the different states. * * For example, suppose we want a shopkeeper actor, whose activities * include waiting behind the counter, sweeping the floor, and stacking * cans. We'd define one ActorState object for each of these activities. * When the shopkeeper switches from standing behind the counter to * sweeping, for example, we simply set the "curState" property in the * shopkeeper object so that it points to the "sweeping" state object. * When it's time to stack cans, we change "curState" to it points to the * "stacking cans" state object. */ class ActorState: TravelMessageHandler, ActorTopicDatabase construct(actor) { location = actor; } /* * Activate the state - this is called when we're about to become * the active state for an actor. We do nothing by default. */ activateState(actor, oldState) { } /* * Deactivate the state - this is called when we're the active state * for an actor, and the actor is about to switch to a new state. * We do nothing by default. */ deactivateState(actor, newState) { } /* * Is this the actor's initial state? If so, we'll automatically * set the actor's curState to point to 'self' during * pre-initialization. For obvious reasons, this should be set to * true for only one state for each actor; if multiple states are * all flagged as initial for the same actor, we'll pick on * arbitrarily as the actual initial state. */ isInitState = nil /* * Should we automatically suggest topics when the player greets our * actor? By default, we show our "topic inventory" (the list of * currently active topics marked as "suggested"). This can be set * to nil to suppress this automatic suggestion list. * * Some authors might not like the idea of automatically suggesting * topics every time we greet a character, but nonetheless wish to * keep the TOPICS command as a sort of hint mechanism. This flag * can be used for this purpose. Authors who don't like suggested * topics at all can simply skip defining any SuggestedTopic entries, * in which case there will never be anything to suggest, rendering * this flag moot. */ autoSuggest = true /* * The 'location' is the actor that we're associated with. * * ActorState objects aren't actual simulation objects, so the * 'location' property isn't used for containment. For convenience, * though, use it to indicate which actor we're associated with; this * lets us use the '+' notation to define the state objects * associated with an actor. */ location = nil /* * Get the actor associated with the state - this is simply the * 'location' property. If we're nested inside another ActorState, * then our actor is our enclosing ActorState's actor. */ getActor() { if (location.ofKind(ActorState)) return location.getActor(); else return location; } /* the owner of any topic entries within the state is just my actor */ getTopicOwner() { return getActor(); } /* initialize the actor state */ initializeActorState() { /* * if we're the initial state for our actor, set the actor's * current state property to point to me */ if (isInitState) getActor().setCurState(self); } /* * Show the special description for the actor when the actor is * associated with this state. By default, we use the actor's * actorHereDesc message, which usually shows a generic message * (something like "Bob is here" or "Bob is sitting on the chair") to * indicate that the actor is present. * * States representing scripted activities should override these to * indicate what the actor is doing: "Bob is sweeping the floor," for * example. */ specialDesc() { getActor().actorHereDesc; } /* show the special description for the actor at a distance */ distantSpecialDesc() { getActor().actorThereDesc; } /* show the special description for the actor in a remote location */ remoteSpecialDesc(actor) { getActor().actorThereDesc; } /* * The list group(s) for the special description. By default, if * our specialDesc isn't overridden, we'll keep this in sync with * the specialDesc by returning our actor's actorListWith. And if * specialDesc *is* overridden, we'll just return an empty list to * indicate that we're not part of any list group. If you want to * provide your own listing group special to the state, simply * override this and speicfy the custom list group. */ specialDescListWith() { /* * if specialDesc is inherited from ActorState, then use the * default handling from the actor; otherwise, use no grouping at * all by default */ if (!overrides(self, ActorState, &specialDesc)) return getActor().actorListWith; else return []; } /* show the special description when we appear in a contents listing */ showSpecialDescInContents(actor, cont) { /* by default, just show our posture in our container */ getActor().listActorPosture(actor); } /* * Our "state" description. This shows information on what the actor * is *currently* doing; we display this after the static part of the * actor's description on EXAMINE <ACTOR>. By default, we add * nothing here, but state objects that represent scripted activies * should override this to describe their scripted activities. */ stateDesc = "" /* * Should we obey an action? If so, returns true; if not, displays * an appropriate response and returns nil. This will only be * called when the issuing actor is different from our actor, since * a command to oneself is implicitly always obeyed. */ obeyCommand(issuingActor, action) { /* * By default, we ignore all orders. We do need to generate a * response, though, so for this purpose, treat the order as a * conversational action, with the 'action' object as the topic. */ handleConversation(issuingActor, action, commandConvType); /* indicate that the order is refused */ return nil; } /* * Suggest topics for the given actor to talk to us about. This is * called when the given actor enters a TOPICS command (in which * case 'explicit' will be true) or enters a conversation with us * via TALK TO or the like (in which case 'explicit' will be nil). */ suggestTopicsFor(actor, explicit) { /* * if this is not an explicit TOPICS request, and we're not in * "auto suggest" mode, don't show anything - we don't want any * automatic suggestions in this mode */ if (!explicit && !autoSuggest) return; /* * show a paragraph break, in case we're being tacked on to * another report; but make it cosmetic, so that this by itself * doesn't suppress a default report, in case we don't end up * displaying any topics */ cosmeticSpacingReport('<.p>'); /* show our suggestion list */ showSuggestedTopicList(getSuggestedTopicList(), actor, getActor(), explicit); } /* * Get our suggested topic list. The suggested topic list consists * of the union of the current ConvNode's suggestion list, the * ActorState list, and the Actor's suggestion list. In each case, * the suggestion list is the list of all SuggestedTopic objects at * each database level. * * The suggestions are arranged in a hierarchy, and each hierarchy * level can prevent suggestions from a lower level from being * included. The top level of the hierarchy is the ConvNode; the * next level is the ActorState; and the last level is the Actor. * Suggestions are limited at each level with the 'limitSuggestions' * property: if true, suggestions from lower levels are not included. */ getSuggestedTopicList() { local v = new Vector(16); local node; local lst; /* add the actor's current conversation node topics */ if ((node = getActor().curConvNode) != nil) { /* if there are any suggested topics in the node, include them */ if ((lst = node.suggestedTopics) != nil) v.appendAll(lst); /* * if this ConvNode is marked as limiting suggestions to * those defined within the node, return what we have * without adding anything from the broader context */ if (node.limitSuggestions) return v; } /* add our own topics */ if ((lst = stateSuggestedTopics) != nil) v.appendAll(lst); /* * if the ActorState is limiting suggestions, don't include any * suggestions from the broader context (i.e., from the Actor * itself) */ if (limitSuggestions) return v; /* if our actor has its own list, add those as well */ if ((lst = getActor().suggestedTopics) != nil) v.appendAll(lst); /* return the combined list */ return v; } /* * get the topic suggestions for this state - by default, we just * return our own suggestedTopics list */ stateSuggestedTopics = (suggestedTopics) /* * Get my implied in-conversation state. This is used when our actor * initiates a conversation without specifying a particular * conversation state to enter (i.e., actor.initiateConversation() is * called with 'state' set to nil). By default, we don't have an * implied conversation state, so we just return 'self' to indicate * that we want to stay in the current state. States that are * coupled with separate in-conversation states, such as * ConversationReadyState, should return their associated * conversation states here. */ getImpliedConvState = (self) /* * General conversation handler. This can be used to process most * conversational commands - ASK, TELL, GIVE, SHOW, etc. The * standard sequence of processing is as follows: * * - If our actor has a non-nil current conversation node (ConvNode) * object, and the ConvNode wants to handle the event, let the * ConvNode handle it. * * - Otherwise, check our own topic database to see if we can find a * TopicEntry that matches the topic; if we can find one, let the * TopicEntry handle it. * * - Otherwise, let the actor handle it. * * 'otherActor' is the actor who originated the conversation command * (usually the player character). 'topic' is the subject being * discussed (the indirect object of ASK ABOUT, for example). * convType' is a ConvType describing the type of conversational * action we're performing. */ handleConversation(otherActor, topic, convType) { local actor = getActor(); local hasDefault; local node; local path; /* determine if I have a default response handler */ hasDefault = propDefined(convType.defaultResponseProp); /* * Figure the database search path for looking up the topics. * We'll start in the ConvNode database, then continue to the * ActorState database, then finally to the Actor database. * However, we won't reach the Actor database if there's a * default response handler in the state, because if we fail to * find it at the state, we'll take the default. * * Since the path we need to provide at each point is the * *remaining* path, don't bother including the ConvNode, since * we'd just have to take it right back out to get the remaining * path after the ConvNode. */ path = [self]; if (!hasDefault) path += actor; /* * If our actor has a current conversation node, check to see if * the conversation node wants to handle it. If not, check our * own topic database, then the actor's. */ if ((node = actor.curConvNode) == nil || !node.handleConversation(otherActor, topic, convType, path)) { /* get the remaining database search path */ path = path.sublist(2); /* * Either we don't have a ConvNode, or the ConvNode isn't * interested in handling the operation. Check to see if we * can handle it through our own topic database. */ if (!handleTopic(otherActor, topic, convType, path)) { /* * We couldn't find anything in our topic database that's * interested in handling it. Check to see if the state * object defines the default response handler method, * and use that as the response if so. */ if (hasDefault) { /* * the state object (i.e., self) does define the * default response method, so invoke that */ convType.defaultResponse(self, otherActor, topic); } else { /* * We don't have a topic database entry and we don't * have our own definition of the default response * handler. All that remains is to let our actor * handle it. */ actor.handleConversation(otherActor, topic, convType); } } } /* whatever happened, run the appropriate after-response handling */ convType.afterResponse(actor, otherActor); } /* * Receive notification that a TopicEntry is being used (via its * handleTopic method) to respond to a command. The TopicEntry will * call this before it shows its message or takes any other action. * By default, we do nothing. */ notifyTopicResponse(fromActor, entry) { } /* * Handle a before-action notification for our actor. By default, * we do nothing. */ beforeAction() { /* do nothing by default */ } /* handle an after-action notification for our actor */ afterAction() { } /* handle a before-travel notification */ beforeTravel(traveler, connector) { local other = getActor().getCurrentInterlocutor(); /* * if our conversational partner is departing, break off the * conversation */ if (connector != nil && other != nil && traveler.isActorTraveling(other)) { /* end the conversation */ if (!endConversation(gActor, endConvTravel)) { /* * they don't want to allow the conversation to end, so * abort the travel action */ exit; } } } /* handle an after-travel notification */ afterTravel(traveler, connector) { } /* * End the current conversation. 'reason' indicates why we're * leaving the conversation - this is one of the endConvXxx enums * defined in adv3.h. beforeTravel() calls this automatically when * the other party is trying to depart, and they're talking to us. * * This returns true if we wish to allow the conversation to end, * nil if not. */ endConversation(actor, reason) { local ourActor = getActor(); local node; /* tell the current ConvNode about it */ if ((node = ourActor.curConvNode) != nil) { local ret; /* the can-end call might show a response, so set our actor */ conversationManager.beginResponse(ourActor); /* ask the node if it's okay to end the conversation */ ret = node.canEndConversation(actor, reason); /* * If the result is blockEndConv, it means that the actor * said something to force the conversation to keep going. * Make a note that the other actor already said something on * this turn so that we don't generate another scripted * message later, and flag this as preventing the * conversation ending. */ if (ret == blockEndConv) { /* flag that the other actor said something this turn */ ourActor.noteConvAction(actor); /* we're unable to end the conversation now */ ret = nil; } /* end the response, leaving the node unchanged by default */ conversationManager.finishResponse( ourActor, ourActor.curConvNode); /* * if the node said no, tell the caller we can't end the * conversation right now */ if (!ret) return nil; /* tell the node we are indeed ending the conversation */ node.endConversation(actor, reason); } /* forget any conversation tree position */ ourActor.setConvNodeReason(nil, 'endConversation'); /* indicate that we are allowing the conversation to end */ return true; } /* * Take a turn. This is called when it's the actor's turn and * there's not something else the actor needs to be doing (such as * following another actor, or carrying out a command in the actor's * pending command queue). * * By default, we perform several steps automatically. * * First, we check to see if the actor is in a ConvNode. If so, the * ConvNode takes precedence. If we haven't been addressed already * in conversation on this turn, we'll let the ConvNode perform its * "continuation," which lets the NPC advance the conversation of its * own volition. In any case, if we have a current ConvNode, we're * done with the turn, since we assume the actor will want to proceed * with the conversation before pursuing its agenda or performing a * background action. * * Second, assuming there's no active ConvNode, we check for an * "agenda" item that's ready to execute. If we find one, we execute * it, and we're done. The agenda item takes precedence over any * other scripting we might have. * * Finally, if we also inherit from Script, and we didn't find an * active ConvNode or an agenda item that was ready to execute, we * invoke our doScript() method. This makes it especially easy to * define random background messages for the actor - just add an * EventList class (ShuffledEventList is usually the right one) to * the state's superclass list, and define a list of background * message strings. */ takeTurn() { local actor = getActor(); /* * Check to see if we want to continue a conversation. If so, * and we haven't already conversed this turn, try the * continuing conversation. If that displays anything, consider * the turn done. * * Otherwise, try executing an agenda item. If we do, consider * the turn done. * * Otherwise, if we're of class Script, execute our scripted * action. */ if (actor.curConvNode != nil && !actor.conversedThisTurn() && actor.curConvNode.npcContinueConversation()) { /* * we displayed an NPC-motivated conversation continuation, * so we're done with this turn */ } else if (actor.executeAgenda()) { /* we executed an agenda item, so we need do nothing more */ } else if (ofKind(Script)) { /* we're a Script, so invoke our scripted action */ doScript(); } } /* * Receive notification that we just followed another actor as part * of our programmed following behavior (in other words, due to our * 'followingActor' property, not due to an explicit FOLLOW command * directed to us). 'success' is true if we ended up in the actor's * location, nil if not. * * This can be used to update the actor's state after a 'follow' * operation occurs; for example, if the actor's state depends on * the actor's location, this can update the state accordingly. We * don't do anything by default. */ justFollowed(success) { /* do nothing by default */ } /* * Our group-travel arrival description. By default, when we perform * an accompanying travel with another actor as the lead actor, the * accompanying travel state will display this message instead of our * specialDesc when the lead actor first arrives in the new location. * We'll just display our own specialDesc by default, but this should * usually be overridden to say something specific to the group * travel arrival. The actual message is entirely dependent on the * nature of the group travel, which is why we don't provide a * special message by default. * * For scripted behavior, it's sometimes better to use arrivingTurn() * rather than this method to describe the behavior. * arrivingWithDesc() is called as part of the room description, so * it's best for any message shown here to fit well into the usual * room description format. For more complex transitions into the * new room state, arrivingTurn() is sometimes more appropriate, * since it runs like a daemon, after the arrival (and thus the new * room description) is completed. */ arrivingWithDesc() { specialDesc(); } /* * Perform any special action on a group-travel arrival. When group * travel is performed using the AccompanyingInTravelState class, * this is essentially called in lieu of the regular takeTurn() * method on the state that is coming into effect after the group * travel. (Not really, but effectively: the accompanying travel * state will still be in effect, so its takeTurn() method is what's * really called, but that method will call this method explicitly.) * By default, we do nothing. Since this runs on our turn, it's a * good place to put any scripted behavior we perform on arriving at * our new destination after the group travel. */ arrivingTurn() { } /* * For our TravelMessageHandler implementation, the nominal traveler * is our actor. Note that this is all we need to implement for * travel message handling, since we simply inherit the default * handling for all of the arrival/departure messages. */ getNominalTraveler() { return getActor(); } ; /* * A "ready for conversation" state. This can be used as the base class * for actor states when the actor is receptive to conversation, and we * want to have the sense of a conversational context. The key feature * that this class provides is the ability to provide messages when * engaging and disengaging the conversation. * * Note that this state is NOT required for conversation, since the basic * ActorState object accepts conversational commands like ASK, TELL, * GIVE, and TAKE. The special feature of the "conversation ready" state * is that we explicitly move the actor to a separate state when * conversation begins. This is especially appropriate for states in * which the NPC is actively carrying on some other activity; the * conversation should interrupt those states, so that the actor stops * the other activity and gives us its full attention. * * This type of state can be associated with its in-conversation state * object in one of two ways. First, the inConvState property can be * explicitly set to point to the in-conversation state object. Second, * this object can be nested inside its in-conversation state object via * the 'location' property (so you can use the '+' syntax to put this * object inside its in-conversation state object). The 'ready' object * goes inside the 'conversing' object because a single 'conversing' * object can frequently be shared among several 'ready' states. */ class ConversationReadyState: ActorState /* * The associated in-conversation state. This should be set to an * InConversationState object that controls the actor's behavior * while carrying on a conversation. Note that the library will * automatically set this if the instance is nested (via its * 'location' property) inside an InConversationState object. */ inConvState = nil /* my implied conversational state is my in-conversation state */ getImpliedConvState = (inConvState) /* * Show our greeting message. If 'explicit' is true, it means that * the player character is greeting us through an explicit greeting * command, such as HELLO or TALK TO. Otherwise, the greeting is * implied by some other conversational action, such a ASK ABOUT or * SHOW TO. We do nothing by default; this should be overridden in * most cases to show some sort of exchange of pleasantries - * something like this: * *. >bob, hello *. "Hi, there," you say. * * Bob looks up over his newspaper. "Oh, hello," he says, putting * down the paper. "What can I do for you?" * * Note that games shouldn't usually override this method. Instead, * you should simply create a HelloTopic entry and put it inside the * state object; we'll find the HelloTopic and show its message as * our greeting. * * If you want to distinguish between explicit and implicit * greetings, you can create an ImpHelloTopic entry for implied * greetings (i.e., the kind of greeting that occurs automatically * when the player jumps right into a conversation with our actor * using ASK ABOUT or the like, without explicitly saying HELLO * first). The regular HelloTopic will handle explicit greetings, * and the ImpHelloTopic will handle the implied kind. */ showGreetingMsg(actor, explicit) { /* look for a HelloTopic in our topic database */ if (handleTopic(actor, explicit ? helloTopicObj : impHelloTopicObj, helloConvType, nil)) "<.p>"; } /* * Enter this state from a conversation. This should show any * message we want to display when we're ending a conversation and * switching from the conversation to this state. 'reason' is the * endConvXxx enum indicating what triggered the termination of the * conversation. 'oldNode' is the ConvNode we were in just before we * initiated the termination - we need this information because we * want to look in the ConvNode for a Bye topic message to display, * but we can't just look in the actor for the node because it will * already have been cleared out by the time we get here. * * Games shouldn't normally override this method. Instead, simply * create a ByeTopic entry and put it inside the state object; we'll * find the ByeTopic and show its message for the goodbye. * * If you want to distinguish between different types of goodbyes, * you can create an ImpByeTopic for any implied goodbye (i.e., the * kind where the other actor just walks away, or where we get bored * of the other actor ignoring us). You can also further * differentiate by creating BoredByeTopic and/or LeaveByeTopic * objects to handle just those cases. The regular ByeTopic will * handle explicit GOODBYE commands, and the others (ImpByeTopic, * BoredByeTopic, LeaveByeTopic) will handle the implied kinds. */ enterFromConversation(actor, reason, oldNode) { local topic; local reasonMap = [endConvBye, byeTopicObj, endConvTravel, leaveByeTopicObj, endConvBoredom, boredByeTopicObj, endConvActor, actorByeTopicObj]; /* figure out which topic object we need, based on the reason code */ topic = reasonMap[reasonMap.indexOf(reason) + 1]; /* * Look for a ByeTopic in the ConvNode; failing that, try our own * database. */ if (oldNode == nil || !oldNode.handleConversation(actor, topic, byeConvType, nil)) { /* there's no node handler; try our own database */ handleTopic(actor, topic, byeConvType, nil); } } /* handle a conversational action directed to our actor */ handleConversation(otherActor, topic, convType) { /* * If this is a greeting, handle it ourselves. Otherwise, pass * it along to our associated in-conversation state. */ if (convType == helloConvType) { /* * Switch to our associated in-conversation state and show a * greeting. Since we're explicitly entering the * conversation, we have no topic entry. */ enterConversation(otherActor, nil); /* show or schedule a topic inventory, as appropriate */ conversationManager.showOrScheduleTopicInventory( getActor(), otherActor); } else { /* * it's not a greeting, so pass it to our in-conversation * state for handling */ inConvState.handleConversation(otherActor, topic, convType); } } /* * Initiate conversation based on the given simulation object. This * is an internal method that isn't usually called directly from game * code; game code usually calls the Actor's initiateTopic(), which * calls this routine to check for a topic that's part of the state * object. */ initiateTopic(obj) { /* defer to our in-conversation state */ return inConvState.initiateTopic(obj); } /* * Receive notification that a TopicEntry is being used (via its * handleTopic method) to respond to a command. If the TopicEntry is * conversational, automatically enter our in-conversation state. */ notifyTopicResponse(fromActor, entry) { if (entry.isConversational) enterConversation(fromActor, entry); } /* * Enter a conversation with the given actor, either explicitly (via * HELLO or TALK TO) or implicitly (by directly asking a question, * etc). 'entry' gives the TopicEntry that's triggering the implicit * conversation entry; if this is nil, it means that we're being * triggered explicitly. */ enterConversation(actor, entry) { local myActor = getActor(); local explicit = (entry == nil); /* if the actor can't talk to us, we can't enter the conversation */ if (!actor.canTalkTo(myActor)) { /* tell them we can't talk now */ reportFailure(&objCannotHearActorMsg, myActor); /* terminate the command */ exit; } /* * Show our greeting, if desired. We show a greeting if we're * being invoked explicitly (that is, there's no TopicEntry), or * if we're being invoked explicitly and the TopicEntry implies a * greeting. */ if (explicit || entry.impliesGreeting) showGreetingMsg(actor, explicit); /* activate the in-conversation state */ myActor.setCurState(inConvState); } /* * Get this state's suggested topic list. ConversationReady states * shouldn't normally have topic entries of their own, since a * ConvversationReady state usually forwards conversation handling * to its corresponding in-conversation state. So, simply return * the suggestion list from our in-conversation state object. */ stateSuggestedTopics = (inConvState.suggestedTopics) /* initialize the actor state object */ initializeActorState() { /* inherit the default handling */ inherited(); /* * if we're nested inside an in-conversation state object, the * containing in-conversation state is the one we'll use for * conversations */ if (location.ofKind(InConversationState)) inConvState = location; } ; /* * The "in-conversation" state. This works with ConversationReadyState * to handle transitions in and out of conversations. In this state, we * are actively engaged in a conversation. * * Throughout this implementation, we assume that we only care about * conversations with a single character, specifically the player * character. There's generally no good reason to fully model * conversations between NPC's, since that kind of NPC activity is in * most cases purely pre-scripted and thus requires no special state * tracking. Since we generally only need to worry about tracking a * conversation with the player character, we don't bother with the * possibility that we're simultaneously in conversation with more than * one other character. */ class InConversationState: ActorState /* * Our attention span, in turns. This is the number of turns that * we'll be willing to stay in the conversation while the other * character is ignoring us. After the conversation has been idle * this long, we'll assume the other actor is no longer talking to * us, so we'll terminate the conversation ourselves. * * If the NPC's doesn't have a limited attention span, set this * property to nil. This will prevent the NPC from ever disengaging * of its own volition. */ attentionSpan = 4 /* * The state to switch to when the conversation ends. Instances can * override this to select the next state. By default, we'll return * to the state that we were in immediately before the conversation * started. */ nextState = (previousState) /* * End the current conversation. 'reason' indicates why we're * leaving the conversation - this is one of the endConvXxx enums * defined in adv3.h. * * This method is a convenience only; you aren't required to call * this method to end the conversation, since you can simply switch * to another actor state directly if you prefer. This method's * main purpose is to display an appropriate message terminating the * conversation while switching to the new state. If you want to * display your own message directly from the code that's changing * the state, there's no reason to call this. * * This returns true if we wish to allow the conversation to end, * nil if not. */ endConversation(actor, reason) { local nxt; local myActor = getActor(); /* * note the current ConvNode for our actor - when we check with * the ConvNode to see about ending the conversation, this will * automatically exit the ConvNode, so we need to save this first * so that we can refer to it later to check for a Bye topic */ local oldNode = myActor.curConvNode; /* * Inherit the base behavior first - if it disallows the action, * return failure. The inherited version will check with the * current ConvNode to see if has any objection. */ if (!inherited(actor, reason)) return nil; /* get the next state */ nxt = nextState; /* if there isn't one, stay in the actor's current state */ if (nxt == nil) nxt = myActor.curState; /* * If the next state is a 'conversation ready' state, tell it * we're entering from a conversation. We're ending the * conversation explicitly only if 'reason' is endConvBye. Pass * along the ConvNode we just exited (if any), so that we can * look for a response in the node. */ if (nxt.ofKind(ConversationReadyState)) nxt.enterFromConversation(actor, reason, oldNode); /* switch our actor to the next state */ myActor.setCurState(nxt); /* indicate that we are allowing the conversation to end */ return true; } /* handle a conversational command */ handleConversation(otherActor, topic, convType) { /* handle goodbyes specially */ if (convType == byeConvType) { /* * If this is an implicit goodbye, run the normal * conversation handling in order to display any implied * ByeTopic message - but capture the output in case we * decide not to end the conversation after all. Only do * this in the case of an implicit goodbye, though - for an * explicit goodbye, there's no need for this as the explicit * BYE will do the same thing on its own. */ local txt = nil; if (topic != byeTopicObj) { txt = mainOutputStream.captureOutput( {: inherited(otherActor, topic, convType) }); } /* * try to end the conversation; if we won't allow it, * terminate the action here */ if (!endConversation(otherActor, endConvBye)) exit; /* show the captured ByeTopic output */ if (txt != nil) say(txt); } else { /* use the inherited handling */ inherited(otherActor, topic, convType); } } /* * provide a default HELLO response, if we don't have a special * TopicEntry for it */ defaultGreetingResponse(actor) { /* * As our default response, point out that we're already at the * actor's service. (This isn't an error, because the other * actor might not have been talking to us, even though we * thought we were talking to them.) */ gLibMessages.alreadyTalkingTo(getActor(), actor); } takeTurn() { local actor = getActor(); /* if we didn't interact this turn, increment our boredom counter */ if (!actor.conversedThisTurn()) actor.boredomCount++; /* run the inherited handling */ inherited(); } /* activate this state */ activateState(actor, oldState) { /* * If the previous state was a ConversationReadyState, or we * have no other state remembered, remember the previous state - * this is the default we'll return to at the end of the * conversation, if the instance doesn't specify another state. * * We don't remember prior states that aren't conv-ready states * to make it easier to temporarily interrupt a conversation * with some other state, and later return to the conversation. * If we remembered every prior state, then we'd return to the * interrupting state when the conversation ended, which is * usually not what's wanted. Usually, we want to return to the * last conv-ready state when a conversation ends, ignoring any * other intermediate states that have been active since the * conv-ready state was last in effect. */ if (previousState == nil || oldState.ofKind(ConversationReadyState)) previousState = oldState; /* * reset the actor's boredom counter, since we're just starting a * new conversation, and add our boredom agenda item to the * active list to monitor our boredom level */ actor.boredomCount = 0; actor.addToAgenda(actor.boredomAgendaItem); /* remember the time of the last conversation command */ actor.lastConvTime = Schedulable.gameClockTime; } /* deactivate this state */ deactivateState(actor, newState) { /* * we're leaving the conversation state, so there's no need to * monitor our boredom level any longer */ actor.removeFromAgenda(actor.boredomAgendaItem); /* do the normal work */ inherited(actor, newState); } /* * The previous state - this is the state we were in before the * conversation began, and the one we'll return to by default when * the conversation ends. We'll set this automatically on * activation. */ previousState = nil ; /* * A special kind of agenda item for monitoring "boredom" during a * conversation. We check to see if our actor is in a conversation, and * the PC has been ignoring the conversation for too long; if so, our * actor initiates the end of the conversation, since the PC apparently * isn't paying any attention to us. */ class BoredomAgendaItem: AgendaItem /* we construct these dynamically during actor initialization */ construct(actor) { /* remember our actor as our location */ location = actor; } /* * we're ready to run if our actor is in an InConversationState and * its boredom count has reached the limit for the state */ isReady() { local actor = getActor(); local state = actor.curState; return (inherited() && state.ofKind(InConversationState) && state.attentionSpan != nil && actor.boredomCount >= state.attentionSpan); } /* on invocation, end the conversation */ invokeItem() { local actor = getActor(); local state = actor.curState; /* tell the state to end the conversation */ state.endConversation(actor.getCurrentInterlocutor(), endConvBoredom); } /* * by default, handle boredom before other agenda items - we do this * because an ongoing conversation will be the first thing on the * NPC's mind */ agendaOrder = 50 ; /* * A "hermit" actor state is a state where the actor is unresponsive to * conversational overtures (ASK ABOUT, TELL ABOUT, HELLO, GOODBYE, YES, * NO, SHOW TO, GIVE TO, and any orders directed to the actor). Any * attempt at conversation will be met with the 'noResponse' message. */ class HermitActorState: ActorState /* * Show our response to any conversational command. We'll simply * show the standard "there's no response" message by default, but * subclasses can (and usually should) override this to explain * what's really going on. Note that this routine will be invoked * for any sort of conversation command, so any override needs to be * generic enough that it's equally good for ASK, TELL, and * everything else. * * Note that it's fairly easy to create a shuffled list of random * messages, if you want to add some variety to the actor's * responses. To do this, use an embedded ShuffledEventList: * * myState: HermitActorState *. noResponse() { myList.doScript(); } *. myList: ShuffledEventList { *. ['message1', 'message2', 'message3'] } *. ; */ noResponse() { mainReport(&noResponseFromMsg, getActor()); } /* all conversation actions get the same default response */ handleConversation(otherActor, topic, convType) { /* just show our standard default response */ noResponse(); } /* * Since the hermit state blocks topics from outside the state, don't * offer suggestions for other topics while in this state. * * Note that you might sometimes want to override this to allow the * usual topic suggestions (by setting this to nil). In particular: * * - If it's not outwardly obvious that the actor is unresponsive, * you'll probably want to allow suggestions. Remember, TOPICS * suggests topics that the *PC* wants to talk about, not things the * NPC is interested in. If the PC doesn't necessarily know that the * NPC won't respond, the PC would still want to ask about those * topics. * * - If the hermit state is to be short-lived, you might want to show * the topic suggestions even in the hermit state, so that the player * is aware that there are still useful topics to explore with the * NPC. The player might otherwise assume that the NPC is out of * useful topics, and not bother trying again later when the NPC * becomes more responsive. */ limitSuggestions = true ; /* * The basic "accompanying" state. In this state, whenever the actor * we're accompanying travels to a location we want to follow, we'll * travel at the same time with the other actor. */ class AccompanyingState: ActorState /* * Check to see if we are to accompany the given traveler on the * given travel. 'traveler' is the Traveler performing the travel, * and 'conn' is the connector that the traveler is about to take. * * Note that 'traveler' is a Traveler object. This will simply be an * Actor (which is a kind of Traveler) when the actor is performing * the travel directly, but it could also be another kind of * Traveler, such as a Vehicle. This routine must determine whether * to accompany other kinds of actors. * * By default, we'll return true to indicate that we want to * accompany any traveler anywhere they go. This should almost * always be overridden in practice to be more specific. */ accompanyTravel(traveler, conn) { return true; } /* * Get our accompanying state object. We'll create a basic * accompanying in-travel state object, returning to the current * state when we're done. 'traveler' is the Traveler object that's * performing the travel; this might be an Actor, but could also be a * Vehicle or other Traveler subclass. */ getAccompanyingTravelState(traveler, connector) { /* * Create the default intermediate state for the travel. Note * that the lead actor is the actor performing the command - this * won't necessarily be the traveler, since the actor could be * steering a vehicle. */ return new AccompanyingInTravelState( getActor(), gActor, getActor().curState); } /* * handle a before-travel notification for my actor */ beforeTravel(traveler, connector) { /* * If we want to accompany the given traveler on this travel, add * ourselves to the initiating actor's list of accompanying * actors. Never set an actor to accompany itself, since doing * so would lead to infinite recursion. */ if (accompanyTravel(traveler, connector) && getActor() != gActor) { /* * Add me to the list of actors accompanying the actor * initiating the travel - that actor will run a nested * travel action on us before doing its own travel. Note * that the initiating actor is gActor, since that's the * actor performing the action that led to the travel. */ gActor.addAccompanyingActor(getActor()); /* put my actor into the appropriate new group travel state */ getActor().setCurState( getAccompanyingTravelState(traveler, connector)); } /* inherit the default handling */ inherited(traveler, connector); } ; /* * "Accompanying in-travel" state - this is an actor state used when an * actor is taking part in a group travel operation. This state lasts * only as long as the single turn - which belongs to the lead actor - * that it takes to carry out the group travel. Once our turn comes * around, we'll restore the actor to the previous state - or, we can set * the actor to a different state, if desired. Setting the actor to a * different state is useful when the group travel triggers a new * scripted activity in the new room. */ class AccompanyingInTravelState: ActorState construct(actor, lead, next) { /* do the normal initialization */ inherited(actor); /* remember the lead actor and the next state */ leadActor = lead; nextState = next; } /* the lead actor of the group travel */ leadActor = nil /* * the next state - we'll switch our actor to this state after the * travel has been completed */ nextState = nil /* * Show our "I am here" description. By default, we'll use the * arrivingWithDesc of the *next* state object. */ specialDesc() { nextState.arrivingWithDesc; } /* take our turn */ takeTurn() { /* * The group travel only takes the single turn in which the * travel is initiated, so by the time our turn comes around, the * group travel is done. Clear out the lead actor's linkage to * us as an accompanying actor. */ leadActor.accompanyingActors.removeElement(getActor()); /* switch our actor to the next state */ getActor().setCurState(nextState); /* * call our next state's on-arrival turn-taking method, so that * it can carry out any desired scripted behavior for our arrival */ nextState.arrivingTurn(); } /* initiate a topic - defer to the next state */ initiateTopic(obj) { return nextState.initiateTopic(obj); } /* * Override our departure messages. When we're accompanying another * actor on a group travel, the lead actor will, as part of its turn, * send each accompanying actor (including us) on ahead. This means * that the lead actor will see us departing from the starting * location, because we'll leave before the lead actor has itself * departed. Rather than using the normal "Bob leaves to the west" * departure report, customize the departure reports to indicate * specifically that we're going with the lead actor. (Note that we * only have to handle the departing messages, since group travel * always sends accompanying actors on ahead of the main actor, hence * the accompanying actors will always be seen departing, not * arriving.) * * Note that all of these call our generic sayDeparting() method by * default, so a subclass can catch all of the departure types at * once just by overriding sayDeparting(). Overriding the individual * methods is still desirable, of course, if you want separate * messages for the different departure types. */ sayDeparting(conn) { gLibMessages.sayDepartingWith(getActor(), leadActor); } sayDepartingDir(dir, conn) { sayDeparting(conn); } sayDepartingThroughPassage(conn) { sayDeparting(conn); } sayDepartingViaPath(conn) { sayDeparting(conn); } sayDepartingUpStairs(conn) { sayDeparting(conn); } sayDepartingDownStairs(conn) { sayDeparting(conn); } /* * Describe local travel using our standard departure message as * well. This is used to describe our travel when our origin and * destination locations are both visible to the PC; in these cases, * we don't describe the departure separately because the whole * process of travel from departure to arrival is visible to the PC * and thus is best handled with a single message, which we generate * here. In our case, since the "accompanying" state describes even * normal travel as though it were visible all along, we can use our * standard "departing" message to describe local travel as well. */ sayArrivingLocally(dest, conn) { sayDeparting(conn); } sayDepartingLocally(dest, conn) { sayDeparting(conn); } ; /* ------------------------------------------------------------------------ */ /* * A pending conversation information object. An Actor keeps a list of * these for pending conversations. */ class PendingConvInfo: object construct(state, node, turns) { /* remember how to start the conversation */ state_ = state; node_ = node; /* compute the game clock time when we can start the conversation */ time_ = Schedulable.gameClockTime + turns; } /* * our ActorState and ConvNode (or ConvNode name string), describing * how we're to start the conversation */ state_ = nil node_ = nil /* the minimum game clock time at which we can start the conversation */ time_ = nil ; /* ------------------------------------------------------------------------ */ /* * An "agenda item." Each actor can have its own "agenda," which is a * list of these items. Each item represents an action that the actor * wants to perform - this is usually a goal the actor wants to achieve, * or a conversational topic the actor wants to pursue. * * On any given turn, an actor can carry out only one agenda item. * * Agenda items are a convenient way of controlling complex behavior. * Each agenda item defines its own condition for when the actor can * pursue the item, and each item defines what the actor does when * pursuing the item. Agenda items can improve the code structure for an * NPC's behavior, since they nicely isolate a single background action * and group it with the conditions that trigger it. But the main * benefit of agenda items is the one-per-turn pacing - by executing at * most one agenda item per turn, we ensure that the NPC will carry out * its self-initiated actions at a measured pace, rather than as a jumble * of random actions on a single turn. * * Note that NPC-initiated conversation messages override agendas. If an * actor has an active ConvNode, AND the ConvNode displays a * "continuation message" on a given turn, then the actor will not pursue * its agenda on that turn. In this way, ConvNode continuation messages * act rather like high-priority agenda items. */ class AgendaItem: object /* * My actor - agenda items should be nested within the actor using * '+' so that we can find our actor. Note that this doesn't add the * item to the actor's agenda - that has to be done explicitly with * actor.addToAgenda(). */ getActor() { return location; } /* * Is this item active at the start of the game? Override this to * true to make the item initially active; we'll add it to the * actor's agenda during the game's initialization. */ initiallyActive = nil /* * Is this item ready to execute? The actor will only execute an * agenda item when this condition is met. By default, we're ready * to execute. Items can override this to provide a declarative * condition of readiness if desired. */ isReady = true /* * Is this item done? On each turn, we'll remove any items marked as * done from the actor's agenda list. We remove items marked as done * before executing any items, so done-ness overrides readiness; in * other words, if an item is both 'done' and 'ready', it'll simply * be removed from the list and will not be executed. * * By default, we simply return nil. Items can override this to * provide a declarative condition of done-ness, or they can simply * set the property to true when they finish their work. For * example, an item that only needs to execute once can simply set * isDone to true in its invokeItem() method; an item that's to be * repeated until some success condition obtains can override isDone * to return the success condition. */ isDone = nil /* * The ordering of the item relative to other agenda items. When we * choose an agenda item to execute, we always choose the lowest * numbered item that's ready to run. You can leave this with the * default value if you don't care about the order. */ agendaOrder = 100 /* * Execute this item. This is invoked during the actor's turn when * the item is the first item that's ready to execute in the actor's * agenda list. We do nothing by default. */ invokeItem() { } /* * Reset the item. This is invoked whenever the item is added to an * actor's agenda. By default, we'll set isDone to nil as long as * isDone isn't a method; this makes it easier to reuse agenda * items, since we don't have to worry about clearing out the isDone * flag when reusing an item. */ resetItem() { /* if isDone isn't a method, reset it to nil */ if (propType(&isDone) != TypeCode) isDone = nil; } ; /* * An AgendaItem initializer. For each agenda item that's initially * active, we'll add the item to its actor's agenda. */ PreinitObject execute() { forEachInstance(AgendaItem, function(item) { /* * If this item is initially active, add the item to its * actor's agenda. */ if (item.initiallyActive) item.getActor().addToAgenda(item); }); } ; /* * A "conversational" agenda item. This type of item is ready to execute * only when the actor hasn't engaged in conversation during the same * turn. This type of item is ideal for situations where we want the * actor to pursue a conversational topic, because we won't initiate the * action until we get a turn where the player didn't directly talk to * us. */ class ConvAgendaItem: AgendaItem isReady = (!getActor().conversedThisTurn() && getActor().canTalkTo(otherActor) && inherited()) /* * The actor we're planning to address - by default, this is the PC. * If the conversational overture will be directed to another NPC, * you can specify that other actor here. */ otherActor = (gPlayerChar) ; /* * A delayed agenda item. This type of item becomes ready to execute * when the game clock reaches a given turn counter. */ class DelayedAgendaItem: AgendaItem /* we're ready if the game clock time has reached our ready time */ isReady = (Schedulable.gameClockTime >= readyTime && inherited()) /* the turn counter on the game clock when we become ready */ readyTime = 0 /* * Set our ready time based on a delay from the current time. We'll * become ready after the given number of turns elapses. For * convenience, we return 'self', so a delayed agenda item can be * initialized and added to an actor's agenda in one simple * operation, like so: * * actor.addToAgenda(item.setDelay(1)); */ setDelay(turns) { /* * initialize our ready time as the given number of turns in the * future from the current game clock time */ readyTime = Schedulable.gameClockTime + turns; /* return 'self' for the caller's convenience */ return self; } ; /* ------------------------------------------------------------------------ */ /* * An Actor is a living person, animal, or other entity with a will of * its own. Actors can usually be addressed with targeted commands * ("bob, go north"), and with commands like ASK ABOUT, TELL ABOUT, GIVE * TO, and SHOW TO. * * Note that, by default, an Actor can be picked up and moved with * commands like TAKE, PUT IN, and so on. This is suitable for some * kinds of actors but not for others: it might make sense with a cat or * a small dog, but not with a bank guard or an orc. For an actor that * can't be taken, use the UntakeableActor or one of its subclasses. * * An actor's contents are the things the actor is carrying or wearing. */ class Actor: Thing, Schedulable, Traveler, ActorTopicDatabase /* flag: we're an actor */ isActor = true /* * Our current state. This is an ActorState object representing what * we're currently doing. Whenever the actor changes to a new state * (for example, because of a scripted activity), this can be changed * to reflect the actor's new state. The state object groups the * parts of the actor's description and other methods that tend to * vary according to what the actor's doing; it's easier to keep * everything related to scripted activities together in a state * object than it is to handle all of the variability with switch() * statements of the like in methods directly in the actor. * * It's not necessary to initialize this if the actor doesn't take * advantage of the ActorState mechanism. If this isn't initialized * for a particular actor, we'll automatically create a default * ActorState object during pre-initialization. */ curState = nil /* set the current state */ setCurState(state) { /* if this isn't a change of state, there's nothing to do */ if (state == curState) return; /* if we have a previous state, tell it it's becoming inactive */ if (curState != nil) curState.deactivateState(self, state); /* notify the new state it's becoming active */ if (state != nil) state.activateState(self, curState); /* remember the new state */ curState = state; } /* * Our current conversation node. This is a ConvNode object that * keeps track of the flow of the conversation. */ curConvNode = nil /* * Our table of conversation nodes. At initialization, the * conversation manager scans all ConvNode instances and adds each * one to its actor's table. This table is keyed by the name of * node, and the value for each entry is the ConvNode object - this * lets us look up the ConvNode object by name. Because each actor * has its own lookup table, ConvNode names only have to be unique * within the actor's set of ConvNodes. */ convNodeTab = perInstance(new LookupTable(32, 32)) /* set the current conversation node */ setConvNode(node) { setConvNodeReason(node, nil); } /* set the current conversation node, with a reason code */ setConvNodeReason(node, reason) { /* remember the old node */ local oldNode = curConvNode; /* if the node was specified by name, look up the object */ if (dataType(node) == TypeSString) node = convNodeTab[node]; /* remember the new node */ curConvNode = node; /* * If we're changing to a new node, notify the new and old * nodes. Note that these notifications occur after the new * node has been set, which ensures that any further node change * triggered by the node change won't redundantly issue the same * notifications: since the old node is no longer active, it * can't receive another departure notification, and since the * new node is already active, it can't receive another * activation. */ if (node != oldNode) { /* if there's an old node, note that we're leaving it */ if (oldNode != nil) oldNode.noteLeaving(); /* let the node know that it's becoming active */ if (node != nil) node.noteActiveReason(reason); } /* * note that we've explicitly set a ConvNode (even if it's not * actually changing), in case the conversation manager is * tracking what's happening during a response */ responseSetConvNode = true; } /* * conversation manager ID - this is assigned by the conversation * manager to map to and from output stream references to the actor; * this is only for internal use by the conversation manager */ convMgrID = nil /* * Flag indicating whether or not we've set a ConvNode in the course * of the current response. This is for use by the converstaion * manager. */ responseSetConvNode = nil /* * Initiate a conversation with the player character. This lets the * NPC initiate a conversation, in response to something the player * character does, or as part of the NPC's scripted activity. This * is only be used for situations where the NPC initiates the * conversation - if the player character initiates conversation with * TALK TO, ASK, TELL, etc., we handle the conversation through our * normal handlers for those commands. * * 'state' is the ActorState to switch to for the conversation. This * will normally be an InConversationState object, but doesn't have * to be. * * You can pass nil for 'state' to use the current state's implied * conversational state. The implied conversational state of a * ConversationReadyState is the associated InConversationState; the * implied conversation state of any other state is simply the same * state. * * 'node' is a ConvNode object, or a string naming a ConvNode object. * We'll make this our current conversation node. A valid * conversation node is required because we use this to generate the * initial NPC greeting of the conversation. In most cases, when the * NPC initiates a conversation, it's because the NPC wants to ask a * question or otherwise say something specific, so there should * always be a conversational context implied, thus the need for a * ConvNode. If there's no need for a conversational context, the * NPC script code might just as well display the conversational * exchange as a plain old message, and not bother going to all this * trouble. */ initiateConversation(state, node) { /* * if there's no state provided, use the current state's implied * conversation state */ if (state == nil) state = curState.getImpliedConvState; /* * if there's an ActorHelloTopic for the old state, invoke it to * show the greeting */ curState.handleTopic(self, actorHelloTopicObj, helloConvType, nil); /* switch to the new state, if it's not the current state */ if (state != nil && state != curState) setCurState(state); /* we're now talking to the player character */ noteConversation(gPlayerChar); /* switch to the conversation node */ setConvNodeReason(node, 'initiateConversation'); /* tell the conversation node that the NPC is initiating it */ if (node != nil) curConvNode.npcInitiateConversation(); } /* * Initiate a conversation based on the given simulation object. * We'll look for an InitiateTopic matching the given object, and if * we can find one, we'll show its topic response. */ initiateTopic(obj) { /* try our current state first */ if (curState.initiateTopic(obj)) return true; /* we didn't find a state object; use the default handling */ return inherited(obj); } /* * Schedule initiation of conversation. This allows the caller to * set up a conversation to start on a future turn. The * conversation will start after (1) the given number of turns has * elapsed, and (2) the player didn't target this actor with a * conversational command on the same turn. This allows us to set * the NPC so that it *wants* to start a conversation, and will do * so as soon as it has a chance to get a word in. * * If 'turns' is zero, the conversation can start the next time the * actor takes a turn; so, if this is called during the PC's action * processing, the conversation can start on the same turn. Note * that if this is called during the actor's takeTurn() processing, * it won't actually start the conversation until the next turn, * because that's the next time we'll check the queue. If 'turns' * is 1, then the player will get at least one more command before * the conversation will begin, and so on with higher numbers. */ scheduleInitiateConversation(state, node, turns) { /* add a new pending conversation to our list */ pendingConv.append(new PendingConvInfo(state, node, turns)); } /* * Break off our current conversation, of the NPC's own volition. * This is the opposite number of initiateConversation: this causes * the NPC to effectively say BYE on its own, rather than waiting * for the PC to decide to end the conversation. * * This call is mostly useful when the actor's current state is an * InConversationState, since the main function of this routine is * to switch to an out-of-conversation state. */ endConversation() { /* * tell the current state to end the conversation of the NPC's * own volition */ curState.endConversation(self, endConvActor); } /* * Our list of pending conversation initiators. In our takeTurn() * processing, we'll check this list for conversations that we can * initiate. */ pendingConv = nil /* * Hide actors from 'all' by default. The kinds of actions that * normally apply to 'all' and the kinds that normally apply to * actors have pretty low overlap. * * If a particular actor looks a lot like an inanimate object, it * might want to override this to participate in 'all' for most or * all actions. */ hideFromAll(action) { return true; } /* * don't hide actors from defaulting, though - it's frequently * convenient and appropriate to assume an actor by default, * especially for commands like GIVE TO and SHOW TO */ hideFromDefault(action) { return nil; } /* * We meet the objHeld precondition for ourself - that is, for any * verb that requires holding an object, we can be considered to be * holding ourself. */ meetsObjHeld(actor) { return actor == self || inherited(actor); } /* * Actors are not listed with the ordinary objects in a room's * description. However, an actor is listed as part of an inventory * description. */ isListed = nil isListedInContents = nil isListedInInventory = true /* the contents of an actor aren't listed in a room's description */ contentsListed = nil /* * Full description. By default, we'll show either the pcDesc or * npcDesc, depending on whether we're the current player character * or a non-player character. * * Generally, individual actors should NOT override this method. * Instead, customize pcDesc and/or npcDesc to describe the permanent * features of the actor. */ desc { /* * show the appropriate messages, depending on whether we're the * player character or a non-player character */ if (isPlayerChar()) { /* show our as-player-character description */ pcDesc; } else { /* show our as-non-player-character description */ npcDesc; } } /* show our status */ examineStatus() { /* * If I'm an NPC, show where I'm sitting/standing/etc. (If I'm * the PC, we don't usually want to show this explicitly to avoid * redundancy. The player is usually sufficiently aware of the * PC's posture by virtue of being in control of the actor, and * the information also tends to show up often enough in other * places, such as on the status line and in the room * description.) */ if (!isPlayerChar()) postureDesc; /* show the status from our state object */ curState.stateDesc; /* inherit the default handling to show our contents */ inherited(); } /* * Show my posture, as part of the full EXAMINE description of this * actor. We'll let our nominal actor container handle it. */ postureDesc() { descViaActorContainer(&roomActorPostureDesc, nil); } /* * The default description when we examine this actor and the actor * is serving as the player character. This should generally not * include any temporary status information; just show constant, * fixed features. */ pcDesc { gLibMessages.pcDesc(self); } /* * Show the description of this actor when this actor is a non-player * character. * * This description should include only the constant, fixed * description of the character. Do not include information on what * the actor is doing right now, because that belongs in the * ActorState object instead. When we display the actor's * description, we'll show this text, and then we'll show the * ActorState description as well; this combination approach makes it * easier to keep the description synchronized with any scripted * activities the actor is performing. * * By default, we'll show this as a "default descriptive report," * since it simply says that there's nothing special to say. * However, whenever this is overridden with an actual description, * you shouldn't bother to use defaultDescReport - simply display the * descriptive message directly: * * npcDesc = "He's wearing a gorilla costume. " */ npcDesc { defaultDescReport(&npcDescMsg, self); } /* examine my contents specially */ examineListContents() { /* if I'm not the player character, show my inventory */ if (!isPlayerChar()) holdingDesc; } /* * Always list actors specially, rather than as ordinary items in * contents listings. We'll send this to our current state object * for processing, since our "I am here" description tends to vary by * state. */ specialDesc() { curState.specialDesc(); } distantSpecialDesc() { curState.distantSpecialDesc(); } remoteSpecialDesc(actor) { curState.remoteSpecialDesc(actor); } specialDescListWith() { return curState.specialDescListWith(); } /* * By default, show the special description for an actor in the group * of special descriptions that come *after* the room's portable * contents listing. An actor's presence is usually a dynamic * feature of a room, and so we don't want to suggest that the actor * is a permanent feature of the room by describing the actor * directly with the room's main description. */ specialDescBeforeContents = nil /* * When we're asked to show a special description as part of the * description of a containing object (which will usually be a nested * room of some kind), just show our posture in our container, rather * than showing our full "I am here" description. */ showSpecialDescInContents(actor, cont) { /* show our posture to indicate our container */ listActorPosture(actor); } /* * By default, put all of the actor special descriptions after the * special descriptions of ordinary objects, by giving actors a * higher listing order value. */ specialDescOrder = 200 /* * Get my listing group for my special description as part of a room * description. By default, we'll let our immediate location decide * how we're grouped. */ actorListWith() { local group; /* * if our special desc is overridden, don't use any grouping by * default - this make a special description defined in the * actor override any grouping we'd otherwise do */ if (overrides(self, Actor, &specialDesc)) return []; /* get the group for the posture */ group = location.listWithActorIn(posture); /* * if we have a group, return a list containing the group; * otherwise return an empty list */ return (group == nil ? [] : [group]); } /* * Actor "I am here" description. This is displayed as part of the * description of a room - it describes the actor as being present in * the room. By default, we let the "nominal actor container" * provide the description. */ actorHereDesc { descViaActorContainer(&roomActorHereDesc, nil); } /* * Actor's "I am over there" description. This is displayed in the * room description when the actor is visible, but is either in a * separate top-level room or is at a distance. By default, we let * the "nominal actor container" provide the description. */ actorThereDesc { descViaActorContainer(&roomActorThereDesc, nil); } /* * Show our status, as an addendum to the given room's name (this is * the room title, shown at the start of a room description and on * the status line). By default, we'll let our nominal actor * container provide the status, to indicate when we're * standing/sitting/lying in a nested room. * * In concrete terms, this generally adds a message such as "(sitting * on the chair)" to the name of a room if we're in a nested room * within the room. When we're standing in the main room, this * generally adds nothing. * * Note that we pass the room we're describing as the "container to * ignore" parameter, because we don't want to say something like * "Phone Booth (standing in the phone booth)" - that is, we don't * want to mention the nominal container again if the nominal * container is what we're naming in the first place. */ actorRoomNameStatus(room) { descViaActorContainer(&roomActorStatus, room); } /* * Describe the actor via the "nominal actor container." The nominal * container is determined by our direct location. * * 'contToIgnore' is a container to ignore. If our nominal container * is the same as this object, we'll generate a description without a * mention of a container at all. * * The reason we have the 'contToIgnore' parameter is that the caller * might already have reported our general location, and now merely * wants to add that we're standing or standing or whatever. In * these cases, if we were to say that we're sitting on or standing * on that same object, it would be redundant information: "Bob is in * the garden, sitting in the garden." The 'contToIgnore' parameter * tells us the object that the caller has already mentioned as our * general location so that we don't re-report the same thing. We * need to know the actual object, rather than just the fact that the * caller mentioned a general location, because our general location * and the specific place we're standing or sitting or whatever might * not be the same: "Bob is in the garden, sitting in the lawn * chair." * */ descViaActorContainer(prop, contToIgnore) { local pov; local cont; /* get our nominal container for our current posture */ cont = location.getNominalActorContainer(posture); /* get the point of view, using the player character by default */ if ((pov = getPOV()) == nil) pov = gPlayerChar; /* * if we have a nominal container, and it's not the one to * ignore, and the player character can see it, generate the * description via the container; otherwise, use a generic * library message that doesn't mention the container */ if (cont not in (nil, contToIgnore) && pov.canSee(cont)) { /* describe via the container */ cont.(prop)(self); } else { /* use the generic library message */ gLibMessages.(prop)(self); } } /* * Describe my inventory as part of my description - this is only * called when we examine an NPC. If an NPC doesn't wish to have * its inventory listed as part of its description, it can simply * override this to do nothing. */ holdingDesc { /* * show our contents as for a normal "examine", but using the * special contents lister for what an actor is holding */ examineListContentsWith(holdingDescInventoryLister); } /* * refer to the player character with my player character referral * person, and refer to all other characters in the third person */ referralPerson { return isPlayerChar() ? pcReferralPerson : ThirdPerson; } /* by default, refer to the player character in the second person */ pcReferralPerson = SecondPerson /* * The referral person of the current command targeting the actor. * This is meaningful only when a command is being directed to this * actor, and this actor is an NPC. * * The referral person depends on the specifics of the language. In * English, a command like "bob, go north" is a second-person * command, while "tell bob to go north" is a third-person command. * The only reason this is important is in interpreting what "you" * means if it's used as an object in the command. "tell bob to hit * you" probably means that Bob should hit the player character, * while "bob, hit you" probably means that Bob should hit himself. */ commandReferralPerson = nil /* determine if I'm the player character */ isPlayerChar() { return libGlobal.playerChar == self; } /* * Implicit command handling style for this actor. There are two * styles for handling implied commands: "player" and "NPC", * indicated by the enum codes ModePlayer and ModeNPC, respectively. * * In "player" mode, each implied command is announced with a * description of the command to be performed; DEFAULT responses are * suppressed; and failures are shown. Furthermore, interactive * requests for more information from the parser are allowed. * Transcripts like this result: * * >open door *. (first opening the door) *. (first unlocking the door) *. What do you want to unlock it with? * * In "NPC" mode, implied commands are treated as complete and * separate commands. They are not announced; default responses are * shown; failures are NOT shown; and interactive requests for more * information are not allowed. When an implied command fails in NPC * mode, the parser acts as though the command had never been * attempted. * * By default, we return ModePlayer if we're the player character, * ModeNPC if not (thus the respective names of the modes). Some * authors might prefer to use "player mode" for NPC's as well as for * the player character, which is why the various parts of the parser * that care about this mode consult this method rather than simply * testing the PC/NPC status of the actor. */ impliedCommandMode() { return isPlayerChar() ? ModePlayer : ModeNPC; } /* * Try moving the given object into this object. For an actor, this * will do one of two things. If 'self' is the actor performing the * action that's triggering this implied command, then we can achieve * the goal simply by taking the object. Otherwise, the way to get * an object into my possession is to have the actor performing the * command give me the object. */ tryMovingObjInto(obj) { if (gActor == self) { /* * I'm performing the triggering action, so I merely need to * pick up the object */ return tryImplicitAction(Take, obj); } else { /* * another actor is performing the action; since that actor * is the one who must perform the implied action, the way to * get an object into my inventory is for that actor to give * it to me */ return tryImplicitAction(GiveTo, obj, self); } } /* desribe our containment of an object as carrying the object */ mustMoveObjInto(obj) { reportFailure(&mustBeCarryingMsg, obj, self); } /* * You can limit the cumulative amount of bulk an actor can hold, and * the maximum bulk of any one object the actor can hold, using * bulkCapacity and maxSingleBulk. These properties are analogous to * the same ones in Container. * * A word of caution on these is in order. Many authors worry that * it's unrealistic if the player character can carry too much at one * time, so they'll fiddle with these properties to impose a carrying * limit that seems realistic. Be advised that authors love this * sort of "realism" a whole lot more than players do. Players * almost universally don't care about it, and in fact tend to hate * the inventory juggling it inevitably leads to. Juggling inventory * isn't any fun for the player. Don't fool yourself about this - * the thoughts in the mind of a player who's tediously carting * objects back and forth three at a time will not include admiration * of your prowess at simulational realism. In contrast, if you set * the carrying limit to infinity, it's a rare player who will even * notice, and a much rarer player who'll complain about it. * * If you really must insist on inventory limits, refer to the * BagOfHolding class for a solution that can salvage most of the * "realism" that the accountancy-inclined author craves, without * creating undue inconvenience for the player. BagOfHolding makes * inventory limits palatable for the player by essentially * automating the required inventory juggling. In fact, for most * players, an inventory limit in conjunction with a bag of holding * is actually better than an unlimited inventory, since it improves * readability by keeping the direct inventory list to a manageable * size. */ bulkCapacity = 10000 maxSingleBulk = 10 /* * An actor can limit the cumulative amount of weight being held, * using weightCapacity. By default we make this so large that * there is effectively no limit to how much weight an actor can * carry. */ weightCapacity = 10000 /* * Can I own the given object? By default, an actor can own * anything. */ canOwn(obj) { return true; } /* * Get the preconditions for travel. By default, we'll add the * standard preconditions that the connector requires for actors. * * Note that these preconditions apply only when the actor is the * traveler. If the actor is in a vehicle, so that the vehicle is * the traveler in a given travel operation, the vehicle's * travelerPreCond conditions are used instead of ours. */ travelerPreCond(conn) { return conn.actorTravelPreCond(self); } /* by default, actors are listed when they arrive aboard a vehicle */ isListedAboardVehicle = true /* * Get the object that's actually going to move when this actor * travels via the given connector. In most cases this is simply the * actor; but when the actor is in a vehicle, travel commands move * the vehicle, not the actor: the actor stays in the vehicle while * the vehicle moves to a new location. We determine this by asking * our immediate location what it thinks about the situation. * * If we have a special traveler explicitly set, it overrides the * traveler indicated by the location. */ getTraveler(conn) { /* * Return our special traveler if we have one; otherwise, if we * have a location, return the traveler indicated by our * location; otherwise, we're the traveler. */ if (specialTraveler != nil) return specialTraveler; else if (location != nil) return location.getLocTraveler(self, conn); else return self; } /* * Get the "push traveler" for the actor. This is the nominal * traveler that we want to use when the actor enters a command like * PUSH BOX NORTH. 'obj' is the object we're trying to push. */ getPushTraveler(obj) { /* * If we already have a special traveler, just use the special * traveler. Otherwise, if we have a location, ask the location * what it thinks. Otherwise, we're the traveler. */ if (specialTraveler != nil) return specialTraveler; else if (location != nil) return location.getLocPushTraveler(self, obj); else return self; } /* is an actor traveling with us? */ isActorTraveling(actor) { /* we're the only actor traveling when we're the traveler */ return (actor == self); } /* invoke a callback on each actor traveling with the traveler */ forEachTravelingActor(func) { /* we're the only actor, so simply invoke the callback on myself */ (func)(self); } /* * Get the actors involved in travel, when we're acting in our role * as a Traveler. When the Traveler is simply the Actor, the only * actor involved in the travel is 'self'. */ getTravelerActors = [self] /* we're the self-motive actor doing the travel */ getTravelerMotiveActors = [self] /* * Set the "special traveler." When this is set, we explicitly * perform travel through this object rather than through the * traveler indicated by our location. Returns the old value, so * that the old value can be restored when the caller has finished * its need for the special traveler. */ setSpecialTraveler(traveler) { local oldVal; /* remember the old value so that we can return it */ oldVal = specialTraveler; /* remember the new value */ specialTraveler = traveler; /* return the old value */ return oldVal; } /* our special traveler */ specialTraveler = nil /* * Try moving the actor into the given room in preparation for * travel, using pre-condition rules. */ checkMovingTravelerInto(room, allowImplicit) { /* try moving the actor into the room */ return room.checkMovingActorInto(allowImplicit); } /* * Check to ensure the actor is ready to enter the given nested * room, using pre-condition rules. By default, we'll ask the given * nested room to handle it. */ checkReadyToEnterNestedRoom(dest, allowImplicit) { /* ask the destination to do the work */ return dest.checkActorReadyToEnterNestedRoom(allowImplicit); } /* * Travel within a location, as from a room to a contained nested * room. This should generally be used in lieu of travelTo when * traveling between locations that are related directly by * containment rather than with TravelConnector objects. * * Travel within a location is not restricted by darkness; we assume * that if the nested objects are in scope at all, travel among them * is allowed. * * This type of travel does not trigger calls to travelerLeaving() * or travelerArriving(). To mitigate this loss of notification, we * call actorTravelingWithin() on the source and destination * objects. */ travelWithin(dest) { /* if I'm not going anywhere, ignore the operation */ if (dest == location) return; /* * Notify the traveler. Note that since this is local travel * within a single top-level location, there's no connector. */ getTraveler(nil).travelerTravelWithin(self, dest); } /* * Traveler interface: perform local travel, between nested rooms * within a single top-level location. */ travelerTravelWithin(actor, dest) { local origin; /* remember my origin */ origin = location; /* notify the source that we're traveling within a room */ if (origin != nil) origin.actorTravelingWithin(origin, dest); /* * if our origin and destination have different effective follow * locations, track the follow */ if (origin != nil && dest != nil && origin.effectiveFollowLocation != dest.effectiveFollowLocation) { /* * notify observing objects of the travel; we're not moving * along a connector, so there is no connector associated * with the tracking information */ connectionTable().forEachAssoc( {obj, val: obj.beforeTravel(self, nil)}); } /* move me to the destination */ moveInto(dest); /* * recalculate the global sense context for message generation * purposes, since we've moved to a new location */ if (gAction != nil) gAction.recalcSenseContext(); /* notify the destination of the interior travel */ if (dest != nil) dest.actorTravelingWithin(origin, dest); } /* * Check for travel in the dark. If we're in a dark room, and our * destination is a dark room, ask the connector for guidance. * * Travel connectors normally call this before invoking our * travelTo() method to carry out the travel. The darkness check * usually must be made before any barrier checks. */ checkDarkTravel(dest, connector) { local origin; /* * If we're not in the dark in the current location, there's no * need to check for dark-to-dark travel; light-to-dark travel * is always allowed. */ if (isLocationLit()) return; /* get the origin - this is the traveler's location */ origin = getTraveler(connector).location; /* * Check to see if the connector itself is visible in the dark. * If it is, then allow the travel without restriction. */ if (connector.isConnectorVisibleInDark(origin, self)) return; /* * We are attempting dark-to-dark travel. We allow or disallow * this type of travel on a per-connector basis, so ask the * connector to handle it. If the connector wishes to disallow * the travel, it will display an appropriate failure report and * terminate the command with 'exit'. */ connector.darkTravel(self, dest); } /* * Travel to a new location. */ travelTo(dest, connector, backConnector) { /* send the request to the traveler */ getTraveler(connector) .travelerTravelTo(dest, connector, backConnector); } /* * Perform scripted travel to the given adjacent location. This * looks for a directional connector in our current location whose * destination is the given location, and for a corresponding * back-connector in the destination location. If we can find the * connectors, we'll perform the travel using travelTo(). * * The purpose of this routine is to simplify scripted travel for * simple cases where directional connectors are available for the * desired travel. This routine is NOT suitable for intelligent * goal-seeking NPC's who automatically try to find their own routes, * for two reasons. First, this routine only lets an NPC move to an * *adjacent* location; it won't try to find a path between arbitrary * locations. Second, this routine is "omniscient": it doesn't take * into account what the NPC knows about the connections between * locations, but simply finds a connector that actually provides the * desired travel. * * What this routine *is* suitable for are cases where we have a * pre-scripted series of NPC travel actions, where we have a list of * rooms we want the NPC to visit in order. This routine simplifies * this type of scripting by automatically finding the connectors; * the script only has to specify the next location for the NPC to * visit. */ scriptedTravelTo(dest) { local conn; /* find a connector from the current location to the new location */ conn = location.getConnectorTo(self, dest); /* if we found the connector, perform the travel */ if (conn != nil) nestedActorAction(self, TravelVia, conn); } /* * Remember the last door I traveled through. We use this * information for disambiguation, to boost the likelihood that an * actor that just traveled through a door is referring to the same * door in a subsequent "close" command. */ rememberLastDoor(obj) { lastDoorTraversed = obj; } /* * Remember our most recent travel. If we know the back connector * (i.e., the connector that reverses the travel we're performing), * then we'll be able to accept a GO BACK command to attempt to * return to the previous location. */ rememberTravel(origin, dest, backConnector) { /* remember the destination of the travel, and the connector back */ lastTravelDest = dest; lastTravelBack = backConnector; } /* * Reverse the most recent travel. If we're still within the same * destination we reached in the last travel, and we know the * connector we arrived through (i.e., the "back connector" for the * last travel, which reverses the connector we took to get here), * then try traveling via the connector. */ reverseLastTravel() { /* * If we don't know the connector back to our previous location, * we obviously can't reverse the travel. If we're not still in * the same location as the previous travel's destination, then * we can't reverse the travel either, because the back * connector isn't applicable to our current location. (This * latter condition could only happen if we've been moved * somewhere without ordinary travel occurring, but this is a * possibility.) */ if (lastTravelBack == nil || lastTravelDest == nil || !isIn(lastTravelDest)) { reportFailure(&cannotGoBackMsg); exit; } /* attempt travel via our back connector */ nestedAction(TravelVia, lastTravelBack); } /* the last door I traversed */ lastDoorTraversed = nil /* the destination and back connector for our last travel */ lastTravelDest = nil lastTravelBack = nil /* * use a custom message for cases where we're holding a destination * object for BOARD, ENTER, etc */ checkStagingLocation(dest) { /* * if the destination is within us, explain specifically that * this is the problem */ if (dest.isIn(self)) reportFailure(&invalidStagingContainerActorMsg, self, dest); else inherited(dest); /* terminate the command */ exit; } /* * Travel arrival/departure messages. Defer to the current state * object on all of these. */ sayArriving(conn) { curState.sayArriving(conn); } sayDeparting(conn) { curState.sayDeparting(conn); } sayArrivingLocally(dest, conn) { curState.sayArrivingLocally(dest, conn); } sayDepartingLocally(dest, conn) { curState.sayDepartingLocally(dest, conn); } sayTravelingRemotely(dest, conn) { curState.sayTravelingRemotely(dest, conn); } sayArrivingDir(dir, conn) { curState.sayArrivingDir(dir, conn); } sayDepartingDir(dir, conn) { curState.sayDepartingDir(dir, conn); } sayArrivingThroughPassage(conn) { curState.sayArrivingThroughPassage(conn); } sayDepartingThroughPassage(conn) { curState.sayDepartingThroughPassage(conn); } sayArrivingViaPath(conn) { curState.sayArrivingViaPath(conn); } sayDepartingViaPath(conn) { curState.sayDepartingViaPath(conn); } sayArrivingUpStairs(conn) { curState.sayArrivingUpStairs(conn); } sayArrivingDownStairs(conn) { curState.sayArrivingDownStairs(conn); } sayDepartingUpStairs(conn) { curState.sayDepartingUpStairs(conn); } sayDepartingDownStairs(conn) { curState.sayDepartingDownStairs(conn); } /* * Get the current interlocutor. By default, we'll address new * conversational commands (ASK ABOUT, TELL ABOUT, SHOW TO) to the * last conversational partner, if that actor is still within range. */ getCurrentInterlocutor() { /* * if we've talked to someone before, and we can still talk to * them now, return that actor; otherwise we have no default */ if (lastInterlocutor != nil && canTalkTo(lastInterlocutor)) return lastInterlocutor; else return nil; } /* * Get the default interlocutor. If there's a current interlocutor, * and we can still talk to that actor, then that's the default * interlocutor. If not, we'll return whatever actor is the default * for a TALK TO command. Note that TALK TO won't necessarily have a * default actor; if it doesn't, we'll simply return nil. */ getDefaultInterlocutor() { local actor; /* check for a current interlocutor */ actor = getCurrentInterlocutor(); /* * if we're not talking to anyone, or if the person we were * talking to can no longer hear us, look for a default object * for a TALK TO command and use it instead as the default */ if (actor == nil || !canTalkTo(actor)) { /* set up a TALK TO command and a resolver */ local tt = new TalkToAction(); local res = new Resolver(tt, gIssuingActor, gActor); /* get the default direct object */ actor = tt.getDefaultDobj(new EmptyNounPhraseProd(), res); /* if that worked, get the object from the resolve info */ if (actor != nil) actor = actor[1].obj_; } /* return what we found */ return actor; } /* * The most recent actor that we've interacted with through a * conversational command (ASK, TELL, GIVE, SHOW, etc). */ lastInterlocutor = nil /* * Our conversational "boredom" counter. While we're in a * conversation, this tracks the number of turns since the last * conversational command from the actor we're talking to. * * Note that this state is part of the actor, even though it's * usually managed by the InConversationState object. The state is * stored with the actor rather than with the state object because * it really describes the condition of the actor, not of the state * object. */ boredomCount = 0 /* * game-clock time (Schedulable.gameClockTime) of the last * conversational command addressed to us by the player character */ lastConvTime = -1 /* * Did we engage in any conversation on the current turn? This can * be used as a quick check in background activity scripts when we * want to run a step only in the absence of any conversation on the * same turn. */ conversedThisTurn() { return lastConvTime == Schedulable.gameClockTime; } /* * Note that we're performing a conversational command targeting the * given actor. We'll make the actors point at each other with their * 'lastInterlocutor' properties. This is called on the character * performing the conversation command: if the player types ASK BOB * ABOUT BOOK, this will be called on the player character actor, * with 'other' set to Bob. */ noteConversation(other) { /* note that we're part of a conversational action */ noteConvAction(other); /* let the other actor know we're conversing with them */ other.noteConversationFrom(self); } /* * Note that another actor is issuing a conversational command * targeting us. For example, if the player types ASK BOB ABOUT * BOOK, then this will be called on Bob, with the player character * actor as 'other'. */ noteConversationFrom(other) { /* note that we're part of a conversational action */ noteConvAction(other); } /* * Note that we're taking part in a conversational action with * another character. This is symmetrical - it could mean we're the * initiator of the conversation action or the target. We'll * remember the person we're talking to, and reset our conversation * time counters so we know we've conversed on this turn. */ noteConvAction(other) { /* note our last conversational partner */ lastInterlocutor = other; /* set the actor to be the pronoun antecedent */ setPronounObj(other); /* * reset our boredom counter, as the other actor has just spoken * to us */ boredomCount = 0; /* remember the time of our last conversation from the PC */ lastConvTime = Schedulable.gameClockTime; } /* note that we're consulting an item */ noteConsultation(obj) { lastConsulted = obj; } /* * Receive notification that a TopicEntry response in our database is * being invoked. We'll just pass this along to our current state. */ notifyTopicResponse(fromActor, entry) { /* let our current state handle it */ curState.notifyTopicResponse(fromActor, entry); } /* the object we most recently consulted */ lastConsulted = nil /* * The actor's "agenda." This is a list of AgendaItem objects that * describe things the actor wants to do of its own volition on its * own turn. */ agendaList = nil /* * our special "boredom" agenda item - this makes us initiate an end * to an active conversation when the PC has ignored us for a given * number of consecutive turns */ boredomAgendaItem = perInstance(new BoredomAgendaItem(self)) /* add an agenda item */ addToAgenda(item) { /* if we don't have an agenda list yet, create one */ if (agendaList == nil) agendaList = new Vector(10); /* add the item */ agendaList.append(item); /* * keep the list in ascending order of agendaOrder values - this * will ensure that we'll always choose the earliest item that's * ready to run */ agendaList.sort(SortAsc, {a, b: a.agendaOrder - b.agendaOrder}); /* reset the agenda item */ item.resetItem(); } /* remove an agenda item */ removeFromAgenda(item) { /* if we have an agenda list, remove the item */ if (agendaList != nil) agendaList.removeElement(item); } /* * Execute the next item in our agenda, if there are any items in the * agenda that are ready to execute. We'll return true if we found * an item to execute, nil if not. */ executeAgenda() { local item; /* if we don't have an agenda, there are obviously no items */ if (agendaList == nil) return nil; /* remove any items that are marked as done */ while ((item = agendaList.lastValWhich({x: x.isDone})) != nil) agendaList.removeElement(item); /* * Scan for an item that's ready to execute. Since we keep the * list sorted in ascending order of agendaOrder values, we can * just pick the earliest item in the list that's ready to run, * since that will be the ready-to-run item with the lowest * agendaOrder number. */ item = agendaList.valWhich({x: x.isReady}); /* if we found an item, execute it */ if (item != nil) { try { /* execute the item */ item.invokeItem(); } catch (RuntimeError err) { /* * If an error occurs while executing the item, mark the * item as done. This will ensure that we won't get * stuck in a loop trying to execute the same item over * and over, which will probably just run into the same * error on each attempt. */ item.isDone = true; /* re-throw the exception */ throw err; } /* tell the caller we found an item to execute */ return true; } else { /* tell the caller we found no agenda item */ return nil; } } /* * Calculate the amount of bulk I'm holding directly. By default, * we'll simply add up the "actor-encumbering bulk" of each of our * direct contents. * * Note that we don't differentiate here based on whether or not an * item is being worn, or anything else - we deliberately leave such * distinctions up to the getEncumberingBulk routine, so that only * the objects are in the business of deciding how bulky they are * under different circumstances. */ getBulkHeld() { local total; /* start with nothing */ total = 0; /* add the bulks of directly-contained items */ foreach (local cur in contents) total += cur.getEncumberingBulk(self); /* return the total */ return total; } /* * Calculate the total weight I'm holding. By default, we'll add up * the "actor-encumbering weight" of each of our direct contents. * * Note that we deliberately only consider our direct contents. If * any of the items we are directly holding contain further items, * getEncumberingWeight will take their weights into account; this * frees us from needing any special knowledge of the internal * structure of any items we're holding, and puts that knowledge in * the individual items where it belongs. */ getWeightHeld() { local total; /* start with nothing */ total = 0; /* add the weights of directly-contained items */ foreach (local cur in contents) total += cur.getEncumberingWeight(self); /* return the total */ return total; } /* * Try making room to hold the given object. This is called when * checking the "room to hold object" pre-condition, such as for the * "take" verb. * * If holding the new object would exceed the our maximum holding * capacity, we'll go through our inventory looking for objects that * can reduce our held bulk with implicit commands. Objects with * holding affinities - "bags of holding", keyrings, and the like - * can implicitly shuffle the actor's possessions in a manner that * is neutral as far as the actor is concerned, thereby reducing our * active holding load. * * Returns true if an implicit command was attempted, nil if not. */ tryMakingRoomToHold(obj, allowImplicit) { local objWeight; local objBulk; local aff; /* get the amount of weight this will add if taken */ objWeight = obj.getEncumberingWeight(self); /* * If this object alone is too heavy for us, give up. We * distinguish this case from the case where the total (of * everything held plus the new item) is too heavy: in the * latter case we can tell the actor that they can pick this up * by dropping something else first, whereas if this item alone * is too heavy, no such advice is warranted. */ if (objWeight > weightCapacity) { reportFailure(&tooHeavyForActorMsg, obj); exit; } /* * if taking the object would push our total carried weight over * our total carrying weight limit, give up */ if (obj.whatIfHeldBy({: getWeightHeld()}, self) > weightCapacity) { reportFailure(&totalTooHeavyForMsg, obj); exit; } /* get the amount of bulk the object will add */ objBulk = obj.getEncumberingBulk(self); /* * if the object is simply too big to start with, we can't make * room no matter what we do */ if (objBulk > maxSingleBulk || objBulk > bulkCapacity) { reportFailure(&tooLargeForActorMsg, obj); exit; } /* * Test what would happen to our bulk if we were to move the * object into our directly held inventory. Do this by running * a "what if" scenario to test moving the object into our * inventory, and check what effect it has on our held bulk. If * it fits, we can let the caller proceed without further work. */ if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity) return nil; /* * if we're not allowed to run implicit commands, we won't be * able to accomplish anything, so give up */ if (!allowImplicit) { reportFailure(&handsTooFullForMsg, obj); exit; } /* * Get "bag of holding" affinity information for my immediate * contents. Consider only objects with encumbering bulk, since * it will do us no good to move objects without any encumbering * bulk. Also ignore objects that aren't being held (some direct * contents aren't considered to be held, such as clothing being * worn). */ aff = getBagAffinities(contents.subset( {x: x.getEncumberingBulk(self) != 0 && x.isHeldBy(self)})); /* if there are no bag affinities, we can't move anything around */ if (aff.length() == 0) { reportFailure(&handsTooFullForMsg, obj); exit; } /* * If we have at least four items, find the two that were picked * up most recently (according to the "holding index" value) and * move them to the end of the list. In most cases, we'll only * have to dispose of one or two items to free up enough space * in our hands, so we'll probably never get to the last couple * of items in our list, so we're effectively ruling out moving * these two most recent items; but they'll be in the list if we * do find we need to move them after all. * * The point of this rearrangement is to avoid annoying cases of * moving something we just picked up, especially if we just * picked it up in order to carry out the command that's making * us free up more space now. This looks especially stupid when * we perform some command that requires picking up two items * automatically: we pick up the first, then we put it away in * order to pick up the second, but then we find that we need * the first again. */ if (aff.length() >= 4) { local a, b; /* remove the two most recent items from the vector */ a = BagAffinityInfo.removeMostRecent(aff); b = BagAffinityInfo.removeMostRecent(aff); /* re-insert them at the end of the vector */ aff.append(b); aff.append(a); } /* * Move each object in the list until we have reduced the bulk * sufficiently. */ foreach (local cur in aff) { /* * Try moving this object to its bag. If the bag is itself * inside this object, don't even try, since that would be an * attempt at circular containment. * * If the object we're trying to hold is inside this object, * don't move the object. That might put the object we're * trying to hold out of reach, since moving an object into a * bag could involve closing the object or making its * contents not directly accessible. */ if (!cur.bag_.isIn(cur.obj_) && !obj.isIn(cur.obj_) && cur.bag_.tryPuttingObjInBag(cur.obj_)) { /* * this routine tried tried to move the object into the * bag - check our held bulk to see if we're in good * enough shape yet */ if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity) { /* * We've met our condition - there's no need to look * any further. Return, telling the caller we've * performed an implicit command. */ return true; } } } /* * If we get this far, it means that we tried every child object * but failed to find anything that could help. Explain the * problem and abort the command. */ reportFailure(&handsTooFullForMsg, obj); exit; } /* * Check a bulk change of one of my direct contents. */ checkBulkChangeWithin(obj) { local objBulk; /* get the object's new bulk */ objBulk = obj.getEncumberingBulk(self); /* * if this change would cause the object to exceed our * single-item bulk limit, don't allow it */ if (objBulk > maxSingleBulk || objBulk > bulkCapacity) { reportFailure(&becomingTooLargeForActorMsg, obj); exit; } /* * If our total carrying capacity is exceeded with this change, * don't allow it. Note that 'obj' is already among our * contents when this routine is called, so we can simply check * our current total bulk within. */ if (getBulkHeld() > bulkCapacity) { reportFailure(&handsBecomingTooFullForMsg, obj); exit; } } /* * Next available "holding index" value. Each time we pick up an * item, we'll assign it our current holding index value and then * increment our value. This gives us a simple way to keep track of * the order in which we picked up items we're carrying. * * Note that we make the simplifying assumption that an object can * be held by only one actor at a time (multi-location items are * generally not portable), which means that we can use a simple * property in each object being held to store its holding index. */ nextHoldingIndex = 1 /* add an object to my contents */ addToContents(obj) { /* assign the new object our next holding index */ obj.holdingIndex = nextHoldingIndex++; /* inherit default handling */ inherited(obj); } /* * Go to sleep. This is used by the 'Sleep' action to carry out the * command. By default, we simply say that we're not sleepy; actors * can override this to cause other actions. */ goToSleep() { /* simply report that we can't sleep now */ mainReport(&cannotSleepMsg); } /* * My current "posture," which specifies how we're positioned with * respect to our container; this is one of the standard library * posture enum values (Standing, etc.) or another posture added by * the game. */ posture = standing /* * Get a default acknowledgment of a change to our posture. This * should acknowledge the posture so that it tells us the current * posture. This is used for a command such as "stand up" from a * chair, so that we can report the appropriate posture status in * our acknowledgment; we might end up being inside another nested * container after standing up from the chair, so we might not * simply be standing when we're done. */ okayPostureChange() { /* get our nominal container for our current posture */ local cont = location.getNominalActorContainer(posture); /* if the container is visible, let it handle it */ if (cont != nil && gPlayerChar.canSee(cont)) { /* describe via the container */ cont.roomOkayPostureChange(self); } else { /* use the generic library message */ defaultReport(&okayPostureChangeMsg, posture); } } /* * Describe the actor as part of the EXAMINE description of a nested * room containing the actor. 'povActor' is the actor doing the * looking. */ listActorPosture(povActor) { /* get our nominal container for our current posture */ local cont = location.getNominalActorContainer(posture); /* if the container is visible, let it handle it */ if (cont != nil && povActor.canSee(cont)) cont.roomListActorPosture(self); } /* * Stand up. This is used by the 'Stand' action to carry out the * command. */ standUp() { /* if we're already standing, say so */ if (posture == standing) { reportFailure(&alreadyStandingMsg); return; } /* ask the location to make us stand up */ location.makeStandingUp(); } /* * Disembark. This is used by the 'Get out' action to carry out the * command. By default, we'll let the room handle it. */ disembark() { /* let the room handle it */ location.disembarkRoom(); } /* * Set our posture to the given status. By default, we'll simply * set our posture property to the new status, but actors can * override this to handle side effects of the change. */ makePosture(newPosture) { /* remember our new posture */ posture = newPosture; } /* * Display a description of the actor's location from the actor's * point of view. * * If 'verbose' is true, then we'll show the full description in all * cases. Otherwise, we'll show the full description if the actor * hasn't seen the location before, or the terse description if the * actor has previously seen the location. */ lookAround(verbose) { /* turn on the sense cache while we're looking */ libGlobal.enableSenseCache(); /* show a description of my immediate location, if I have one */ if (location != nil) location.lookAroundPov(self, self, verbose); /* turn off the sense cache now that we're done */ libGlobal.disableSenseCache(); } /* * Get my "look around" location name as a string. This returns a * string containing the location name that we display in the status * line or at the start of a "look around" description of my * location. */ getLookAroundName() { return mainOutputStream.captureOutput( {: location.lookAroundWithinName(self, getVisualAmbient()) }) .specialsToText(); } /* * Adjust a table of visible objects for 'look around'. By default, * we remove any explicitly excluded objects. */ adjustLookAroundTable(tab, pov, actor) { /* remove any explicitly excluded objects */ foreach (local cur in excludeFromLookAroundList) tab.removeElement(cur); /* inherit the base handling */ inherited(tab, pov, actor); } /* * Add an object to the 'look around' exclusion list. Returns true * if the object was already in the list, nil if not. */ excludeFromLookAround(obj) { /* * if the object is already in the list, don't add it again - * just tell the caller it's already there */ if (excludeFromLookAroundList.indexOf(obj) != nil) return true; /* add it to the list and tell the caller it wasn't already there */ excludeFromLookAroundList.append(obj); return nil; } /* remove an object from the 'look around' exclusion list */ unexcludeFromLookAround(obj) { excludeFromLookAroundList.removeElement(obj); } /* * Our list of objects explicitly excluded from 'look around'. These * objects will be suppressed from any sort of listing (including in * the room's contents list and in special descriptions) in 'look * around' when this actor is doing the looking. */ excludeFromLookAroundList = perInstance(new Vector(5)) /* * Get the location into which objects should be moved when the * actor drops them with an explicit 'drop' command. By default, we * return the drop destination of our current container. */ getDropDestination(objToDrop, path) { return (location != nil ? location.getDropDestination(objToDrop, path) : nil); } /* * The senses that determine scope for this actor. An actor might * possess only a subset of the defined sense. * * By default, we give each actor all of the human senses that we * define, except touch. In general, merely being able to touch an * object doesn't put the object in scope, because if an object * isn't noticed through some other sense, touch would only make an * object accessible if it's within arm's reach, which for our * purposes means that the object is being held directly by the * actor. Imagine an actor in a dark room: lots of things might be * touchable in the sense that there's no physical barrier to * touching them, but without some other sense to locate the * objects, the actor wouldn't have any way of knowing where to * reach to touch things, so they're not in scope. So, touch isn't * a scope sense. */ scopeSenses = [sight, sound, smell] /* * "Sight-like" senses: these are the senses that operate like sight * for the actor, and which the actor can use to determine the names * of objects and the spatial relationships between objects. These * senses should operate passively, in the sense that they should * tend to collect sensory input continuously and without explicit * action by the actor, the way sight does and the way touch, for * example, does not. These senses should also operate instantly, * in the sense that the sense can reasonably take in most or all of * a location at one time. * * These senses are used to determine what objects should be listed * in room descriptions, for example. * * By default, the only sight-like sense is sight, since other human * senses don't normally provide a clear picture of the spatial * relationships among objects. (Touch could with some degree of * effort, but it can't operate passively or instantly, since * deliberate and time-consuming action would be necessary.) * * An actor can have more than one sight-like sense, in which case * the senses will act effectively as one sense that can reach the * union of objects reachable through the individual senses. */ sightlikeSenses = [sight] /* * Hearing-like senses. These are senses that the actor can use to * hear objects. */ hearinglikeSenses = [sound] /* * Smell-like senses. These are senses that the actor can use to * smell objects. */ smelllikeSenses = [smell] /* * Communication senses: these are the senses through which the * actor can communicate directly with other actors through commands * and messages. * * Conceptually, these senses are intended to be only those senses * that the actors would *naturally* use to communicate, because * senses in this list allow direct communications via the most * ordinary game commands, such as "bob, go east". * * If some form of indirect communication is possible via a sense, * but that form is not something the actor would think of as the * most natural, default form of communication, it should *not* be * in this list. For example, two sighted persons who can see one * another but cannot hear one another could still communicate by * writing messages on pieces of paper, but they would ordinarily * communicate by talking. In such a case, sound should be in the * list but sight should not be, because sight is not a natural, * default form of communications for the actors. */ communicationSenses = [sound] /* * Determine if I can communicate with the given character via a * natural, default form of communication that we share with the * other character. This determines if I can talk to the other * character. We'll return true if I can talk to the other actor, * nil if not. * * In order for the player character to issue a command to a * non-player character (as in "bob, go east"), the NPC must be able * to sense the PC via at least one communication sense that the two * actors have in common. * * Likewise, in order for a non-player character to say something to * the player, the player must be able to sense the NPC via at least * one communication sense that the two actors have in common. */ canTalkTo(actor) { local common; /* * first, get a list of the communications senses that we have * in common with the other actor - we must have a sense channel * via this sense */ common = communicationSenses.intersect(actor.communicationSenses); /* * if there are no common senses, we can't communicate, * regardless of our physical proximity */ if (common == []) return nil; /* * Determine how well the other actor can sense me in these * senses. Note that all that matters it that the actor can * hear me, because we're determine if I can talk to the other * actor - it doesn't matter if I can hear the other actor. */ foreach (local curSense in common) { local result; /* * determine how well the other actor can sense me in this * sense */ result = actor.senseObj(curSense, self); /* check whether or not this is good enough */ if (actor.canBeTalkedTo(self, curSense, result)) return true; } /* * if we get this far, we didn't find any senses with a clear * enough communications channel - we can't talk to the other * actor */ return nil; } /* * Determine whether or not I can understand an attempt by another * actor to talk to me. 'talker' is the actor doing the talking. * 'sense' is the sense we're testing; this will always be a sense * in our communicationSenses list, and will always be a * communications sense we have in common with the other actor. * 'info' is a SenseInfo object giving information on the clarity of * the sense path to the other actor. * * We return true if we can understand the communication, nil if * not. There is no middle ground where we can partially * understand; we can either understand or not. * * Note that this routine is concerned only with our ability to * sense the communication. The result here should NOT pay any * attention to whether or not we can actually communicate given a * clear sense path - for example, this routine should not reflect * whether or not we have a spoken language in common with the other * actor. * * This is a service method for canTalkTo. This is broken out as a * separate method so that individual actors can override the * necessary conditions for communications in particular senses. */ canBeTalkedTo(talker, sense, info) { /* * By default, we allow communication if the sense path is * transparent or distant. We don't care what the sense is, * since we know we'll never be asked about a sense that's not * in our communicationSenses list. */ return info.trans is in (transparent, distant); } /* * Flag: we wait for commands issued to other actors to complete * before we get another turn. If this is true, then whenever we * issue a command to another actor ("bob, go north"), we will not * get another turn until the other actor has finished executing the * full set of commands we issued. * * By default, this is true, which means that we wait for other * actors to finish all of the commands we issue before we take * another turn. * * If this is set to nil, we'll continue to take turns while the * other actor carries out our commands. In this case, the only * time cost to us of issuing a command is given by orderingTime(), * which normally takes one turn for issuing a command, regardless * of the command's complexity. Some games might wish to use this * mode for interesting effects with NPC's carrying out commands in * parallel with the player, but it's an unconventional style that * some players might find confusing, so we don't use this mode by * default. */ issueCommandsSynchronously = true /* * Flag: the "target actor" of the command line automatically reverts * to this actor at the end of a sentence, when this actor is the * issuer of a command. If this flag is nil, an explicit target * actor stays in effect until the next explicit target actor (or the * end of the entire command line, if no other explicit target actors * are named); if this flag is true, a target actor is in effect only * until the end of a sentence. * * Consider this command line: * * >Bob, go north and get fuel cell. Get log tape. * * If this flag is nil, then the second sentence ("get log tape") is * interpreted as a command to Bob, because Bob is explicitly * designated as the target of the command, and this remains in * effect until the end of the entire command line. * * If this flag is true, on the other hand, then the second sentence * is interpreted as a command to the player character, because the * target actor designation ("Bob,") lasts only until the end of the * sentence. Once a new sentence begins, we revert to the issuing * actor (the player character, since the command came from the * player via the keyboard). */ revertTargetActorAtEndOfSentence = nil /* * The amount of time, in game clock units, it takes me to issue an * order to another actor. By default, it takes one unit (which is * usually equal to one turn) to issue a command to another actor. * However, if we are configured to wait for our issued commands to * complete in full, the ordering time is zero; we don't need any * extra wait time in this case because we'll wait the full length * of the issued command to begin with. */ orderingTime(targetActor) { return issueCommandsSynchronously ? 0 : 1; } /* * Wait for completion of a command that we issued to another actor. * The parser calls this routine after each time we issue a command * to another actor. * * If we're configured to wait for completion of orders given to * other actors before we get another turn, we'll set ourselves up * in waiting mode. Otherwise, we'll do nothing. */ waitForIssuedCommand(targetActor) { /* if we can issue commands asynchronously, there's nothing to do */ if (!issueCommandsSynchronously) return; /* * Add an empty pending command at the end of the target actor's * queue. This command won't do anything when executed; its * purpose is to let us track whether or not the target is still * working on commands we have issued up to this point, which we * can tell by looking to see whether our empty command is still * in the actor's queue. * * Note that we can't simply wait until the actor's queue is * empty, because the actor could acquire new commands while * it's working on our pending commands, and we wouldn't want to * wait for those to finish. Adding a dummy pending command is * a reliable way of tracking the actor's queue, because any * changes to the target actor's command queue will leave our * dummy command in its proper place until the target actor gets * around to executing it, at which point it will be removed. * * Remember the dummy pending command in a property of self, so * that we can check later to determine when the command has * finished. */ waitingForActor = targetActor; waitingForInfo = new PendingCommandMarker(self); targetActor.pendingCommand.append(waitingForInfo); } /* * Synchronous command processing: the target actor and dummy * pending command we're waiting for. When these are non-nil, we * won't take another turn until the given PendingCommandInfo has * been removed from the given target actor's command queue. */ waitingForActor = nil waitingForInfo = nil /* * Add the given actor to the list of actors accompanying my travel * on the current turn. This does NOT set an actor in "follow mode" * or "accompany mode" or anything like that - don't use this to make * an actor follow me around. Instead, this makes the given actor go * with us for the CURRENT travel only - the travel we're already in * the process of performing to process the current TravelVia action. */ addAccompanyingActor(actor) { /* if we don't have the accompanying actor vector yet, create it */ if (accompanyingActors == nil) accompanyingActors = new Vector(8); /* add the actor to my list */ accompanyingActors.append(actor); } /* * My vector of actors who are accompanying me. * * This is for internal bookkeeping only, and it applies to the * current travel only. This is NOT a general "follow mode" setting, * and it shouldn't be used to get me to follow another actor or * another actor to follow me. To make me accompany another actor, * simply override accompanyTravel() so that it returns a suitable * ActorState object. */ accompanyingActors = nil /* * Get the list of objects I can follow. This is a list of all of * the objects which I have seen departing a location - these are * all in scope for 'follow' commands. */ getFollowables() { /* return the list of the objects we know about */ return followables_.mapAll({x: x.obj}); } /* * Do I track departing objects for following the given object? * * By default, the player character tracks everyone, and NPC's track * only the actor they're presently tasked to follow. Most NPC's * will never accept 'follow' commands, so there's no need to track * everyone all the time; for efficiency, we take advantage of this * assumption so that we can avoid storing a bunch of tracking * information that will never be used. */ wantsFollowInfo(obj) { /* * by default, the player character tracks everyone, and NPC's * track only the object (if any) they're currently tasked to * follow */ return isPlayerChar() || followingActor == obj; } /* * Receive notification that an object is leaving its current * location as a result of the action we're currently processing. * Actors (and possibly other objects) will broadcast this * notification to all Actor objects connected in any way by * containment when they move under their own power (such as with * Actor.travelTo) to a new location. We'll keep tracking * information if we are configured to keep tracking information for * the given object and we can see the given object. Note that this * is called when the object is still at the source end of the travel * - the important thing is that we see the object departing. * * 'obj' is the object that is seen to be leaving, and 'conn' is the * TravelConnector it is taking. * * 'conn' is the connector being traversed. If we're simply being * observed in this location (as in a call to setHasSeen), rather * than being observed to leave the location, the connector will be * nil. * * 'from' is the effective starting location of the travel. This * isn't necessarily the departing object's location, since the * departing object could be inside a vehicle or some other kind of * traveler object. * * Note that this notification is sent only to actors with some sort * of containment connection to the object that's moving, because a * containment connection is necessary for there to be a sense * connection. */ trackFollowInfo(obj, conn, from) { local info; /* * If we're not tracking the given object, or we can't see the * given object, ignore the notification. In addition, we * obviously have no need to track ourselves.x */ if (obj == self || !wantsFollowInfo(obj) || !canSee(obj)) return; /* * If we already have a FollowInfo for the given object, re-use * the existing one; otherwise, create a new one and add it to * our tracking list. */ info = followables_.valWhich({x: x.obj == obj}); if (info == nil) { /* we don't have an existing one - create a new one */ info = new FollowInfo(); info.obj = obj; /* add it to our list */ followables_ += info; } /* remember information about the travel */ info.connector = conn; info.sourceLocation = from; } /* * Get information on what to do to make this actor follow the given * object. This returns a FollowInfo object that reports our last * knowledge of the given object's location and departure, or nil if * we don't know anything about how to follow the actor. */ getFollowInfo(obj) { return followables_.valWhich({x: x.obj == obj}); } /* * By default, all actors are followable. */ verifyFollowable() { return true; } /* * Verify a "follow" command being performed by this actor. */ actorVerifyFollow(obj) { /* * check to see if we're in the same effective follow location * as the target; if we are, it makes no sense to follow the * target, since we're already effectively at the same place */ if (obj.location != nil && (location.effectiveFollowLocation == obj.location.effectiveFollowLocation)) { /* * We're in the same location as the target. If we're the * player character, this makes no sense, because the player * character can't go into follow mode (as that would take * away the player's ability to control the player * character). If we're an NPC, though, this simply tells * us to go into follow mode for the target, so there's * nothing wrong with it. */ if (isPlayerChar) { /* * The target is right here, but we're the player * character, so it makes no sense for us to go into * follow mode. If we can see the target, complain that * it's already here; if not, we can only assume it's * here, but we can't know for sure. */ if (canSee(obj)) illogicalNow(&followAlreadyHereMsg); else illogicalNow(&followAlreadyHereInDarkMsg); } } else if (!canSee(obj)) { /* * The target isn't here, and we can't see it from here, so * we must want to follow it to its current location. Get * information on how we will follow the target. If there's * no such information, we obviously can't do any following * because we never saw the target go anywhere in the first * place. */ if (getFollowInfo(obj) == nil) { /* we've never heard of the target */ illogicalNow(&followUnknownMsg); } } } /* * Carry out a "follow" command being performed by this actor. */ actorActionFollow(obj) { local canSeeObj; /* note whether or not we can see the target */ canSeeObj = canSee(obj); /* * If we're not the PC, check to see if this is a follow-mode * request; otherwise, try to go to the location of the target. */ if (!isPlayerChar && canSeeObj) { /* * If we're not already following this actor, acknowledge the * request and go into 'follow' mode. If we're already * following this actor, and we didn't issue the command to * ourself, let them know we're already in the requested * mode. Otherwise, ignore it silently - if we issued the * command to ourself, it's because we're just executing our * own 'follow' mode imperative. */ if (followingActor != obj) { /* let them know we're going to follow the actor now */ reportAfter(&okayFollowModeMsg); /* go into follow mode */ followingActor = obj; } else if (gIssuingActor != self) { /* let them know we're already in follow mode */ reportAfter(&alreadyFollowModeMsg); } /* * if we're already in the target's effective follow * location, that's all we need to do */ if (location.effectiveFollowLocation == obj.location.effectiveFollowLocation) return; } /* * If we can see the target, AND we're in the same top-level * location as the target, then simply use a "local travel" * operation to move into the same location. This only works * with targets that are within the same top-level location, * since that's the whole point of the local-travel routines. * For non-local travel, we need to perform a full-fledged travel * command instead. */ if (canSeeObj && isIn(obj.getOutermostRoom())) { /* * We have no information, so we will only have made it past * verification if we can see the other actor from our * current location. Try moving to the other actor's * effective follow location. */ obj.location.effectiveFollowLocation.checkMovingActorInto(true); /* * Since checkMovingActorInto will do its work through * implicit actions, if we're the player character, then the * entire action will have been performed implicitly, so we * won't have a real report for the series of generated * actions, just implied action announcements. If we're an * NPC, on the other hand, we'll generate the full reports, * since NPC implied actions simply show what the actor is * doing. So, if we're the PC, generate an additional * default acknowledgment of the 'follow' action. */ if (isPlayerChar) defaultReport(&okayFollowInSightMsg, location.effectiveFollowLocation); } else { local info; local srcLoc; /* get the information on how to follow the target */ info = getFollowInfo(obj); /* get the effective follow location we have to be in */ srcLoc = info.sourceLocation.effectiveFollowLocation; /* if there's no connector, we can't go anywhere */ if (info.connector == nil) { /* * We have no departure information, so we can't follow * the actor. If we're currently within sight of the * location where we last saw the actor, it means that we * saw the actor here, then went somewhere else, then * came back, and in our absence the actor itself * departed. In this case, report that we don't know * where the actor went. * * If we're not in sight of the location where we last * saw the actor, then instead remind the player of where * that was. */ if (canSee(srcLoc)) { /* * we're where we last saw the actor, but the actor * must have departed while we were away - so we * simply don't know where the actor went */ reportFailure(&followUnknownMsg); } else { /* * we've gone somewhere else since we last saw the * actor, so remind the player of where it was that * we saw the actor */ reportFailure(&cannotFollowFromHereMsg, srcLoc); } /* in any case, that's all we can do now */ return; } /* * Before we can follow the target, we must be in the same * effective location that the target was in when we * observed the target leaving. */ if (location.effectiveFollowLocation != srcLoc) { /* * If we can't even see the effective follow location, we * must have last observed the other actor traveling from * an unrelated location. In this case, simply say that * we don't know where the followee went. */ if (!canSee(srcLoc)) { reportFailure(&cannotFollowFromHereMsg, srcLoc); return; } /* * Try moving into the same location, by invoking the * pre-condition handler for moving me into the * effective follow location from our memory of the * actor's travel. We *could* run this as an actual * precondition, but it's easier to run it here now that * we've sorted out exactly what we want to do. */ srcLoc.checkMovingActorInto(true); } /* perform a TravelVia action on the connector */ nestedAction(TravelVia, info.connector); } } /* * Our list of followable information. Each entry in this list is a * FollowInfo object that tracks a particular followable. */ followables_ = [] /* determine if I've ever seen the given object */ hasSeen(obj) { return obj.(seenProp); } /* mark the object to remember that I've seen it */ setHasSeen(obj) { obj.noteSeenBy(self, seenProp); } /* receive notification that another actor is observing us */ noteSeenBy(actor, prop) { /* do the standard work to remember that we've been seen */ inherited(actor, prop); /* * Update the follow tracking information with the latest * observed location. We're merely observing the fact that the * actor is here, not that the actor is departing, so the * connector is nil. * * The point of noting the actor's presence in the "follow info" * is that we want to replace any previous memory we have of the * actor departing from another location. Now that we know where * the actor is, any old memory of the actor having left another * location is now irrelevant. We only keep track of one "follow * info" record per actor, so this new record will replace any * older record. */ actor.trackFollowInfo(self, nil, location); } /* * Determine if I know about the given object. I know about an * object if it's specifically marked as known to me; I also know * about the object if I can see it now, or if I've ever seen it in * the past. */ knowsAbout(obj) { return canSee(obj) || hasSeen(obj) || obj.(knownProp); } /* mark the object as known to me */ setKnowsAbout(obj) { obj.(knownProp) = true; } /* * My 'seen' property. By default, this is simply 'seen', which * means that we don't distinguish who's seen what - in other words, * there's a single, global 'seen' flag per object, and if anyone's * ever seen something, then we consider that to mean everyone has * seen it. * * Some games might want to track each NPC's sight memory * separately, or at least they might want to track it individually * for a few specific NPC's. You can do this by making up a new * property name for each NPC whose sight memory you want to keep * separate, and simply setting 'seenProp' to that property name for * each such NPC. For example, for Bob, you could make the property * bobHasSeen, so in Bob you'd define 'sightProp = &bobHasSeen'. */ seenProp = &seen /* * My 'known' property. By default, this is simply 'known', which * means that we don't distinguish who knows what. * * As with 'seenProp' above, if you want to keep track of each NPC's * knowledge separately, you must override this property for each * NPC who's to have its own knowledge base to use a separate * property name. For example, if you want to keep track of what * Bob knows individually, you could define 'knownProp = &bobKnows' * in Bob. */ knownProp = &isKnown /* * Determine if the actor recognizes the given object as a "topic," * which is an object that represents some knowledge the actor can * use in conversations, consultations, and the like. * * By default, we'll recognize any Topic object marked as known, and * we'll recognize any game object for which our knowsAbout(obj) * returns true. Games might wish to override this in some cases to * limit or expand an actor's knowledge according to what the actor * has experienced of the setting or story. Note that it's often * easier to control actor knowledge using the lower-level * knowsAbout() and setKnowsAbout() methods, though. */ knowsTopic(obj) { /* we know the object as a topic if we know about it at all */ return knowsAbout(obj); } /* * Determine if the given object is a likely topic for a * conversational action performed by this actor. By default, we'll * return true if the topic is known, nil if not. */ isLikelyTopic(obj) { /* if the object is known, it's a possible topic */ return knowsTopic(obj); } /* we are the owner of any TopicEntry objects contained within us */ getTopicOwner() { return self; } /* * Suggest topics of conversation. This is called by the TOPICS * command (in which case 'explicit' is true), and whenever we first * engage a character in a stateful conversation (in which case * 'explicit' is nil). * * We'll show the list of suggested topics associated with our * current conversational partner. If there are no topics, we'll say * nothing unless 'explicit' is true, in which case we'll simply say * that there are no topics that the player character is thinking * about. * * The purpose of this method is to let the game author keep an * "inventory" of topics with this actor for a given conversational * partner. This inventory is meant to represent the topics that on * the player character's mind - things the player character wants to * talk about with the other actor. Note that we're talking about * what the player *character* is thinking about - obviously we don't * know what's on the player's mind. * * When we enter conversation, or when the player asks for advice, * we'll show this inventory. The idea is to help guide the player * through a conversation without the more heavy-handed device of a * formal conversation menu system, so that conversations have a more * free-form feel without leaving the player hunting in the dark for * the magic ASK ABOUT topic. * * The TOPICS system is entirely optional. If a game doesn't specify * any SuggestedTopic objects, then this routine will simply never be * called, and the TOPICS command won't be allowed. Some authors * think it gives away too much to provide a list of topic * suggestions like this, and others don't like anything that smacks * of a menu system because they think it destroys the illusion * created by the text-input command line that the game is boundless. * Authors who feel this way can just ignore the TOPICS system. But * be aware that the illusion of boundlessness isn't always a good * thing for players; hunting around for ASK ABOUT topics can make * the game's limits just as obvious, if not more so, by exposing the * vast number of inputs for which the actor doesn't have a good * response. Players aren't stupid - a string of variations on "I * don't know about that" is just as obviously mechanistic as a * numbered list of menu choices. Using the TOPICS system might be a * good compromise for many authors, since the topic list can help * guide the player to the right questions without making the player * feel straitjacketed by a menu list. */ suggestTopics(explicit) { local actor; /* * if we're talking to someone, look up their suggested topics; * otherwise, we have nothing to suggest */ if ((actor = getCurrentInterlocutor()) != nil) { /* * we're talking to someone - suggest topics appropriate to * the person we're talking to */ actor.suggestTopicsFor(self, explicit); } else if (explicit) { /* we're not talking to anyone, so there's nothing to suggest */ gLibMessages.noTopicsNotTalking; } } /* * Suggest topics that the given actor might want to talk to us * about. The given actor is almost always the player character, * since generally NPC's don't talk to one another using * conversation commands (there'd be no point; they're simple * programmed automata, not full-blown AI's). */ suggestTopicsFor(actor, explicit) { /* by default, let our state suggest topics */ curState.suggestTopicsFor(actor, explicit); } /* * Receive notification that a command is being carried out in our * presence. */ beforeAction() { /* * If another actor is trying to take something in my inventory, * by default, do not allow it. */ if (gActor != self && (gActionIs(Take) || gActionIs(TakeFrom)) && gDobj.isIn(self)) { /* check to see if we want to allow this action */ checkTakeFromInventory(gActor, gDobj); } /* let our state object take a look at the action */ curState.beforeAction(); } /* * Perform any actor-specific processing for an action. The main * command processor invokes this on gActor after notifying nearby * objects via beforeAction(), but before carrying out the main * action of the command. */ actorAction() { /* do nothing by default */ } /* * Receive notification that a command has just been carried out in * our presence. */ afterAction() { /* let the state object handle it */ curState.afterAction(); } /* receive a notification that someone is about to travel */ beforeTravel(traveler, connector) { /* let the state object handle it */ curState.beforeTravel(traveler, connector); /* * If desired, track the departure so that we can follow the * traveler later. First, track the departure of each actor * traveling with the traveler. */ traveler.forEachTravelingActor( {actor: trackFollowInfo(actor, connector, traveler.location)}); /* * if the traveler is distinct from the actors traveling, track * it as well */ if (!traveler.isActorTraveling(traveler)) trackFollowInfo(traveler, connector, traveler.location); } /* receive a notification that someone has just traveled here */ afterTravel(traveler, connector) { /* let the state object handle it */ curState.afterTravel(traveler, connector); } /* * Receive notification that I'm initiating travel. This is called * on the actor performing the travel action before the travel is * actually carried out. */ actorTravel(traveler, connector) { /* * If other actors are accompanying me on this travel, run the * same travel action on the accompanying actors, using nested * actions. */ if (accompanyingActors != nil && accompanyingActors.length() != 0) { /* * Run the same travel action as a nested action on each * accompanying actor. Skip this for any accompanying actor * we're carrying, as they'll naturally go with us as a * result of being carried. */ foreach (local cur in accompanyingActors) { /* if the actor's not being carried, run the same action */ if (!cur.isIn(self)) nestedActorAction(cur, TravelVia, gDobj); } /* * The accompanying actor list applies for this single group * travel command, so now that we've moved everyone, we have * no further need for the list. Clear it out. */ accompanyingActors.removeRange(1, accompanyingActors.length()); } } /* * Check to see if we want to allow another actor to take something * from my inventory. By default, we won't allow it - we'll always * fail the command. */ checkTakeFromInventory(actor, obj) { /* don't allow it - show an error and terminate the command */ mainReport(&willNotLetGoMsg, self, obj); exit; } /* * Build a list of the objects that are explicitly registered to * receive notification when I'm the actor in a command. */ getActorNotifyList() { return actorNotifyList; } /* * Add an item to our registered notification items. These items * are to receive notifications when we're the actor performing a * command. * * Items can be added here if they must be notified of actions * performed by the actor even when the items aren't connected by * containment with the actor at the time of the action. All items * connected to the actor by containment are automatically notified * of each action; only items that must receive notification even * when not in scope need to be registered here. */ addActorNotifyItem(obj) { actorNotifyList += obj; } /* remove an item from the registered notification list */ removeActorNotifyItem(obj) { actorNotifyList -= obj; } /* our list of registered actor notification items */ actorNotifyList = [] /* * Get the ambient light level in the visual senses at this actor. * This is the ambient level at the actor. */ getVisualAmbient() { local ret; local cache; /* check for a cached value */ if ((cache = libGlobal.actorVisualAmbientCache) != nil && (ret = cache[self]) != nil) { /* found a cached entry - use it */ return ret; } /* get the maximum ambient level at self for my sight-like senses */ ret = senseAmbientMax(sightlikeSenses); /* if caching is active, cache our result for next time */ if (cache != nil) cache[self] = ret; /* return the result */ return ret; } /* * Determine if my location is lit for my sight-like senses. */ isLocationLit() { /* * Check for a simple, common case before doing the full * sense-path calculation: if our location is providing its own * light to its interior, then the location is lit. Most simple * rooms are always lit. */ if (sightlikeSenses.indexOf(sight) != nil && location != nil && location.brightness > 1 && location.transSensingOut(sight) == transparent) return true; /* * We don't have the simple case of light directly from our * location, so run the full sense path check and get our * maximum visual ambience level. If it's above the "self-lit" * level of 1, then we can see. */ return (getVisualAmbient() > 1); } /* * Get the best (most transparent) sense information for one of our * visual senses to the given object. */ bestVisualInfo(obj) { local best; /* we don't have a best value yet */ best = nil; /* check each sight-like sense */ foreach (local sense in sightlikeSenses) { /* * get the information for the object in this sense, and keep * the best (most transparent) info we've seen so far */ best = SenseInfo.selectMoreTrans(best, senseObj(sense, obj)); } /* return the best one we found */ return best; } /* * Build a list of all of the objects of which an actor is aware. * * An actor is aware of an object if the object is within reach of * the actor's senses, and has some sort of presence in that sense. * Note that both of these conditions must be true for at least one * sense possessed by the actor; an object that is within earshot, * but not within reach of any other sense, is in scope only if the * object is making some kind of noise. * * In addition, objects that the actor is holding (i.e., those * contained by the actor directly) are always in scope, regardless * of their reachability through any sense. */ scopeList() { local lst; /* we have nothing in our master list yet */ lst = new Vector(32); /* oneself is always in one's own scope list */ lst.append(self); /* iterate over each sense */ foreach (local sense in scopeSenses) { /* * get the list of objects with a presence in this sense * that can be sensed from our point of view, and and append * it to our master list */ lst.appendUnique(sensePresenceList(sense)); } /* add all of the items we are directly holding */ lst.appendUnique(contents); /* * ask each of our direct contents to add any contents of their * own that are in scope by virtue of their containers being in * scope */ foreach (local cur in contents) cur.appendHeldContents(lst); /* add any items that are specially in scope in the location */ if (location != nil) { /* get the extra scope items */ local extra = location.getExtraScopeItems(self); /* if this is a non-nil list, add it to our list */ if (extra.length() != 0) lst.appendUnique(extra); } /* * Finally, add anything extra each item already in scope wants * to add. Note that we keep going until we've visited each * element of the vector at its current length on each iteration, * so if we add any new items, we'll check them to see if they * want to add any new items, and so on. */ for (local i = 1 ; i <= lst.length() ; ++i) { local extra; /* get the extra scope items for this item */ extra = lst[i].getExtraScopeItems(self); /* if the extra item list is non-nil, add it to our list */ if (extra.length() != 0) lst.appendUnique(extra); } /* return the result */ return lst.toList(); } /* * Determine if I can see the given object. This returns true if * the object can be sensed at all in one of my sight-like senses, * nil if not. */ canSee(obj) { /* try each sight-like sense */ foreach (local sense in sightlikeSenses) { /* * if I can sense the object in this sense, I can sense the * object */ if (senseObj(sense, obj).trans != opaque) return true; } /* we didn't find any sight-like sense where we can see the object */ return nil; } /* * Determine if I can hear the given object. */ canHear(obj) { /* try each hearling-like sense */ foreach (local sense in hearinglikeSenses) { /* * if I can sense the object in this sense, I can sense the * object */ if (senseObj(sense, obj).trans != opaque) return true; } /* we found no hearing-like sense that lets us hear the object */ return nil; } /* * Determine if I can smell the given object. */ canSmell(obj) { /* try each hearling-like sense */ foreach (local sense in smelllikeSenses) { /* * if I can sense the object in this sense, I can sense the * object */ if (senseObj(sense, obj).trans != opaque) return true; } /* we found no smell-like sense that lets us hear the object */ return nil; } /* * Find the object that prevents us from seeing the given object. */ findVisualObstructor(obj) { /* try to find an opaque obstructor in one of our visual senses */ foreach (local sense in sightlikeSenses) { local obs; /* cache path information for this sense */ cacheSenseInfo(connectionTable(), sense); /* if we find an obstructor in this sense, return it */ if ((obs = findOpaqueObstructor(sense, obj)) != nil) return obs; } /* we didn't find any obstructor */ return nil; } /* * Build a table of full sensory information for all of the objects * visible to the actor through the actor's sight-like senses. * Returns a lookup table with the same set of information as * senseInfoTable(). */ visibleInfoTable() { /* return objects visible from my own point of view */ return visibleInfoTableFromPov(self); } /* * Build a table of full sensory information for all of the objects * visible to me from a particular point of view through my * sight-like senses. */ visibleInfoTableFromPov(pov) { local tab; /* we have no master table yet */ tab = nil; /* iterate over each sense */ foreach (local sense in sightlikeSenses) { local cur; /* get information for all objects for the current sense */ cur = pov.senseInfoTable(sense); /* merge the table so far with the new table */ tab = mergeSenseInfoTable(cur, tab); } /* return the result */ return tab; } /* * Build a lookup table of the objects that can be sensed for the * purposes of taking inventory. We'll include everything in the * normal visual sense table, plus everything directly held. */ inventorySenseInfoTable() { local visInfo; local cont; local ambient; local info; /* * Start with the objects visible to the actor through the * actor's sight-like senses. */ visInfo = visibleInfoTable(); /* get the ambient light level at the actor */ if ((info = visInfo[self]) != nil) ambient = info.ambient; else ambient = 0; /* * We'll assume that, for each item that the actor is directly * holding AND knows about, the actor can still identify the item * by touch, even if it's not visible. This way, when we're in a * dark room, we'll still be able to refer to the objects we're * directly holding, as long as we already know about them. * * Likewise, add items within our direct contents that are * considered equally held. */ cont = new Vector(32); foreach (local cur in contents) { /* add this item from our contents */ cont.append(cur); /* add its contents that are themselves equally as held */ cur.appendHeldContents(cont); } /* * Make a fully-sensible entry for each of our held items. We * can simply replace any existing entry in the table that we got * from the visual senses, since a fully transparent entry will * be at least as good as anything we got from the normal visual * list. Only include items that the actor knows about; we'll * assume that we can identify by touch anything we're holding if * we already know what it is, but not otherwise. */ foreach (local cur in cont) { /* if we know about the object, make it effectively visible */ if (knowsAbout(cur)) visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient); } /* return the table */ return visInfo; } /* * Show what the actor is carrying. */ showInventory(tall) { /* * show our inventory with our default listers as given by our * inventory/wearing lister properties */ showInventoryWith(tall, inventoryLister); } /* * Show what the actor is carrying, using the given listers. * * Note that this method must be overridden if the actor does not * use a conventional 'contents' list property to store its full set * of contents. */ showInventoryWith(tall, inventoryLister) { local infoTab; /* get the table of objects sensible for inventory */ infoTab = inventorySenseInfoTable(); /* list in the appropriate mode ("wide" or "tall") */ inventoryLister.showList(self, self, contents, ListRecurse | (tall ? ListTall : 0), 0, infoTab, nil); /* mention sounds coming from inventory items */ inventorySense(sound, inventoryListenLister); /* mention odors coming from inventory items */ inventorySense(smell, inventorySmellLister); } /* * Add to an inventory description a list of things we notice * through a specific sense. */ inventorySense(sense, lister) { local infoTab; local presenceList; /* get the information table for the desired sense */ infoTab = senseInfoTable(sense); /* * get the list of everything with a presence in this sense that * I'm carrying */ presenceList = senseInfoTableSubset(infoTab, {obj, info: obj.isIn(self) && obj.(sense.presenceProp)}); /* add a paragraph break */ cosmeticSpacingReport('<.p>'); /* list the items */ lister.showList(self, nil, presenceList, 0, 0, infoTab, nil); } /* * The Lister object that we use for inventory listings. By * default, we use actorInventoryLister, but this can be overridden * if desired to use a different listing style. */ inventoryLister = actorInventoryLister /* * The Lister for inventory listings, for use in a full description * of the actor. By default, we use the "long form" inventory * lister, on the assumption that most actors have relatively lengthy * descriptive text. This can be overridden to use other formats; * the short-form lister, for example, is useful for actors with only * brief descriptions. */ holdingDescInventoryLister = actorHoldingDescInventoryListerLong /* * Perform library pre-initialization on the actor */ initializeActor() { /* set up an empty pending command list */ pendingCommand = new Vector(5); /* create a default inventory lister if we don't have one already */ if (inventoryLister == nil) inventoryLister = actorInventoryLister; /* create our antecedent tables */ antecedentTable = new LookupTable(8, 8); possAnaphorTable = new LookupTable(8, 8); /* if we don't have a state object, create a default */ if (curState == nil) setCurState(new ActorState(self)); /* create our pending-conversation list */ pendingConv = new Vector(5); } /* * Note conditions before an action or other event. By default, we * note our location and light/dark status, so that we comment on * any change in the light/dark status after the event if we're * still in the same location. */ noteConditionsBefore() { /* note our original location and light/dark status */ locationBefore = location; locationLitBefore = isLocationLit(); } /* * Note conditions after an action or other event. By default, if * we are still in the same location we were in when * noteConditionsBefore() was last called, and the light/dark status * has changed, we'll mention the change in light/dark status. */ noteConditionsAfter() { /* * If our location hasn't changed but our light/dark status has, * note the new status. We don't make any announcement if the * location has changed, since the travel routine will * presumably have shown us the new location's light/dark status * implicitly as part of the description of the new location * after travel. */ if (location == locationBefore && isLocationLit() != locationLitBefore) { /* consider this the start of a new turn */ "<.commandsep>"; /* note the change with a new 'NoteDarkness' action */ newActorAction(self, NoteDarkness); /* * start another turn, in case this occurred during an * implicit action or the like */ "<.commandsep>"; } } /* conditions we noted in noteConditionsBefore() */ locationBefore = nil locationLitBefore = nil /* let the actor have a turn as soon as the game starts */ nextRunTime = 0 /* * Scheduling order - this determines the order of execution when * several items are schedulable at the same game clock time. * * We choose a scheduling order that schedules actors in this * relative order: * * 100 player character, ready to execute *. 200 NPC, ready to execute *. 300 player character, idle *. 400 NPC, idle * * An "idle" actor is one that is waiting for another character to * complete a command, or an NPC with no pending commands to * perform. (For the player character, it doesn't matter whether or * not there's a pending command, because if the PC has no pending * command, we ask the player for one.) * * This ordering ensures that each actor gets a chance to run each * turn, but that actors with work to do go first, and other things * being equal, the player character goes ahead of NPC's. */ scheduleOrder = 100 /* calculate the scheduling order */ calcScheduleOrder() { /* determine if we're ready to run */ if (readyForTurn()) scheduleOrder = isPlayerChar() ? 100 : 200; else scheduleOrder = isPlayerChar() ? 300 : 400; /* return the scheduling order */ return scheduleOrder; } /* * Determine if we're ready to do something on our turn. We're * ready to do something if we're not waiting for another actor to * finish doing something and either we're the player character or * we already have a pending command in our command queue. */ readyForTurn() { /* * if we're waiting for another actor, we're not ready to do * anything */ if (checkWaitingForActor()) return nil; /* * if we're the player character, we're always ready to take a * turn as long as we're not waiting for another actor (which we * now know we're not), because we can either execute one of our * previously queued commands, or we can ask for a new command * to perform */ if (isPlayerChar()) return true; /* * if we have something other than placeholders in our command * queue, we're ready to take a turn, because we can execute the * next command in our queue */ if (pendingCommand.indexWhich({x: x.hasCommand}) != nil) return true; /* * we have no specific work to do, so we're not ready for our * next turn */ return nil; } /* * Check to see if we're waiting for another actor to do something. * Return true if so, nil if not. If we've been waiting for another * actor, and the actor has finished the task we've been waiting for * since the last time we checked, we'll clean up our internal state * relating to the wait and return nil. */ checkWaitingForActor() { local cmdIdx; local idx; /* if we're not waiting for an actor, simply return nil */ if (waitingForActor == nil) return nil; /* * We're waiting for an actor to complete a command. Check to * see if the completion marker is still in the actor's queue; if * it's not, then the other actor has already completed our task. * If the completion marker is in the other actor's queue, but * there are no command entries before it, then we're also done * waiting, because we're not actually waiting for the completion * marker but instead for the tasks that were ahead of it in the * main game execution loop. * * So, find the index of our marker in the queue, and find the * index of the first real command in the queue. If our marker * is still in the queue, and there's a command in the queue * before our marker, the actor we're waiting for still has * things to do before we're ready, so we're still waiting. */ idx = waitingForActor.pendingCommand.indexOf(waitingForInfo); cmdIdx = waitingForActor.pendingCommand.indexWhich({x: x.hasCommand}); if (idx != nil && cmdIdx != nil && idx > cmdIdx) { /* * The marker is still in the queue, and there's at least * one other command ahead of it, so the other actor hasn't * finished the task we've been waiting for. Tell the * caller that we are indeed still waiting for someone. */ return true; } /* * The other actor has disposed of our end-marker (or is about * to, because it's the next thing left in the actor's queue), * so it has finished with all of the commands we have been * waiting for. However, if I haven't caught up in game clock * time with the actor I've been waiting for, I'm still waiting. */ if (waitingForActor.nextRunTime > nextRunTime) return true; /* we're done waiting - forget our wait status information */ waitingForActor = nil; waitingForInfo = nil; /* tell the caller we're no longer waiting for anyone */ return nil; } /* the action the actor performed most recently */ mostRecentAction = nil /* * Add busy time. An action calls this when we are the actor * performing the action, and the action consumes game time. This * marks us as busy for the given time units. */ addBusyTime(action, units) { /* note the action being performed */ mostRecentAction = action; /* adjust the next run time by the busy time */ nextRunTime += units; } /* * When it's our turn and we don't have any command to perform, * we'll call this routine, which can perform a scripted operation * if desired. */ idleTurn() { local tCur = Schedulable.gameClockTime; local origNextRunTime = nextRunTime; /* * if we haven't been targeted for conversation on this turn, * see if we have a conversation we want to start */ if (lastConvTime < tCur) { /* check for a conversation that's ready to go */ local info = pendingConv.valWhich({x: tCur >= x.time_}); /* if we found one, kick it off */ if (info != nil) { /* remove it from the list */ pendingConv.removeElement(info); /* start the conversation */ initiateConversation(info.state_, info.node_); } } /* notify our state object that we're taking a turn */ curState.takeTurn(); /* * If we haven't already adjusted our next run time, consume a * turn, so we're not ready to run again until the next game time * increment. In some cases, we'll already have made this * adjustment; for example, we might have run a nested command * within our state object's takeTurn() method. */ if (nextRunTime == origNextRunTime) ++nextRunTime; } /* * Receive notification that this is a non-idle turn. This is * called whenever a command in our pending command queue is about * to be executed. * * This method need not do anything at all, since the caller will * take care of running the pending command. The purpose of this * method is to take care of any changes an actor wants to make when * it receives an explicit command, as opposed to running its own * autonomous activity. * * By default, we cancel follow mode if it's in effect. It usually * makes sense for an explicit command to interrupt follow mode; * follow mode is usually started by an explicit command in the * first place, so it is usually sensible for a new command to * replace the one that started follow mode. */ nonIdleTurn() { /* by default, cancel follow mode */ followingActor = nil; } /* * If we're following an actor, this keeps track of the actor we're * following. NPC's can use this to follow around another actor * whenever possible. */ followingActor = nil /* * Handle a situation where we're trying to follow an actor but * can't. By default, this simply cancels our follow mode. * * Actors might want to override this to be more tolerant. For * example, an actor might want to wait until five turns elapse to * give up on following, in case the target actor returns after a * brief digression; or an actor could stay in follow mode until it * received other instructions, or found something better to do. */ cannotFollow() { /* * by default, simply cancel follow mode by forgetting about the * actor we're following */ followingActor = nil; } /* * Execute one "turn" - this is a unit of time passing. The player * character generally is allowed to execute one command in the * course of a turn; a non-player character with a programmed task * can perform an increment of the task. * * We set up an ActorTurnAction environment and invoke our * executeActorTurn() method. In most cases, subclasses should * override executeActorTurn() rather than this method, since * overriding executeTurn() directly will lose the action * environment. */ executeTurn() { /* start a new command visually when a new actor is taking over */ "<.commandsep>"; /* * Execute the turn in a daemon action context, and in the sight * context of the actor. The sense context will ensure that we * report the results of the action only if the actor is visible * to the player character; in most cases, the actor's * visibility is equivalent to the visibility of the effects, so * this provides a simple way of ensuring that the results of * the action are reported if and only if they're visible to the * player character. * * Note that if we are the player character, don't use the sense * context filtering -- we normally want full reports for * everything the player character does. */ return withActionEnv(EventAction, self, {: callWithSenseContext(isPlayerChar() ? nil : self, sight, {: executeActorTurn() }) }); } /* * The main processing for an actor's turn. In most cases, * subclasses should override this method (rather than executeTurn) * to specialize an actor's turn processing. */ executeActorTurn() { /* * If we have a pending response, and we're in a position to * deliver it, our next work is to deliver the pending response. */ if (pendingResponse != nil && canTalkTo(pendingResponse.issuer_)) { /* * We have a pending response, and the command issuer from * the pending response can hear us now, so we can finally * deliver the response. * * If the issuer is the player character, send to the player * using our deferred message generator; otherwise, call the * issuer's notification routine, since it's an NPC-to-NPC * notification. */ if (pendingResponse.issuer_.isPlayerChar()) { /* * we're notifying the player - use the deferred message * generator */ getParserDeferredMessageObj().(pendingResponse.prop_)( self, pendingResponse.args_...); } else { /* it's an NPC-to-NPC notification - notify the issuer */ pendingResponse.issuer_.notifyIssuerParseFailure( self, pendingResponse.prop_, pendingResponse.args_); } /* * in either case, we've gotten this out of our system now, * so we can forget about the pending response */ pendingResponse = nil; } /* check to see if we're waiting for another actor */ if (checkWaitingForActor()) { /* * we're still waiting, so there's nothing for us to do; take * an idle turn and return */ idleTurn(); return true; } /* * if we're the player character, and we have no pending commands * to execute, our next task will be to read and execute a * command */ if (pendingCommand.length() == 0 && isPlayerChar()) { local toks; /* read a command line and get the resulting token list */ toks = readMainCommandTokens(rmcCommand); /* * re-activate the main transcript - reading the command * line will have deactivated the transcript, but we want it * active again now that we're about to start executing the * command */ gTranscript.activate(); /* * If it came back nil, it means that the input was fully * processed in pre-parsing; this means that we don't have * any more work to do on this turn, so we can simply end our * turn now. */ if (toks == nil) return true; /* retrieve the token list from the command line */ toks = toks[2]; /* * Add it to our pending command queue. Since we read the * command from the player, and we're the player character, * we treat the command as coming from myself. * * Since this is a newly-read command line, we're starting a * new sentence. */ addPendingCommand(true, self, toks); } /* * Check to see if we have any pending command to execute. If * so, our next task is to execute the pending command. */ if (pendingCommand.length() != 0) { local cmd; /* remove the first pending command from our queue */ cmd = pendingCommand[1]; pendingCommand.removeElementAt(1); /* if this is a real command, note the non-idle turn */ if (cmd.hasCommand) nonIdleTurn(); /* execute the first pending command */ cmd.executePending(self); /* * We're done with this turn. If we no longer have any * pending commands, tell the scheduler to refigure the * execution order, since another object might now be ready * to run ahead of our idle activity. */ if (pendingCommand.indexWhich({x: x.hasCommand}) == nil) return nil; else return true; } /* * If we're following an actor, and the actor isn't in sight, see * if we can catch up. */ if (followingActor != nil && location != nil && (followingActor.location.effectiveFollowLocation != location.effectiveFollowLocation)) { local info; /* see if we have enough information to follow */ info = getFollowInfo(followingActor); /* * Check to see if we have enough information to follow the * actor. We can only follow if we saw the actor depart at * some point, and we're in the same location where we last * saw the actor depart. (We have to be in the same * location, because we follow by performing the same command * we saw the actor perform when we last saw the actor * depart. Repeating the command will obviously be * ineffective unless we're in the same location as the actor * was.) */ if (info != nil) { local success; /* * we know how to follow the actor, so simply perform * the same command we saw the actor perform. */ newActorAction(self, Follow, followingActor); /* note whether or not we succeeded */ success = (location.effectiveFollowLocation == followingActor.location.effectiveFollowLocation); /* notify the state object of our attempt */ curState.justFollowed(success); /* * if we failed to track the actor, note that we are * unable to follow the actor */ if (!success) { /* note that we failed to follow the actor */ cannotFollow(); } /* we're done with this turn */ return true; } else { /* * we don't know how to follow this actor - call our * cannot-follow handler */ cannotFollow(); } } /* we have no pending work to perform, so take an idle turn */ idleTurn(); /* no change in scheduling priority */ return true; } /* * By default, all actors are likely command targets. This should * be overridden for actors who are obviously not likely to accept * commands of any kind. * * This is used to disambiguate target actors in commands, so this * should provide an indication of what should be obvious to a * player, because the purpose of this information is to guess what * the player is likely to take for granted in specifying a target * actor. */ isLikelyCommandTarget = true /* * Determine if we should accept a command. 'issuingActor' is the * actor who issued the command: if the player typed the command on * the command line, this will be the player character actor. * * This routine performs only the simplest check, since it doesn't * have access to the specific action being performed. This is * intended as a first check, to allow us to bypass noun resolution * if the actor simply won't accept any command from the issuer. * * Returns true to accept a command, nil to reject it. If this * routine returns nil, and the command came from the player * character, a suitable message should be displayed. * * Note that most actors should not override this routine simply to * express the will of the actor to accept a command, since this * routine performs a number of checks for the physical ability of * the actor to execute a command from the issuer. To determine * whether or not the actor should obey physically valid commands * from the issuer, override obeyCommand(). */ acceptCommand(issuingActor) { /* if we're the current player character, accept any command */ if (isPlayerChar()) return true; /* if we can't hear the issuer, we can't talk to it */ if (issuingActor != self && !issuingActor.canTalkTo(self)) { /* report that the target actor can't hear the issuer */ reportFailure(&objCannotHearActorMsg, self); /* tell the caller that the command cannot proceed */ return nil; } /* if I'm busy doing something else, say so */ if (nextRunTime > Schedulable.gameClockTime) { /* tell the issuing actor I'm busy */ notifyParseFailure(issuingActor, &refuseCommandBusy, [issuingActor]); /* tell the caller to abandon the command */ return nil; } /* check to see if I have other work to perform first */ if (!acceptCommandBusy(issuingActor)) return nil; /* we didn't find any reason to object, so allow the command */ return true; } /* * Check to see if I'm busy with pending commands, and if so, * whether or not I should accept a new command. Returns true if we * should accept a command, nil if not. If we return nil, we must * notify the issuer of the rejection. * * By default, we won't accept a command if we have any work * pending. */ acceptCommandBusy(issuingActor) { /* if we have any pending commands, don't accept a new command */ if (pendingCommand.length() != 0) { /* * if we have only commands from the same issuer pending, * cancel all of the pending commands and accept the new * command instead */ foreach (local info in pendingCommand) { /* * if this is from a different issuer, don't accept a * new command */ if (info.issuer_ != issuingActor) { /* tell the other actor that we're busy */ notifyParseFailure(issuingActor, &refuseCommandBusy, [issuingActor]); /* tell the caller to abandon the command */ return nil; } } /* * all of the pending commands were from the same issuer, so * presumably the issuer wants to override those commands; * remove the old ones from our pending queue */ pendingCommand.removeRange(1, pendingCommand.length()); } /* we didn't find any problems */ return true; } /* * Determine whether or not we want to obey a command from the given * actor to perform the given action. We only get this far when we * determine that it's possible for us to accept a command, given * the sense connections between us and the issuing actor, and given * our pending command queue. * * When this routine is called, the action has been determined, and * the noun phrases have been resolved. However, we haven't * actually started processing the action yet, so the globals for * the noun slots (gDobj, gIobj, etc) are NOT available. If the * routine needs to know which objects are involved, it must obtain * the full list of resolved objects from the action (using, for * example, getResolvedDobjList()). * * When there's a list of objects to be processed (as in GET ALL), * we haven't started working on any one of them yet - this check is * made once for the entire command, and applies to the entire list * of objects. If the actor wants to respond specially to * individual objects, you can do that by overriding actorAction() * instead of this routine. * * This routine should display an appropriate message and return nil * if the command is not to be accepted, and should simply return * true to accept the command. * * By default, we'll let our state object handle this. * * Note that actors that override this might also need to override * wantsFollowInfo(), since an actor that accepts "follow" commands * will need to keep track of the movements of other actors if it is * to carry out any following. */ obeyCommand(issuingActor, action) { /* note that the issuing actor is targeting me in conversation */ issuingActor.noteConversation(self); /* let the state object handle it */ return curState.obeyCommand(issuingActor, action); } /* * Say hello/goodbye/yes/no to the given actor. We'll greet the * target actor is the target actor was specified (i.e., actor != * self); otherwise, we'll greet our current default conversational * partner, if we have one. */ sayHello(actor) { sayToActor(actor, helloTopicObj, helloConvType); } sayGoodbye(actor) { sayToActor(actor, byeTopicObj, byeConvType); } sayYes(actor) { sayToActor(actor, yesTopicObj, yesConvType); } sayNo(actor) { sayToActor(actor, noTopicObj, noConvType); } /* handle one of the conversational addresses */ sayToActor(actor, topic, convType) { /* * If the target actor is the same as the issuing actor, then no * target actor was specified in the command, so direct the * address to our current conversational partner, if we have * one. */ if (actor == self) actor = getDefaultInterlocutor(); /* * if we found an actor, send the address to the actor's state * object; otherwise, handle it with the given default message */ if (actor != nil) { /* make sure we can talk to the other actor */ if (!canTalkTo(actor)) { /* can't talk to them - say so and give up */ reportFailure(&objCannotHearActorMsg, actor); exit; } /* remember our current conversational partner */ noteConversation(actor); /* handle it as a topic */ actor.curState.handleConversation(self, topic, convType); } else { /* * we don't know whom we're addressing; just show the default * message for an unknown interlocutor */ mainReport(convType.unknownMsg); } } /* * Handle the XSPCLTOPIC pseudo-command. This command is generated * by the SpecialTopic pre-parser when it recognizes the player's * input as matching an active SpecialTopic's custom syntax. Our * job is to route this back to our current interlocutor's active * ConvNode, so that it can find the SpecialTopic that it matched in * pre-parsing and show its response. */ saySpecialTopic() { local actor; /* send it to our interlocutor */ if ((actor = getCurrentInterlocutor()) == nil || actor.curConvNode == nil) { /* * We don't seem to have a current interlocutor, or the * interlocutor doesn't have a current conversation node. * This is inconsistent; there's no way we could have * generated XSPCLTOPIC from our pre-parser under these * conditions. The most likely thing is that the player * tried typing in XSPCLTOPIC manually. Politely ignore it. */ gLibMessages.commandNotPresent; } else { /* note the conversation directed to the other actor */ noteConversation(actor); /* send the request to the ConvNode for processing */ actor.curConvNode.saySpecialTopic(self); } } /* * Add a command to our pending command list. The new command is * specified as a list of tokens to be parsed, and it is added after * any commands already in our pending list. */ addPendingCommand(startOfSentence, issuer, toks) { /* add a descriptor to the pending command list */ pendingCommand.append( new PendingCommandToks(startOfSentence, issuer, toks)); } /* * Insert a command at the head of our pending command list. The * new command is specified as a list of tokens to parse, and it is * inserted into our pending command list before any commands * already in the list. */ addFirstPendingCommand(startOfSentence, issuer, toks) { /* add a descriptor to the start of our list */ pendingCommand.insertAt( 1, new PendingCommandToks(startOfSentence, issuer, toks)); } /* * Add a resolved action to our pending command list. The new * command is specified as a resolved Action object; it is added * after any commands already in our list. */ addPendingAction(startOfSentence, issuer, action, [objs]) { /* add a descriptor to the pending command list */ pendingCommand.append(new PendingCommandAction( startOfSentence, issuer, action, objs...)); } /* * Insert a resolved action at the start of our pending command * list. The new command is specified as a resolved Action object; * it is added before any commands already in our list. */ addFirstPendingAction(startOfSentence, issuer, action, [objs]) { /* add a descriptor to the pending command list */ pendingCommand.insertAt(1, new PendingCommandAction( startOfSentence, issuer, action, objs...)); } /* pending commands - this is a list of PendingCommandInfo objects */ pendingCommand = nil /* * pending response - this is a single PendingResponseInfo object, * which we'll deliver as soon as the issuing actor is in a position * to hear us */ pendingResponse = nil /* * get the library message object for a parser message addressed to * the player character */ getParserMessageObj() { /* * If I'm the player character, use the player character message * object; otherwise, use the default non-player character * message object. * * To customize parser messages from a particular actor, create * an object based on npcMessages, and override this routine in * the actor so that it returns the custom object rather than * the standard npcMessages object. To customize messages for * ALL of the NPC's in a game, simply modify npcMessages itself, * since it's the default for all non-player characters. */ return isPlayerChar() ? playerMessages : npcMessages; } /* * Get the deferred library message object for a parser message * addressed to the player character. We only use this to generate * messages deferred from non-player characters. */ getParserDeferredMessageObj() { return npcDeferredMessages; } /* * Get the library message object for action responses. This is * used to generate library responses to verbs. */ getActionMessageObj() { /* * return the default player character or NPC message object, * depending on whether I'm the player or not; individual actors * can override this to supply actor-specific messages for * library action responses */ return isPlayerChar() ? playerActionMessages: npcActionMessages; } /* * Notify an issuer that a command sent to us resulted in a parsing * failure. We are meant to reply to the issuer to let the issuer * know about the problem. messageProp is the libGlobal message * property describing the error, and args is a list with the * (varargs) arguments to the message property. */ notifyParseFailure(issuingActor, messageProp, args) { /* * In case the actor is in a remote location but in scope for the * purposes of the conversation only (such as over a phone or * radio), run this in a neutral sense context. Since we're * reporting a parser failure, we want the message to be * displayed no matter what the scope situation is. */ callWithSenseContext(nil, nil, function() { /* check who's talking to whom */ if (issuingActor.isPlayerChar()) { /* * The player issued the command. If the command was * directed to an NPC (i.e., we're not the player), check * to see if the player character is in scope from our * perspective. */ if (issuingActor != self && !canTalkTo(issuingActor)) { /* * The player issued the command to an NPC, but the * player is not capable of hearing the NPC's * response. */ cannotRespondToCommand(issuingActor, messageProp, args); } else { /* * generate a message using the appropriate message * generator object */ getParserMessageObj().(messageProp)(self, args...); } } else { /* * the command was issued from one NPC to another - * notify the issuer of the problem, but don't display * any messages, since this interaction is purely among * the NPC's */ issuingActor. notifyIssuerParseFailure(self, messageProp, args); } }); } /* * We have a parser error to report to the player, but we cannot * respond at the moment because the player is not capable of * hearing us (there is no sense path for our communications senses * from us to the player actor). Defer reporting the message until * later. */ cannotRespondToCommand(issuingActor, messageProp, args) { /* * Remember the problem for later deliver. If we already have a * deferred response, forget it - just report the latest * problem. */ pendingResponse = new PendingResponseInfo(issuingActor, messageProp, args); /* * Some actors might want to override this to start searching * for the player character. We don't have any generic * mechanism to conduct such a search, but a game that * implements one might want to make use of it here. */ } /* * Receive notification that a command we sent to another NPC * failed. This is only called when one NPC sends a command to * another NPC; this is called on the issuer to let the issuer know * that the target can't perform the command because of the given * resolution failure. * * By default, we don't do anything here, because we don't have any * default code to send a command from one NPC to another. Any * custom NPC actor that sends a command to another NPC actor might * want to use this to deal with problems in processing those * commands. */ notifyIssuerParseFailure(targetActor, messageProp, args) { /* by default, we do nothing */ } /* * Antecedent lookup table. Each actor keeps its own table of * antecedents indexed by pronoun type, so that we can * simultaneously have different antecedents for different pronouns. */ antecedentTable = nil /* * Possessive anaphor lookup table. In almost all cases, the * possessive anaphor for a given pronoun will be the same as the * corresponding regular pronoun: HIS indicates possession by HIM, * for example. In a few cases, though, the anaphoric quality of * possessives takes precedence, and these will differ. For * example, in TELL BOB TO DROP HIS BOOK, "his" refers back to Bob, * while in TELL BOB TO HIT HIM, "him" refers to whatever it * referred to before the command. */ possAnaphorTable = nil /* * set the antecedent for the neuter singular pronoun ("it" in * English) */ setIt(obj) { setPronounAntecedent(PronounIt, obj); } /* set the antecedent for the masculine singular ("him") */ setHim(obj) { setPronounAntecedent(PronounHim, obj); } /* set the antecedent for the feminine singular ("her") */ setHer(obj) { setPronounAntecedent(PronounHer, obj); } /* set the antecedent list for the ungendered plural pronoun ("them") */ setThem(lst) { setPronounAntecedent(PronounThem, lst); } /* look up a pronoun's value */ getPronounAntecedent(typ) { /* get the stored antecedent for this pronoun */ return antecedentTable[typ]; } /* set a pronoun's antecedent value */ setPronounAntecedent(typ, val) { /* remember the value in the antecedent table */ antecedentTable[typ] = val; /* set the same value for the possessive anaphor */ possAnaphorTable[typ] = val; } /* set a possessive anaphor value */ setPossAnaphor(typ, val) { /* set the value in the possessive anaphor table only */ possAnaphorTable[typ] = val; } /* get a possessive anaphor value */ getPossAnaphor(typ) { return possAnaphorTable[typ]; } /* forget the possessive anaphors */ forgetPossAnaphors() { /* copy all of the antecedents to the possessive anaphor table */ antecedentTable.forEachAssoc( {key, val: possAnaphorTable[key] = val}); } /* * Copy pronoun antecedents from the given actor. This should be * called whenever an actor issues a command to us, so that pronouns * in the command are properly resolved relative to the issuer. */ copyPronounAntecedentsFrom(issuer) { /* copy every element from the issuer's table */ issuer.antecedentTable.forEachAssoc( {key, val: setPronounAntecedent(key, val)}); } /* -------------------------------------------------------------------- */ /* * Verb processing */ /* show a "take from" message as indicating I don't have the dobj */ takeFromNotInMessage = &takeFromNotInActorMsg /* verify() handler to check against applying an action to 'self' */ verifyNotSelf(msg) { /* check to make sure we're not trying to do this to myself */ if (self == gActor) illogicalSelf(msg); } /* macro to verify we're not self, and inherit the default behavior */ #define verifyNotSelfInherit(msg) \ verify() \ { \ verifyNotSelf(msg); \ inherited(); \ } /* * For the basic physical manipulation verbs (TAKE, DROP, PUT ON, * etc), it's illogical to operate on myself, so check for this in * verify(). Otherwise, handle these as we would ordinary objects, * since we might be able to manipulate other actors in the normal * manner, especially actors small enough that we can pick them up. */ dobjFor(Take) { verifyNotSelfInherit(&takingSelfMsg) } dobjFor(Drop) { verifyNotSelfInherit(&droppingSelfMsg) } dobjFor(PutOn) { verifyNotSelfInherit(&puttingSelfMsg) } dobjFor(PutUnder) { verifyNotSelfInherit(&puttingSelfMsg) } dobjFor(Throw) { verifyNotSelfInherit(&throwingSelfMsg) } dobjFor(ThrowAt) { verifyNotSelfInherit(&throwingSelfMsg) } dobjFor(ThrowDir) { verifyNotSelfInherit(&throwingSelfMsg) } dobjFor(ThrowTo) { verifyNotSelfInherit(&throwingSelfMsg) } /* customize the message for THROW TO <actor> */ iobjFor(ThrowTo) { verify() { /* by default, we don't want to catch anything */ illogical(&willNotCatchMsg, self); } } /* treat PUT SELF IN FOO as GET IN FOO */ dobjFor(PutIn) { verify() { /* the target actor is always unsuitable as a default */ if (gActor == self) nonObvious; } check() { /* if I'm putting myself somewhere, treat it as GET IN */ if (gActor == self) replaceAction(Enter, gIobj); /* do the normal work */ inherited(); } } dobjFor(Kiss) { preCond = [touchObj] verify() { /* cannot kiss oneself */ verifyNotSelf(&cannotKissSelfMsg); } action() { mainReport(&cannotKissActorMsg); } } dobjFor(AskFor) { preCond = [canTalkToObj] verify() { /* it makes no sense to ask myself for something */ verifyNotSelf(&cannotAskSelfForMsg); } action() { /* note that the issuer is targeting us with conversation */ gActor.noteConversation(self); /* let the state object handle it */ curState.handleConversation(gActor, gTopic, askForConvType); } } dobjFor(TalkTo) { preCond = [canTalkToObj] verify() { /* it's generally illogical to talk to oneself */ verifyNotSelf(&cannotTalkToSelfMsg); } action() { /* note that the issuer is targeting us in conversation */ gActor.noteConversation(self); /* handle it as a 'hello' topic */ curState.handleConversation(gActor, helloTopicObj, helloConvType); } } iobjFor(GiveTo) { verify() { /* it makes no sense to give something to myself */ verifyNotSelf(&cannotGiveToSelfMsg); /* it also makes no sense to give something to itself */ if (gDobj == gIobj) illogicalSelf(&cannotGiveToItselfMsg); } action() { /* take note that I've seen the direct object */ noteObjectShown(gDobj); /* note that the issuer is targeting us with conversation */ gActor.noteConversation(self); /* let the state object handle it */ curState.handleConversation(gActor, gDobj, giveConvType); } } iobjFor(ShowTo) { verify() { /* it makes no sense to show something to myself */ verifyNotSelf(&cannotShowToSelfMsg); /* it also makes no sense to show something to itself */ if (gDobj == gIobj) illogicalSelf(&cannotShowToItselfMsg); } action() { /* take note that I've seen the direct object */ noteObjectShown(gDobj); /* note that the issuer is targeting us with conversation */ gActor.noteConversation(self); /* let the actor state object handle it */ curState.handleConversation(gActor, gDobj, showConvType); } } /* * Note that the given object has been explicitly shown to me. By * default, we'll mark the object and its visible contents as having * been seen by me. This is called whenever we're the target of a * SHOW TO or GIVE TO, since presumably such an explicit act of * calling our attention to an object would make us consider the * object as having been seen in the future. */ noteObjectShown(obj) { local info; /* get the table of things we can see */ info = visibleInfoTable(); /* if the object is in the table, mark it as seen */ if (info[obj] != nil) setHasSeen(obj); /* also mark the visible contents of the object as having been seen */ obj.setContentsSeenBy(info, self); } dobjFor(AskAbout) { preCond = [canTalkToObj] verify() { /* it makes no sense to ask oneself about something */ verifyNotSelf(&cannotAskSelfMsg); } action() { /* note that the issuer is targeting us with conversation */ gActor.noteConversation(self); /* let our state object handle it */ curState.handleConversation(gActor, gTopic, askAboutConvType); } } dobjFor(TellAbout) { preCond = [canTalkToObj] verify() { /* it makes no sense to tell oneself about something */ verifyNotSelf(&cannotTellSelfMsg); } check() { /* * If the direct object is the issuing actor, rephrase this * as "issuer, ask actor about iobj". * * Note that we do this in 'check' rather than 'action', * because this will ensure that we'll rephrase the command * properly even if the subclass overrides with its own * check, AS LONG AS the overriding method inherits this base * definition first. If we did the rephrasing in the * 'action', then an overriding 'check' might incorrectly * disqualify the operation on the assumption that it's an * ordinary TELL ABOUT rather than what it really is, which * is a rephrased ASK ABOUT. */ if (gDobj == gIssuingActor) replaceActorAction(gIssuingActor, AskAbout, gActor, gTopic); } action() { /* note that the issuer is targeting us with conversation */ gActor.noteConversation(self); /* let the state object handle it */ curState.handleConversation(gActor, gTopic, tellAboutConvType); } } /* * Handle a conversational command. All of the conversational * actions (HELLO, GOODBYE, YES, NO, ASK ABOUT, ASK FOR, TELL ABOUT, * SHOW TO, GIVE TO) are routed here when we're the target of the * action (for example, we're BOB in ASK BOB ABOUT TOPIC) AND the * ActorState doesn't want to handle the action. */ handleConversation(actor, topic, convType) { /* try handling the topic from our topic database */ if (!handleTopic(actor, topic, convType, nil)) { /* the topic database didn't handle it; use a default response */ defaultConvResponse(actor, topic, convType); } } /* * Show a default response to a conversational action. By default, * we'll show the default response for our conversation type. */ defaultConvResponse(actor, topic, convType) { /* call the appropriate default response for the ConvType */ convType.defaultResponse(self, actor, topic); } /* * Show our default greeting message - this is used when the given * another actor greets us with HELLO or TALK TO, and we don't * otherwise handle it (such as via a topic database entry). * * By default, we'll just show "there's no response" as a default * message. We'll show this in default mode, so that if the caller * is going to show a list of suggested conversation topics (which * the 'hello' and 'talk to' commands will normally try to do), the * topic list will override the "there's no response" default. In * other words, we'll have one of these two types of exchanges: * *. >talk to bob *. There's no response * *. >talk to bill *. You could ask him about the candle, the book, or the bell, or *. tell him about the crypt. */ defaultGreetingResponse(actor) { defaultReport(&noResponseFromMsg, self); } /* show our default goodbye message */ defaultGoodbyeResponse(actor) { mainReport(&noResponseFromMsg, self); } /* * Show the default answer to a question - this is called when we're * the actor in ASK <actor> ABOUT <topic>, and we can't find a more * specific response for the given topic. * * By default, we'll show the basic "there's no response" message. * This isn't a very good message in most cases, because it makes an * actor pretty frustratingly un-interactive, which gives the actor * the appearance of a cardboard cut-out. But there's not much * better that the library can do; the potential range of actors * makes a more specific default response impossible. If the default * response were "I don't know about that," it wouldn't work very * well if the actor is someone who only speaks Italian. So, the * best we can do is this generally rather poor default. But that * doesn't mean that authors should resign themselves to a poor * default answer; instead, it means that actors should take care to * override this when defining an actor, because it's usually * possible to find a much better default for a *specific* actor. * * The *usual* way of providing a default response is to define a * DefaultAskTopic (or a DefaultAskTellTopic) and put it in the * actor's topic database. */ defaultAskResponse(fromActor, topic) { mainReport(&noResponseFromMsg, self); } /* * Show the default response to being told of a topic - this is * called when we're the actor in TELL <actor> ABOUT <topic>, and we * can't find a more specific response for the topic. * * As with defaultAskResponse, this should almost always be * overridden by each actor, since the default response ("there's no * response") doesn't make the actor seem very dynamic. * * The usual way of providing a default response is to define a * DefaultTellTopic (or a DefaultAskTellTopic) and put it in the * actor's topic database. */ defaultTellResponse(fromActor, topic) { mainReport(&noResponseFromMsg, self); } /* the default response for SHOW TO */ defaultShowResponse(byActor, topic) { mainReport(¬InterestedMsg, self); } /* the default response for GIVE TO */ defaultGiveResponse(byActor, topic) { mainReport(¬InterestedMsg, self); } /* the default response for ASK FOR */ defaultAskForResponse(byActor, obj) { mainReport(&noResponseFromMsg, self); } /* default response to being told YES */ defaultYesResponse(fromActor) { mainReport(&noResponseFromMsg, self); } /* default response to being told NO */ defaultNoResponse(fromActor) { mainReport(&noResponseFromMsg, self); } /* default refusal of a command */ defaultCommandResponse(fromActor, topic) { mainReport(&refuseCommand, self, fromActor); } ; /* ------------------------------------------------------------------------ */ /* * An UntakeableActor is one that can't be picked up and moved. */ class UntakeableActor: Actor, Immovable /* use customized messages for some 'Immovable' methods */ cannotTakeMsg = &cannotTakeActorMsg cannotMoveMsg = &cannotMoveActorMsg cannotPutMsg = &cannotPutActorMsg /* TASTE tends to be a bit rude */ dobjFor(Taste) { action() { mainReport(&cannotTasteActorMsg); } } /* * even though we act like an Immovable, we don't count as an * Immovable for listing purposes */ contentsInFixedIn(loc) { return nil; } ; /* * A Person is an actor that represents a human character. This is just * an UntakeableActor with some custom versions of the messages for * taking and moving the actor. */ class Person: UntakeableActor /* customize the messages for trying to take or move me */ cannotTakeMsg = &cannotTakePersonMsg cannotMoveMsg = &cannotMovePersonMsg cannotPutMsg = &cannotPutPersonMsg cannotTasteActorMsg = &cannotTastePersonMsg /* * use a fairly large default bulk, since people are usually fairly * large compared with the sorts of items that one carries around */ bulk = 10 ; /* ------------------------------------------------------------------------ */ /* * Pending response information structure */ class PendingResponseInfo: object construct(issuer, prop, args) { issuer_ = issuer; prop_ = prop; args_ = args; } /* the issuer of the command (and target of the response) */ issuer_ = nil /* the message property and argument list for the message */ prop_ = nil args_ = [] ; /* * Pending Command Information structure. This is an abstract base class * that we subclass for particular ways of representing the command to be * executed. */ class PendingCommandInfo: object construct(issuer) { issuer_ = issuer; } /* * Check to see if this pending command item has a command to * perform. This returns true if we have a command, nil if we're * just a queue placeholder without any actual command to execute. */ hasCommand = true /* execute the command */ executePending(targetActor) { } /* the issuer of the command */ issuer_ = nil /* we're at the start of a "sentence" */ startOfSentence_ = nil ; /* a pending command based on a list of tokens from an input string */ class PendingCommandToks: PendingCommandInfo construct(startOfSentence, issuer, toks) { inherited(issuer); startOfSentence_ = startOfSentence; tokens_ = toks; } /* * Execute the command. We'll parse our tokens and execute the * parsed results. */ executePending(targetActor) { /* parse and execute the tokens */ executeCommand(targetActor, issuer_, tokens_, startOfSentence_); } /* the token list for the command */ tokens_ = nil ; /* a pending command based on a pre-resolved Action and its objects */ class PendingCommandAction: PendingCommandInfo construct(startOfSentence, issuer, action, [objs]) { inherited(issuer); startOfSentence_ = startOfSentence; action_ = action; objs_ = objs; } /* execute the pending command */ executePending(targetActor) { /* invoke the action's main execution method */ try { /* run the action */ newActionObj(CommandTranscript, issuer_, targetActor, action_, objs_...); } catch (TerminateCommandException tcExc) { /* * the command cannot proceed; simply abandon the command * action here */ } } /* the resolved Action to perform */ action_ = nil /* the resolved objects for the action */ objs_ = nil ; /* * A pending command marker. This is not an actual pending command; * rather, it's just a queue marker. We sometimes want to synchronize * some other activity with an actor's progress through its command * queue; for example, we might want one actor to wait until another * actor has executed a particular pending action. These markers can be * used for this kind of synchronization; they move through the queue * like ordinary pending commands, so we can tell if an actor has reached * a particular command by observing the marker's progress through the * queue. */ class PendingCommandMarker: PendingCommandInfo /* I have no command to execute */ hasCommand = nil ; /* ------------------------------------------------------------------------ */ /* * Set the current player character */ setPlayer(actor) { /* remember the new player character */ libGlobal.playerChar = actor; /* set the root global point of view to this actor */ setRootPOV(actor, actor); }
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