/* asktell.t: ASK/TELL-based conversation system for TADS
* V1.0
* Suzanne Skinner, 1999, Public Domain
* [email protected]
*
* This file implements a topic-based ask/tell system similar to that used
* used by WorldClass. It also implements information sources (i.e. things
* that let you look up information), since they are a simple and obvious
* addition. It requires TADS 2.4 at minimum (since it uses 2.4's new
* disambiguation hook, disambigXobj). Preferably, you should use a patched
* or later version of TADS, since 2.4 has a glitch which will cause
* disambiguation questions to look odd.
*
* To use asktell.t, include it after adv.t in your main source file. See
* the comments in the source below, especially for the superclasses topic,
* ioTopicVerb, and movableActor, to learn how to implement conversations
* using this library.
*
* This file uses #pragma C+, but sets back to #pragma C- at the end.
*
* Features:
*
*   + Full disambiguation: If you "ask so-and-so about tree", and there
*     are two different trees you might be asking about, the game will
*     respond with a disambiguation question: just as with other verbs.
*
*   + Topic-based: Indirect objects for ASK/TELL are scoped to allow
*     topics only. Non-topic objects will never show up in disambiguation
*     questions.
*
*   + Knowledge-based: A topic may be known or unknown at a given time.
*     Unknown topics will also never show up in disambiguation questions.
*
*   + Mimesis-preserving: If the player uses vocabulary that doesn't match
*     any topics, the npc's "disavow" property will be output, instead of
*     a cryptic message such as "something tells you that won't be a very
*     productive topic". disambigXobj allows us to accomplish this.
*
* Tips:
*
*   + Be careful of unknown topics. If you have a topic that can be
*     referred to by a very general noun, e.g. "enchanted tree" (which
*     can be called simply "tree" by the player), it may be best to make
*     it a knownTopic. Or, make sure there is a general known topic that
*     matches that noun from the start (e.g. a "forest"
*     object). Otherwise, the player may get peeved by the seemingly
*     erroneous "you don't know about that" message.
*
*   + WorldClass sets up carryable items to be topics by default. I
*     recommend against this, at least in a large game (in my own game,
*     there are often 3-4 different versions of the same item floating
*     around). Keep the topic system separate to help insure that no
*     impossible disambiguation questions will show up ("Which ball do
*     you mean, the ball, the ball, or the ball?").
*
*   + This library assumes that only topics draw a distinction between
*     known and unknown. But if you prefer a WCish world in which
*     *anything* can be unknown (and therefore, "ask about [unknown
*     non-topic]" should result in "you don't know about that"), the
*     changes shouldn't be hard to make. Just fiddle with disambigIobj.
*
*   + Scenario: You have in your game a red gem and a green sword. But
*     nowhere does there exist a green gem. What happens when the player
*     types "ask so-and-so about green gem"? This:
*
*        I don't see any green gem here.
*
*     This happens in any TADS implementation of ASK/TELL. It is the
*     catchall error message displayed when a non-existent noun/adjective
*     combination is used. This is an artifact of how TADS is set up
*     internally: it favors local-scope verbs, for which that error *does*
*     make sense. You can change the message (#9) using parseError to say
*     something more meaningful (e.g. "I don't know of any such thing"),
*     but there are complications: the same error message is used for
*     other situations of a different nature. Also, there is no practical
*     way (that I know of) to determine what verb was used in this case.
*     It is handled too early in the parsing process.
*
*     It's possible to fix this somewhat, but tricky. I won't go into
*     details here, but if enough people ask, i can add the solution into
*     this source file.
*/

#pragma C+

/*************************** New Superclasses ***************************/

// topic: something which can be asked about, told about, or looked up in
// information sources. The code for scoping and disambiguation with topics
// is largely contained in ioTopicVerb.
//
// Important properties:
//   + known: indicates whether the player currently knows about this
//     topic. If nil, it will never show up in disambiguation questions,
//     and the player cannot get information by asking about it.
//   + unknownMsg: the message printed when the player's vocabulary matches
//     only unknown topic(s).
class topic: thing
 known = nil
 unknownMsg = "You don't know about that."

 location = nil
;

