actor.t
#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