hintsys.t
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 by Michael J. Roberts. All Rights Reserved.
*
* 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 "adv3.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.
*/
property scoreCount;
/* ------------------------------------------------------------------------ */
/*
* 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.
*/
enum OpenGoal, ClosedGoal, UndiscoveredGoal;
class Goal: MenuTopicItem, HintMenuObject
/*
* 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 = []
/*
* 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)
*/
openWhenDescribed = nil
/* close the goal when the given object is described */
closeWhenDescribed = 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))
|| (openWhenDescribed != nil && openWhenDescribed.described)
|| (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))
|| (closeWhenDescribed != nil && closeWhenDescribed.described)
|| (closeWhenAchieved != nil && closeWhenAchieved.scoreCount != 0)
|| (closeWhenKnown != nil && gPlayerChar.knowsAbout(closeWhenKnown))
|| (closeWhenRevealed != nil && gRevealed(closeWhenRevealed))
|| closeWhenTrue)
/*
* 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 */
mainReport(gLibMessages.hintsDisabled);
}
/*
* 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)
{
mainReport(gLibMessages.hintsNotPresent);
return;
}
/* if hints are disabled, reject the request */
if (sessionHintStatus.hintsDisabled)
{
mainReport(gLibMessages.sorryHintsDisabled);
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)
{
mainReport(gLibMessages.currentlyNoHints);
return;
}
/* if we haven't warned about hints, do so now */
if (!showHintWarning())
return;
/* display the hint menu */
topHintMenuObj.display();
/* all done */
mainReport(gLibMessages.hintsDone);
}
/*
* 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
*/
gLibMessages.showHintWarning();
/* 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
;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3