// knownTopic: a topic which is known from the beginning.
//
// Important properties: none
class knownTopic: topic
 known = true
;

// infoSource: something the player can look things up in.
//
// Important properties:
//   + askTopics(topic, words): This works the same as askTopics in
//     movableActor. "topic" is the topic asked about, and "words" are the
//     vocabulary words that were used to refer to it. The method should
//     output text and return true if the infoSource has an entry for that
//     topic, otherwise return nil. This method will never be called with
//     an unknown topic.
//   + disavow: This method will be printed whenever askTopics returns nil.
class infoSource: thing
 askTopics(topic, words) = {return nil;}
 disavow = "There's no entry for that topic."

 verIoLookupIn(actor) = {
   if (self.location != Me)
     "You're not holding <<self.thedesc>>.";
 }
 ioLookupIn(actor, dobj) = {
   // We have to handle catchallUnknownTopic here, unlike with other
   // verbs:
   if (dobj == catchallUnknownTopic)
     topic.unknownMsg;
   else if (!self.askTopics(dobj, objwords(1)))
     self.disavow;
 }

 verDoConsultOn(actor, io) = {self.verIoLookupIn(actor);}
 doConsultOn(actor, io) = {
   if (!self.askTopics(io, objwords(2)))
     self.disavow;
 }
;

// ioTopicVerb: a verb which uses topics as indirect objects. Non-topics
// will never show up in disambiguation questions. validIo and validIoList
// are not used for scoping, nor are verIoAskAbout and the like used.
// Instead, all disambiguation is done within disambigIobj. If no topics
// match the player's input, that method will return the special catchall
// topic catchallNonTopic. Since no NPC's or infoSources know about this
// topic, it will simply cause a "disavow" to be printed. Similarly, if
// topics match but no *known* topics match, catchallUnknownTopic will be
// returned, which will cause a reply of "you don't know about that" to
// any type of query (ask, tell, consult, look up).
//
// Important properties: none
class ioTopicVerb: deepverb
 validIoList(actor, prep, dobj) = (nil)
 validIo(actor, obj, seqno) = true
 ioDefault(actor, prep) = (nil)

 disambigIobj(actor, prep, dobj, verprop, wordlist, objlist, flaglist,
              numberWanted, isAmbiguous, silent) = {
   local i, len;
   local newlist = [];
   local unknownTopicsFound = nil;

   len = length(objlist);
   for (i=1; i <= len; i++) {
     if (isclass(objlist[i], topic)) {
       if (objlist[i].known)
         newlist += objlist[i];
       else
         unknownTopicsFound = true;
     }
   }
   if (length(newlist) < 1) {
     if (unknownTopicsFound)
       newlist += catchallUnknownTopic;
     else
       newlist += catchallNonTopic;
   }
   return newlist;
 }
;

// doTopicVerb: a verb which uses topics as direct objects. disambigDobj
// simply calls disambigIobj on ioTopicVerb. See ioTopicVerb for more
// details.
//
// Important properties: none
class doTopicVerb: deepverb
 validDoList(actor, prep, io) = (nil)
 validDo(actor, obj, seqno) = true
 doDefault(actor, prep, io) = (nil)

 // Allowing multiple objects can cause erroneous sdescs
 // (e.g. catchallNonTopic.sdesc) to get printed.
 rejectMultiDobj(prep) = {
   "You can't use multiple objects with that verb.";
   return true;
 }

 disambigDobj(actor, prep, io, verprop, wordlist, objlist, flaglist,
              numberWanted, isAmbiguous, silent) = {
   return ioTopicVerb.disambigIobj(actor, prep, io, verprop, wordlist,
          objlist, flaglist, numberWanted, isAmbiguous, silent);
 }
;

/************************** adv.t Modifications **************************/

// Modifications to thing for default responses to ask, tell, consult,
// look up, and redirections of ioConsultOn to doConsultOn, ioAskFor to
// doAskFor.
//
// Important properties: none, unless you want to change default responses.
modify thing
 replace verDoAskAbout(actor, io) = {"There is no response.";}
 verIoAskFor(actor) = {}
 verDoAskFor(actor, io) = {"There is no response.";}
 ioAskFor(actor, dobj) = {dobj.doAskFor(actor, self);}

 replace verDoTellAbout(actor, io)= {"There is no response.";}

 verIoConsultOn(actor) = {}
 verDoConsultOn(actor, io) = {"That's not an information source.";}
 ioConsultOn(actor, dobj) = {dobj.doConsultOn(actor, self);}

 verIoLookupIn(actor) = {"That's not an information source.";}
 verDoLookupIn(actor, io) = {}
