# hintsys.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 by Michael J. Roberts.  All Rights Reserved.
     *   Adapted for adv3lite library by Eric Eve
     *
     *   TADS 3 Library - Hint System
     *
     *   This module provides a hint system framework.  Games can use this
     *   framework to define context-sensitive hints for players.
     *
     *   This module depends on the menus module to display the user interface.
     */

    /* include the library header */
    #include "advlite.h"

    /* ------------------------------------------------------------------------ */
    /*
     *   We refer to some properties defined primarily in score.t - that's an
     *   optional module, though, so make sure the compiler has heard of these.
     *   We do the same for some properties in eventList.t
     */
    property scoreCount;
    property achieved;
    property eventList;

    /* HintOpener provides some properties common to Goal and ExtraHint, in particuar
     * those relating to opening and closing Goals and ExtraHints.
     */
    class HintOpener: object
        /*
         *   An optional object that, when seen by the player character, opens
         *   this goal.  It's often convenient to declare a goal open as soon
         *   as the player enters a particular area or has encountered a
         *   particular object.  For such cases, simply set this property to
         *   the room or object that opens the goal, and we'll automatically
         *   mark the goal as Open the next time the player asks for a hint
         *   after seeing the referenced object.
         */
        openWhenSeen = nil

        /*
         *   An option object that, when seen by the player character, closes
         *   this goal.  Many goals will be things like "how do I find the
         *   X?", in which case it's nice to close the goal when the X is
         *   found.
         */
        closeWhenSeen = nil

        /*
         *   this is like openWhenSeen, but opens the topic when the given
         *   object is described (with EXAMINE)
         */
        openWhenExamined = nil

        /* close the goal when the given object is described */
        closeWhenExamined = nil

        /*
         *   An optional object that, when moved, opens this goal.  It's often
         *   convenient to declare a goal open as soon as the player enters a
         *   particular area or has encountered a particular object.  For such
         *   cases, simply set this property to the object that opens the goal, and
         *   we'll automatically mark the goal as Open the next time the player asks
         *   for a hint after seeing the referenced object.
         */
        openWhenMoved = nil

        /*
         *   An optional object that, when moved, closes this goal.  Many goals will
         *   be things like "how do I find the X?", in which case it's nice to close
         *   the goal when the X is found.
         */
        closeWhenMoved = nil

        /*
         *   An optional Achievement object that opens this goal.  This goal
         *   will be opened automatically once the goal is achieved, if the
         *   goal was previously undiscovered.  This makes it easy to set up a
         *   hint topic that becomes available after a particular puzzle is
         *   solved, which is useful when a new puzzle only becomes known to
         *   the player after a gating puzzle has been solved.
         */
        openWhenAchieved = nil

        /*
         *   An optional Achievement object that closes this goal.  Once the
         *   achievement is completed, this goal's state will automatically be
         *   set to Closed.  This makes it easy to associate the goal with a
         *   puzzle: once the puzzle is solved, there's no need to show hints
         *   for the goal any more.
         */
        closeWhenAchieved = nil

        /*
         *   An optional Topic or Thing that opens this goal when the object
         *   becomes "known" to the player character.  This will open the goal
         *   as soon as gPlayerChar.knowsAbout(openWhenKnown) returns true.
         *   This makes it easy to open a goal as soon as the player comes
         *   across some information in the game.
         */
        openWhenKnown = nil

        /* an optional Topic or Thing that closes this goal when known */
        closeWhenKnown = nil

        /*
         *   An optional <.reveal> tag name that opens this goal.  If this is
         *   set to a non-nil string, we'll automatically open this goal when
         *   the tag has been revealed via <.reveal> (or gReveal()).
         */
        openWhenRevealed = nil

        /* an optional <.reveal> tag that closes this goal when revealed */
        closeWhenRevealed = nil

        /*
         *   An optional arbitrary check that opens the goal.  If this returns
         *   true, we'll open the goal.  This check is made in addition to the
         *   other checks (openWhenSeen, openWhenDescribed, etc).  This can be
         *   used for any custom check that doesn't fit into one of the
         *   standard openWhenXxx properties.
         */
        openWhenTrue = nil

        /* an optional general-purpose check that closes the goal */
        closeWhenTrue = nil

        /*
         *   Determine if there's any condition that should open this goal.
         *   This checks openWhenSeen, openWhenDescribed, and all of the other
         *   openWhenXxx conditions; if any of these return true, then we'll
         *   return true.
         *
         *   Note that this should generally NOT be overridden in individual
         *   instances; normally, instances would define openWhenTrue instead.
         *   However, some games might find that they use the same special
         *   condition over and over in many goals, often enough to warrant
         *   adding a new openWhenXxx property to Goal.  In these cases, you
         *   can use 'modify Goal' to override openWhen to add the new
         *   condition: simply define openWhen as (inherited || newCondition),
         *   where 'newCondition' is the new special condition you want to
         *   add.
         */
        openWhen = (
            (openWhenSeen != nil && gPlayerChar.hasSeen(openWhenSeen))
            || (openWhenExamined != nil && openWhenExamined.examined)
            || (openWhenMoved != nil && openWhenMoved.moved)
            || (openWhenAchieved != nil && openWhenAchieved.scoreCount != 0)
            || (openWhenKnown != nil && gPlayerChar.knowsAbout(openWhenKnown))
            || (openWhenRevealed != nil && gRevealed(openWhenRevealed))
            || openWhenTrue)

        /*
         *   Determine if there's any condition that should close this goal.
         *   We'll check closeWhenSeen, closeWhenDescribed, and all of the
         *   other closeWhenXxx conditions; if any of these return true, then
         *   we'll return true.
         */
        closeWhen = (
            (closeWhenSeen != nil && gPlayerChar.hasSeen(closeWhenSeen))
            || (closeWhenExamined != nil && closeWhenExamined.examined)
            || (closeWhenMoved != nil && closeWhenMoved.moved)
            || (closeWhenAchieved != nil && closeWhenAchieved.scoreCount != 0)
            || (closeWhenKnown != nil && gPlayerChar.knowsAbout(closeWhenKnown))
            || (closeWhenRevealed != nil && gRevealed(closeWhenRevealed))
            || closeWhenTrue)

    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A basic hint menu object.  This is an abstract base class that
     *   encapsulates some behavior common to different hint menu classes.
     */
    class HintMenuObject: object
        /*
         *   The topic order.  When we're about to show a list of open topics,
         *   we'll sort the list in ascending order of this property, then in
         *   ascending order of title.  By default, we set this order value to
         *   1000; if individual goals don't override this, then they'll
         *   simply be sorted lexically by topic name.  This can be used if
         *   there's some basis other than alphabetical order for sorting the
         *   list.
         */
        topicOrder = 1000

        /*
         *   Compare this goal to another, for the purposes of sorting a list
         *   of topics.  Returns a positive number if this goal sorts after
         *   the other one, a negative number if this goal sorts before the
         *   other one, 0 if the relative order is arbitrary.
         *
         *   By default, we'll sort by topicOrder if the topicOrder values are
         *   different, otherwise alphabetically by title.
         */
        compareForTopicSort(other)
        {
            /* if the topicOrder values are different, sort by topicOrder */
            if (topicOrder != other.topicOrder)
                return topicOrder - other.topicOrder;

            /* the topicOrder values are the same, so sort by title */
            if (title > other.title)
                return 1;
            else if (title < other.title)
                return -1;
            else
                return 0;
        }
    ;

    /*
     *   A Goal represents an open task: something that the player is trying
     *   to achieve.  A Goal is an abstract object, not part of the simulated
     *   world of the game.
     *
     *   Each goal is associated with a hint topic (usually shown as a
     *   question, such as "How do I get past the guard?") and an ordered list
     *   of hints.  The hints are usually ordered from most general to most
     *   specific.  The idea is to let the player control how big a hint they
     *   get; we start with a small nudge and work towards giving away the
     *   puzzle completely, so the player can stop as soon as they see
     *   something that helps.
     *
     *   At any given time, a goal can be in one of three states:
     *
     *   - Open: this means that the player is (or ought to be) aware of the
     *   goal, but the goal hasn't yet been achieved.  Determining this
     *   awareness is up to the goal.  In some cases, a goal is opened as soon
     *   as the player has seen a particular object or entered a particular
     *   area; in other cases, a goal might be opened by a scripted event,
     *   such as a speech by an NPC telling the player they have to accomplish
     *   something.  A goal could even be opened by viewing a hint for another
     *   goal, because that hint could explain a gating goal that the player
     *   might not otherwise been able to know about.
     *
     *   - Undiscovered: this means that the player doesn't yet have any
     *   reason to know about the goal.
     *
     *   - Closed: this means that the player has accomplished the goal, or in
     *   some cases that the goal has become irrelevant.
     *
     *   The hint system only shows goals that are Open.  We don't show Closed
     *   goals because the player presumably has no need of them any longer;
     *   we don't show Undiscovered goals to avoid giving away developments
     *   later in the game before they become relevant.
     */

    class Goal: MenuTopicItem, HintMenuObject, HintOpener
        /*
         *   The topic question associated with the goal.  The hint system
         *   shows a list of the topics for the goals that are currently open,
         *   so that the player can decide what area they want help on.
         */
        title = ''

        /*
         *   Our parent menu - this is usually a HintMenu object.  In very
         *   simple hint systems, this could simply be a top-level hint menu
         *   container; more typically, the hint system will be structured
         *   into a menu tree that organizes the hint topics into several
         *   different submenus, for easier navigatino.
         */
        location = nil

        /*
         *   The list of hints for this topic.  This should be ordered from
         *   most general to most specific; we offer the hints in the order
         *   they appear in this list, so the earlier hints should give away
         *   as little as possible, while the later hints should get
         *   progressively closer to just outright giving away the answer.
         *
         *   Each entry in the list can be a simple (single-quoted) string, or
         *   it can be a Hint object.  In most cases, a string will do.  A
         *   Hint object is only needed when displaying the hint has some side
         *   effect, such as opening a new Goal.
         */
        menuContents = []

        /*
         *   Has this goal been fully displayed?  The hint system automatically
         *   sets this to true when the last item in our hint list is
         *   displayed.
         *
         *   You can use this, for example, to automatically remove the hint
         *   from the hint menu after it's been fully displayed.  (You might
         *   want to do this with a hint for a red herring, for example.  After
         *   the player has learned that the red herring is a red herring, they
         *   probably won't need to see that particular line of hints again, so
         *   you can remove the clutter in the menu by closing the hint after
         *   it's been fully displayed.)  To do this, simply add this to the
         *   Goal object:
         *
         *.    closeWhenTrue = (goalFullyDisplayed)
         */
        goalFullyDisplayed = nil

        /*
         *   Check our menu state and update it if necessary.  Each time our
         *   parent menu is about to display, it'll call this on its sub-items
         *   to let them update their current states.  This method can promote
         *   the state to Open or Closed if the necessary conditions for the
         *   goal have been met.
         *
         *   Sometimes it's more convenient to set a goal's state explicitly
         *   from a scripted event; for example, if the goal is associated
         *   with a scored achievement, awarding the goal's achievement will
         *   set the goal's state to Closed.  In these cases, there's no need
         *   to use this method, since you're managing the goal's state
         *   explicitly.  The purpose of this method is to make it easy to
         *   catch goal state changes that can be reached by several different
         *   routes; in these cases, you can just write a single test for
         *   those conditions in this method rather than trying to catch every
         *   possible route to the new conditions and writing code in all of
         *   those.
         *
         *   The default implementation looks at our openWhenSeen property.
         *   If this property is not nil, then we'll check the object
         *   referenced in this property; if our current state is
         *   Undiscovered, and the object referenced by openWhenSeen has been
         *   seen by the player character, then we'll change our state to
         *   Open.  We'll make the corresponding check for openWhenDescribed.
         */
        updateContents()
        {
            /*
             *   If we're currently Undiscovered, and our openWhenSeen object
             *   has been seen by the player charater, change our state to
             *   Open.  Likewise, if our gating achievement has been scored,
             *   open the goal.
             */
            if (goalState == UndiscoveredGoal && openWhen)
            {
                /*
                 *   the player has encountered our gating object, so open
                 *   this goal
                 */
                goalState = OpenGoal;
            }

            /*
             *   if we're currently Undiscovered or Open, and our Achievement
             *   has been scored, then change our state to Closed - once the
             *   goal has been achieved, there's no need to offer hints on the
             *   topic any longer
             */
            if (goalState is in (UndiscoveredGoal, OpenGoal) && closeWhen)
            {
                /* the goal has been achieved, so close it */
                goalState = ClosedGoal;
            }
        }

        /* display a sub-item, keeping track of when we've shown them all */
        displaySubItem(idx, lastBeforeInput, eol)
        {
            /* do the inherited work */
            inherited(idx, lastBeforeInput, eol);

            /* if we just displayed the last item, note it */
            if (idx == menuContents.length())
                goalFullyDisplayed = true;
        }

        /* we're active in our parent menu if our goal state is Open */
        isActiveInMenu = (goalState == OpenGoal)

        /*
         *   This goal's current state.  We'll start off undiscovered.  When a
         *   goal should be open from the very start of the game, this should
         *   be overridden and set to OpenGoal.
         */
        goalState = UndiscoveredGoal
    ;

    /*
     *   A Hint encapsulates one hint from a topic.  In many cases, hints can
     *   be listed in a topic simply as strings, rather than using Hint
     *   objects.  Hint objects provide a little more control, though; in
     *   particular, a Hint object can specify some additional code to run
     *   when the hint is shown, so that it can apply any side effects of
     *   showing the hint (for example, when a hint is shown, it could mark
     *   another Goal object as Open, which might be desirable if the hint
     *   refers to another topic that the player might not yet have
     *   encountered).
     */
    class Hint: MenuTopicSubItem
        /* the hint text */
        hintText = ''

        /*
         *   A list of other Goal objects that this hint references.  By
         *   default, when we show this hint for the first time, we'll promote
         *   each goal in this list from Undiscovered to Open.
         *
         *   Sometimes, it's necessary to solve one puzzle before another can
         *   be solved.  In these cases, some hints for the first puzzle
         *   (which depends on the second), especially the later, more
         *   specific hints, might need to refer to the other puzzle.  This
         *   would make the player aware of the other puzzle even if they
         *   weren't already.  In such cases, it's a good idea to make sure
         *   that we make hints for the other puzzle available immediately,
         *   since otherwise the player might be confused by the absence of
         *   hints about it.
         */
        referencedGoals = []

        /*
         *   Get my hint text.  By default, we mark as Open any goals listed
         *   in our referencedGoals list, then return our hintText string.
         *   Individual Hint objects can override this as desired to apply any
         *   additional side effects.
         */
        getItemText()
        {
            /* scan the referenced goals list */
            foreach (local cur in referencedGoals)
            {
                /* if this goal is not yet discovered, open it */
                if (cur.goalState == UndiscoveredGoal)
                    cur.goalState = OpenGoal;
            }

            /* return our hint text */
            return hintText;
        }
    ;

    /*
     *   A hint menu.  This same class can be used for the top-level hints
     *   menu and for sub-menus within the hints menu.
     *
     *   The typical hint menu system will be structured into a top-level hint
     *   menu that contains a set of sub-menus for the main areas of the game;
     *   each sub-menu will have a series of Goal items, each Goal providing a
     *   set of answers to a particular question.  Something like this:
     *
     *   topHintMenu: TopHintMenu 'Hints';
     *.  + HintMenu 'General Questions';
     *.  ++ Goal 'What am I supposed to be doing?' [answer, answer, answer];
     *.  ++ Goal 'Amusing things to try' [thing, thing, thing];
     *.  + HintMenu 'First Area';
     *.  ++ Goal 'How do I get past the shark?' [answer, answer, answer];
     *.  ++ Goal 'How do I open the fish tank?' [answer, answer, answer];
     *.  + HintMenu 'Second Area';
     *.  ++ Goal 'Where is the gold key?' [answer, answer, answer];
     *.  ++ Goal 'How do I unlock the gold door?' [answer, answer, answer];
     *
     *   Note that there's no requirement that the hint menu tree takes
     *   exactly this shape.  A very small game could dispense with the
     *   submenus and simply put all of the goals directly in the top hint
     *   menu.  A very large game with lots of goals could add more levels of
     *   sub-menus to make it easier to navigate the large number of topics.
     */
    class HintMenu: MenuItem, HintMenuObject
        /* the menu's title */
        title = ''

        /* update our contents */
        updateContents()
        {
            local vec = new Vector(16);

            /*
             *   First, run through all of our sub-items, and update their
             *   contents.  We only want to show our active contents, so we
             *   need to check with each item to find out which is active.
             */
            foreach (local cur in allContents)
                cur.updateContents();

            /* create a vector containing all of our active items */
            foreach (local cur in allContents)
            {
                /* if this item is active, add it to the active vector */
                if (cur.isActiveInMenu)
                    vec.append(cur);
            }

            /* set our contents list to the list of active items */
            contents = vec;
        }

        /* we're active in a menu if we have any active contents */
        isActiveInMenu = (contents.length() != 0)

        /* add a sub-item to our contents */
        addToContents(obj)
        {
            /*
             *   add the sub-item to our allContents list rather than our
             *   active contents
             */
            allContents += obj;
        }

        /* initialize our contents list */
        initializeContents()
        {
            /* sort our allContents list in the object-defined sorting order */
            allContents = allContents.sort(
                SortAsc, {a, b: a.compareForTopicSort(b)});
        }

        /*
         *   our list of all of our sub-items (some of which may not be
         *   active, in which case they'll appear in this list but not in our
         *   'contents' list, which contains only active contents)
         */
        allContents = []
    ;

    /*
     *   A hint menu version of the long topic menu.
     */
    class HintLongTopicItem: MenuLongTopicItem, HintMenuObject
        /*
         *   presume these are always active - they're usually used for things
         *   like hint system instructions that should always be available
         */
        isActiveInMenu = true
    ;

    /*
     *   Top-level hint menu.  As a convenience, an object defined of this
     *   class will automatically register itself as the top-level hint menu
     *   during pre-initialization.
     */
    class TopHintMenu: HintMenu, PreinitObject
        /* register as the top-level hint menu during pre-initialization */
        execute() { hintManager.topHintMenuObj = self; }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   The default hint system user interface implementation.  All of the
     *   hint-related verbs operate by calling methods in the object stored in
     *   the global variable gHintSystem, which we'll by default initialize
     *   with a reference to this object.  Games can replace this with their
     *   own implementations if desired.
     */
    hintManager: PreinitObject
        /* during pre-initialization, register as the global hint manager */
        execute() { gHintManager = self; }

        /*
         *   Disable hints - this is invoked by the HINTS OFF action.
         *
         *   Some users don't like on-line hint systems because they find them
         *   to be too much of a temptation.  To address this concern, we
         *   provide this HINTS OFF command.  Players who want to ensure that
         *   their will-power won't crumble later on in the face of a
         *   difficult puzzle can type HINTS OFF early on, before the going
         *   gets rough; this will disable hints for the rest of the session.
         *   It's kind of like giving your credit card to a friend before
         *   going to the mall, making the friend promise that they won't let
         *   you spend more than such and such an amount, no matter how much
         *   you beg and plead.
         */
        disableHints()
        {
            /*
             *   Remember that hints have been disabled.  Keep this
             *   information in the transient session object, since we want
             *   the disabled status to last for the rest of this session,
             *   even if we restore or restart later.
             */
            sessionHintStatus.hintsDisabled = true;

            /* acknowledge it */
            DMsg(hints disabled, '<.notification>Hints are now disabled.<./notification> ');
        }

        /*
         *   The top-level hint menu.  This must be provided by the game, and
         *   should be set during initialization.  If this is nil, hints won't
         *   be available.
         *
         *   We don't provide a default top-level hint menu because we want to
         *   give the game maximum flexibility in defining this object exactly
         *   as it wants.  For convenience, an object of class TopHintMenu
         *   will automatically register itself during pre-initialization -
         *   but note that there should be only one such object in the entire
         *   game, since if there are more than one, only one will be
         *   arbitrarily chosen as the registered object.
         */
        topHintMenuObj = nil

        /*
         *   Show hints - invoke the hint system.
         */
        showHints()
        {
            /* if there is no top-level hint menu, no hints are available */
            if (topHintMenuObj == nil)
            {
                Hints.sayHintsNotPresent();
                return;
            }

            /* if hints are disabled, reject the request */
            if (sessionHintStatus.hintsDisabled)
            {
                DMsg(sorry hints disabled, '<.parser>Sorry, but hints have been
                    disabled for this session, as you requested.  If you&rsquo;ve
                    changed your mind, you&rsquo;ll have to save your current
                    position, exit the TADS interpreter, and start a new interpreter
                    session.<./parser> ');
                return;
            }

            /* bring the hint menu tree up to date */
            topHintMenuObj.updateContents();

            /* if there are no hints available, say so and give up */
            if (topHintMenuObj.contents.length() == 0)
            {
                DMsg(currently no hints, '<.parser>Sorry, no hints are currently
                    available. Please check back later.<./parser> ');
                return;
            }

            /* if we haven't warned about hints, do so now */
            if (!showHintWarning())
                return;

            /* display the hint menu */
            topHintMenuObj.display();

            /* all done */
            DMsg(hints done, '<.parser>Done.<./parser> ');
        }

        /*
         *   Show a warning before showing any hints.  By default, we'll show
         *   this at most once per session or once per saved game.  Returns
         *   true if we are to proceed to the hints, nil if not.
         */
        showHintWarning()
        {
            /*
             *   If we have previously warned in this session, or if we've
             *   warned in a previous session and the same game was later
             *   saved and restored, don't warn again.  The transient session
             *   object tells us if we've asked in this session; the normal
             *   persistent object tells us if we've asked in a previous
             *   session that we've since saved and restored.
             */
            if (!sessionHintStatus.hintWarning && !gameHintStatus.hintWarning)
            {
                /*
                 *   we haven't asked yet in either the session or the game,
                 *   so show the warning now
                 */
                cquoteOutputFilter.deactivate();

                 DMsg(showHintWarning, '<.notification>Warning: Some people
                     don&rsquo;t like built-in hints, since the temptation to ask
                     for help prematurely can become overwhelming when hints are so
                     close at hand.  If you&rsquo;re worried that your willpower
                     won&rsquo;t hold up, you can disable hints for the rest of this
                     session by typing <<aHref('hints off', 'HINTS OFF')
           >>.  If you still want to see the hints now, type
                     <<aHref('hint', 'HINT')>>.<./notification> ');

                cquoteOutputFilter.activate();

                /* note that we've shown the warning */
                sessionHintStatus.hintWarning = true;
                gameHintStatus.hintWarning = true;

                /* don't proceed to hints now; let them ask again */
                return nil;
            }

            /*
             *   They've already seen the warning before.  It's possible that
             *   they've seen it in a past session with the game and not
             *   otherwise during this session, but now that we're accessing
             *   the hint system once, don't bother with another warning for
             *   the rest of this session.
             */
            sessionHintStatus.hintWarning = true;

            /* proceed to the hints */
            return true;
        }
    ;

    /*
     *   We keep several pieces of information about the status of the hint
     *   system.  Some of it pertains to the current session, independently of
     *   any saving/restoring/restarting, so we keep this information in a
     *   transient object.  Some pertains to the present game, so we keep it
     *   in an ordinary persistent object, so that it's saved and restored
     *   along with the game.
     */
    transient sessionHintStatus: object
        /* flag: we've warned about the hint system in this session */
        hintWarning = nil

        /* flag: we've disabled hints for this session */
        hintsDisabled = nil
    ;

    gameHintStatus: object
        /* flag: we've warned about the hint system in this session */
        hintWarning = nil
    ;

    property achieved;

    //==============================================================================
    /*
     *   FRAMEWORK FOR THE EXTRA HINTS MECHANISM
     *
     *   The ExtraHint class provides a framework for providing players (especially
     *   novice players) with extra hints, nudges, and tips during the course of
     *   play. When it is activated (either at player request, or because the player
     *   appears to be having trouble), a hint or tip can be displayed at the end of
     *   the turn in response to what the player has just typed, or some condition
     *   that has just become true, or which became true a certain number of turns
     *   ago.
     *
     *   It is also possible to define conditions (typically the achievement of some
     *   goal) that render a particular ExtraHint otiose, so that it is not
     *   displayed if it is no longer needed.
     *
     *   We simply need to define objects of the ExtraHint class to encapsulate the
     *   extra hints we want displayed.
     *
     *   A series of ExtraHints can be defined using a template if desired.
     *
     *   The first (optional) element in the template (e.g. ->doorAchievement) is
     *   the achievement we check to see if this ExtraHint has become redundant. For
     *   example, if the player has already taken the flyer, flyer.achievement has
     *   been achieved, and so we don't need this first ExtraHint.
     *
     *   The second (optional) element in the template (e.g. +1) is the hintDelay;
     *   this is number of turns that should elapse between the openWhen condition
     *   (see below) becoming true and the hint being offered. This optionally
     *   allows the player a few turns to work on the solution before being offered
     *   a gratuitous hint.
     *
     *   The third element in the template is simply the text of the extra hint to
     *   display.
     *
     *   The openWhen property holds the condition that must be true before this
     *   ExtraHint is displayed. In the case of the first ExtraHint, the condition
     *   is that the player char has visited the location that contains the
     *   telephone pole.
     */
    class ExtraHint: HintOpener
        location = extraHintManager

        /*
         *   The condition under which we close this ExtraHint. By default we close
         *   it if either hintDone becomes true or our closeWhen condition is true.
         */
        checkClose()
        {
            return hintDone || closeWhen;
        }

        /*
         *   The number of turns between openWhen becoming true and this hint
         *   being displayed.
         */
        hintDelay = 0

        /*
         *   If the closeWhen condition is true we remove this ExtraHint from the
         *   list of potentially active ExtraHints and return nil to tell the
         *   caller that this ExtraHint was not displayed. Otherwise we check if
         *   this ExtraHint (a) meets its openWhen conditions to be displayed
         *   and (b) is due to be displayed because hintDelay turns have passed
         *   since it was open. If both conditions are met we show our text and
         *   return true to tell our caller that an ExtraHint has been displayed
         *   (important since we display at most one ExtraHint per turn).
         *   Otherwise we return nil.
         */
        doHint()
        {
            if(checkClose())
            {
                extraHintManager.removeFromContents(self);
                return nil;
            }

            if(openWhen)
            {
                if(openedWhen == nil)
                    openedWhen = libGlobal.totalTurns;

                if(libGlobal.totalTurns < (openedWhen + hintDelay))
                   return nil;

                showHint();
                return true;
            }
            return nil;
        }

        /* The text to display in relation to this ExtraHint */
        hintText = ""

        /* Show the text related to this ExtraHint */
        showHint()
        {
            "<.extrahint>";
            if(ofKind(Script))
            {
                doScript();

                /*
                 *   Reset openedWhen so that we don't display this hint again until
                 *   after another hintDelay turns.
                 */
                openedWhen = libGlobal.totalTurns;
            }
            else
                hintText();

            "<./extrahint>";

            /*
             *   Mark this hint as done now we've shown it, unless we're an
             *   EventList with more items to show.
             */
            if(eventList == nil || getScriptState() > eventList.length)
                hintDone = true;

        }

        /* Set the number of turns that must elapse before we display this hint */
        setDelay(val)
        {
            hintDone = nil;
            hintDelay = val;
        }

        /*
         *   For internal use only: Flag; have we done with this hint (because we've
         *   displayed it)?
         */
        hintDone = nil

        /* The turn on which this ExtraHint was first opened */
        openedWhen = nil

        /*
         *   Our priority. When two ExtraHints become available on the same term,
         *   the one with the higher priority will be displayed.
         */
        priority = 100
    ;

    extraHintStyleTag: StyleTag 'extrahint' '<.p><i>' '</i><.p>';

    /*
     *   The extraHintManager carries out the work of starting and stopping the
     *   display of ExtraHints, and determing which, if any, ExtraHint is ready
     *   to be displayed.
     */
    extraHintManager: PreinitObject

        /* Start the Daemon that checks whether to display any ExtraHints. */
        activate()
        {
            /*
             *   First check that our daemonID is nil so we don't start another
             *   Daemon if one is already running.
             */
            if(daemonID == nil)
                daemonID = new Daemon(self, &hintDaemon, 1);
        }

        /*
         *   Even if this module is included, there may be no ExtraHints defined in
         *   the game, in which case we might want to check whether any ExtraHints
         *   exist before offering them to the player.
         */
        extraHintsExist()
        {
            return firstObj(ExtraHint) != nil;
        }

    	/* Stop the Daemon that checks whether to display any ExtraHints. */
        deactivate()
        {
            if(daemonID)
            {
                daemonID.removeEvent();
                daemonID = nil;
            }
        }

        daemonID = nil

        /* The extraHintManager is activated if it has a running daemon */
        activated = (daemonID != nil)

        /*
         *   When we're active we run through every ExtraHint object in our
         *   contents and run its doHint() method till either we run out of
         *   ExtraHint objects or we find one that displays an extra hint, and
         *   so returns true.
         */
        hintDaemon()
        {
            foreach(local obj in contents)
            {
                if(obj.doHint())
                    break;
            }
        }

        contents = []

        addToContents(obj)   {  contents += obj; }

        removeFromContents(obj) {  contents -= obj; }

        /* Build a list of all ExtraHints in our contents property at PreInit */
        execute()
        {
            /* First register our existence */
            gExtraHintManager = self;

            /* Then add every ExtraHint to our contents */
            forEachInstance(ExtraHint, {x: addToContents(x) });

            /* Finally sort the ExtraHints in descending order of priority */
            contents = contents.sort(SortDesc, {a, b: a.priority - b.priority });
        }

        explainExtraHints()
        {
            DMsg(explain extra hints, 'If you\'re new to Interactive Fiction and
                would like to read a few extra hints and tips that will pop up here
                and there as you explore the story, type <<cmdStr('ON')>>. If you
                decide you don\'t want any more of these bonus tips, simply type
                <<cmdStr('OFF')>>. ' );
        }

        cmdStr(stat)
        {
            return BMsg(extra hint cmd str,
            '<<aHref('EXTRA ' + stat, 'EXTRA ' + stat, 'Turning extra hints ' +
                    stat.toLower)>>');
        }
    ;

Adv3Lite Library Reference Manual
Generated on 26/02/2025 from adv3Lite version 2.2