/* asktell.t: ASK/TELL-based conversation system for TADS
 * V1.0
 * Suzanne Skinner, 1999, Public Domain
 * tril@igs.net
 *
 * 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-