;

// Modifications to movableActor (parent class for all actors)
//
// Important properties:
//   + askTopics(topic, words): This method should output text and return
//     true for a valid topic, otherwise return nil, in which case disavow
//     will be printed by doAskAbout. The specific vocabulary words used to
//     refer to the topic are passed in the "words" parameter.
//   + tellTopics(topic, words): works almost identically to askTopics.
//   + doAskFor(actor, io): You should override this method if you want to
//     allow the player to ask this NPC *for* something. Like "ask about",
//     it takes topics only. By default, it simply outputs self.disavow.
//   + disavow: default changed to "There is no response.". This method will
//     be called if askTopics returns nil.
//   + tellDisavow: calls disavow by default. This method will be called if
//     tellTopics returns nil.
modify movableActor
 askTopics(topic, words) = {return nil;}
 tellTopics(topic, words) = {return nil;}
 doAskFor(actor, io) = {self.disavow;}
 replace disavow = "There is no response."
 tellDisavow = {self.disavow;}

 replace doAskAbout(actor, io) = {
   if (!self.askTopics(io, objwords(2)))
     self.disavow;
 }

 verDoTellAbout(actor, io) = {}
 doTellAbout(actor, io) = {
   if (!self.tellTopics(io, objwords(2)))
     self.tellDisavow;
 }

 verDoAskFor(actor, io) = {}
;

/**************************** Special Objects ****************************/

// catchallNonTopic: If the game is expecting a topic, and the player
// enters vocabulary that does not match any objects of the topic class
// (known or unknown), then the disambiguation function on ioTopicVerb or
// doTopicVerb will return a single-item list consisting of
// catchallNonTopic. Since no NPC or infoSource will have a response/entry
// for this topic, it will simply cause a "disavow" statement to be
// printed.
//
// Important properties: none
catchallNonTopic: knownTopic
 sdesc = "non-topic (you should never see this)"
;

// catchallUnknownTopic: Similar to catchallNonTopic, this topic will be
// returned by disambiguation functions if the player's vocabulary matches
// no *known* topics, but at least one unknowntopic. Default messages are
// set here so that ask, tell, and consult will all output topic.unknownMsg
// in response. The default message for "look up" must be handled in
// infoSource itself, in ioLookupIn.
//
// Important properties: none
catchallUnknownTopic: knownTopic
 sdesc = "unknown topic (you should never see this)"
 ioAskAbout(actor, dobj) = {topic.unknownMsg;}
 ioAskFor(actor, dobj) = {topic.unknownMsg;}
 ioTellAbout(actor, dobj) = {topic.unknownMsg;}
 ioConsultOn(actor, dobj) = {topic.unknownMsg;}
;

/*************************** New Prepositions ***************************/

forPrep: Prep
 preposition = 'for'
 sdesc = "for"
;

/************************ New and Replaced Verbs ************************/

replace askVerb: ioTopicVerb, darkVerb
 verb = 'ask'
 sdesc = "ask"
 prepDefault = aboutPrep
 ioAction(aboutPrep) = 'AskAbout'
 ioAction(forPrep) = 'AskFor'
;

replace tellVerb: ioTopicVerb, darkVerb
 verb = 'tell'
 sdesc = "tell"
 prepDefault = aboutPrep
 ioAction(aboutPrep) = 'TellAbout'
;

consultVerb: ioTopicVerb
 verb = 'consult'
 sdesc = "consult"
 prepDefault = onPrep
 ioAction(onPrep) = 'ConsultOn'
 ioAction(aboutPrep) = 'ConsultOn'
;

lookupVerb: doTopicVerb
 verb = 'look up' 'read about'
 sdesc = "look up"
 prepDefault = inPrep
 ioAction(inPrep) = 'LookupIn'
;

#pragma C-