Last “Chapter” Hint Advancement
This opaque phrase refers to ADV3 menu behavior that auto-advances (usually) LongTopicItems whose isChapterMenu
property is set, allowing the player to frictionlessly advance from one menu entry to the next without having to back out, advance, enter. It is a nice touch with one gotcha: When encountering the LAST hint in a menu, it pops you back to the containing menu which is, assuming every entry was read, now ALSO exhausted. I don’t know why this chafed me so. It’s just, here I was space-barring my way from one entry to the next, only to be pulled up short into an exhausted menu that needed more clumsy navigation.
The behavior I decided I wanted was to pop up to the first menu in the branch that was NOT now exhausted. Granted, this is also potentially confusing to the player, suddenly displacing them up the menu tree. Perhaps it is my super-human sense of spatial awareness that made this preferable to me.
This does require some gymnastics. First, I add a menu return code, which are specified in adv3.h. Technically, these defines are more than a code, they are an index into a libMessages list-of-lists property which defines acceptable navigation keys for the menu system. I am not adding anything there, just adding a code which I can process. Here are the standard defs…
/* ------------------------------------------------------------------------ */
/*
* Definitions for the menu system
*/
/*
* The indices for the key values used to navigate menus, which are held
* in the keyList array of MenuItems.
*/
#define M_QUIT 1
#define M_PREV 2
#define M_UP 3
#define M_DOWN 4
#define M_SEL 5
To which I added
/* =====================================================================
*
* Menu system enhancement, end submenu when topics exhausted
* (extends adv3.h defs)
*
* ======================================================================
*/
#define M_PREVPREV 6
in my own adv3_jjmcc.h
A word about how menus are implemented. The TopHintMenu is a special menu. When invoked, its display()
method massages banners, clears the screen and populates it with top level menu items. It then enters a do..while
loop spinning until it receives valid input (defined in libMessages). Depending on input it might navigate into subordinate elements, but only exits the loop on a up/quit. Each subordinate element does the same, meaning you end up in a series of nested, active do..while
loops depending on how deep your menu tree is.
To get the behavior I want, when chapter advancement is in play, you must pop out of these nested loops one at a time, detecting either a menu with unread entries, or the final (top) level.
Here is innermost while
code to do that, mostly stock with just my added code to process:
/* if the player selected a sub-menu, invoke the selection */
while (loc == M_SEL
&& selection != 0
&& selection <= contents.length())
{
/*
* Invoke the sub-menu, checking for a QUIT result. If
* the user isn't quitting, we'll display our own menu
* again; in this case, update it now, in case something
* in the sub-menu changed our own contents.
*/
loc = contents[selection].showMenuHtml(topMenu);
/* see what we have */
switch (loc)
{
case M_UP:
/* they want to go directly to the previous menu */
loc = M_SEL;
--selection;
break;
case M_DOWN:
/* they want to go directly to the next menu */
loc = M_SEL;
++selection;
break;
// JJMcC: NEW PROCESSING
// if return code is M_PREVPREV means we were in the last chapter
// if fully read, recurse up until hit menu with unread elements
// (add loop exit condition below as well)
// note the value of 'loc' is returned when exiting this level
case M_PREVPREV:
if (!unread) { // ie this menu level fully read
// must top out at topMenu
if (location == topMenu) loc = M_PREV;
// otherwise carry M_PREVPREV up to next level
break;
} // if unread fall into M_PREV case
case M_PREV:
/* they want to return to this menu level */
loc = nil;
/* update our contents */
updateContents();
/* make sure we refresh the title area */
refreshTitle = true;
break;
}
WHERE this code has to go is kludgey. Basically it has to go DEEP into the showMenuHtml
and, if you go to text mode for screen readers, showMenuText
methods. Note that this code ONLY fixes console mode TADS. HTMLTADS has yet a different processing tree that I did NOT try to adjust. Before I dump that wall of code on you, there is one more thing needs doing. The chapter code must detect and return the M_PREVPREV code when last in line is consumed. Here is the code for that, ALSO deep in a method (look for JJMcC below):
modify MenuLongTopicItem
unread = true
menuContentsTxt = nil // menuContents replaced by method in preInit
// only change is at the end of this method
//
showMenuCommon(topMenu)
{
local evt, key, loc, nxt;
/* update our contents, as needed */
updateContents();
/* show our heading, centered */
"<CENTER><b><<heading>></b></CENTER>\b";
/* show our contents */
"<<menuContents>>\b";
/* check to see if we should offer chapter navigation */
nxt = (isChapterMenu ? location.getNextMenu(self) : nil);
/* if there's a next chapter, show how we can navigate to it */
if (nxt != nil)
{
/* show the navigation */
gLibMessages.menuNextChapter(topMenu.keyList, nxt.title,
'next', 'menu');
}
else
{
/* no chaptering - just print the ending message */
"<<menuLongTopicEnd>>";
}
/* wait for an event */
for (;;)
{
evt = inputManager.getEvent(nil, nil);
switch(evt[1])
{
case InEvtHref:
/* check for a 'next' or 'prev' command */
if (evt[2] == 'next')
return M_DOWN;
else if (evt[2] == 'prev')
return M_UP;
else if (evt[2] == 'menu')
return M_PREV;
break;
case InEvtKey:
/* get the key */
key = evt[2].toLower();
/*
* if we're in plain text mode, add a blank line after
* the key input
*/
if (statusLine.statusDispMode == StatusModeText)
"\b";
/* look up the command key */
loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil});
/*
* if it's 'next', either proceed to the next menu or
* return to the previous menu, depending on whether
* we're in chapter mode or not
* JJMcC ONLY CHANGE: add test for immediate menu top or unread,
* pop to first unread level
*/
if (loc == M_SEL)
// return (nxt == nil ? M_PREV : M_DOWN); // orig code
// if no nxt item (see above), pop once if containing menu
// is Top or unread
// otherwise let menus know to recurse until true
// if is a nxt item, nav to that directly with M_DOWN
return (nxt == nil ?
((location == topMenu) || location.unread ?
M_PREV : M_PREVPREV)
: M_DOWN);
/* if it's 'prev', return to the previous menu */
if (loc == M_PREV || loc == M_QUIT)
return loc;
/* ignore other keys */
break;
}
}
}
;
Ok, final code snippet is menu loop recursion. Must be replicated (with minor tweak for do..while
conditional details) in showMenuHtml
and showMenuText
both DEEP within those respecitve methods. Eye glazing ahead.
/* ==============================================================================
* digest M_PREVPREV return code from above.
* includes additional return value in subMenu do loop
* look for JJMcC for only changes
* ============================================================================
*/
modify MenuItem
showMenuText(topMenu)
{
local i, selection, len, key = '', loc;
// * remember the key list *
curKeyList = topMenu.keyList;
// * bring our contents up to date, as needed *
updateContents();
// * keep going until the player exits this menu level *
do
{
// *
// * For text mode, print the title, then show the menu
// * options as a numbered list, then ask the player to make a
// * selection.
// *
// * get the number of items in the menu *
len = contents.length();
// * show the menu heading *
"\n<b><<heading>></b>\b";
// * show the contents as a numbered list *
for (i = 1; i <= len; i++)
{
// * leave room for two-digit numeric labels if needed *
if (len > 9 && i <= 10) "\ ";
// * show the item's number and title *
"<<i>>.\ <<contents[i].title>>\n";
}
// * show the main prompt *
gLibMessages.textMenuMainPrompt(topMenu.keyList);
// * main input loop *
do
{
// *
// * Get a key, and convert any alphabetics to lower-case.
// * Do not allow real-time interruptions, as menus are
// * meta-game interactions.
// *
key = inputManager.getKey(nil, nil).toLower();
// * check for a command key *
loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil});
// * also check for a numeric selection *
selection = toInteger(key);
} while ((selection < 1 || selection > len)
&& loc != M_QUIT && loc != M_PREV);
// *
// * show the selection if it's an ordinary key (an ordinary
// * key is represented by a single character; if we have more
// * than one character, it's one of the '[xxx]' special key
// * representations)
// *
if (key.length() == 1)
"<<key>>";
// * add a blank line *
"\b";
// *
// * If the selection is a number, then the player selected
// * that menu option. Call that submenu or topic's display
// * routine. If the routine returns nil, the player selected
// * QUIT, so we should quit as well.
// *
while (selection != 0 && selection <= contents.length())
{
// * invoke the child menu *
loc = contents[selection].showMenuText(topMenu);
// *
// * Check the result. If it's nil, it means QUIT; if it's
// * 'next', it means we're to proceed directly to our next
// * sub-menu. If the user didn't select QUIT, then
// * refresh our menu contents, as we'll be displaying our
// * menu again and its contents could have been affected
// * by the sub-menu invocation.
// *
switch(loc)
{
case M_QUIT:
// * they want to quit - leave the submenu loop *
selection = 0;
break;
case M_UP:
// * they want to go to the previous menu directly *
--selection;
break;
case M_DOWN:
// * they want to go to the next menu directly *
++selection;
break;
// JJMcC: NEW PROCESSING
// if return code is M_PREVPREV means we were in the last chapter
// if fully read, recurse up until hit menu with unread elements
// (add loop exit condition below as well)
// note the value of 'loc' is returned when exiting this level
case M_PREVPREV:
if (!unread) { // ie this menu level fully read
selection = 0; // text-mode specific loop end case
// must top out at topMenu
if (location == topMenu) loc = M_PREV;
// otherwise carry M_PREVPREV up to next level
break;
} // otherwise fall into M_PREV case
case M_PREV:
// *
// * they want to show this menu again - update our
// * contents so that we account for any changes made
// * while running the submenu, then leave the submenu
// * loop
// *
updateContents();
selection = 0;
// *
// * forget the 'prev' command - we don't want to back
// * up any further just yet, since the submenu just
// * wanted to get back to this point
// *
loc = nil;
break;
}
}
} while (loc != M_QUIT && loc != M_PREV && loc != M_PREVPREV);
// return the desired next action
return loc;
}
showMenuHtml(topMenu)
{
local len, selection = 1, loc;
local refreshTitle = true;
/* remember the key list */
curKeyList = topMenu.keyList;
/* update the menu contents, as needed */
updateContents();
/* keep going until the user exits this menu level */
do
{
/* refresh our title in the instructions area if necessary */
if (refreshTitle)
{
refreshTopMenuBanner(topMenu);
refreshTitle = nil;
}
/* get the number of items in the menu */
len = contents.length();
/* check whether we're in banner API or <banner> tag mode */
if (statusLine.statusDispMode == StatusModeApi)
{
/* banner API mode - clear our window */
contentsMenuBanner.clearWindow();
/* advise the interpreter of our best guess for our size */
if (fullScreenMode)
contentsMenuBanner.setSize(100, BannerSizePercent, nil);
else
contentsMenuBanner.setSize(len + 1, BannerSizeAbsolute,
true);
/* set up our desired color scheme */
"<body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
}
else
{
/*
* <banner> tag mode - set up our tag. In full-screen
* mode, set our height to 100% immediately; otherwise,
* leave the height unspecified so that we'll use the
* size of our contents. Use a border only if we're not
* taking up the full screen.
*/
"<banner id=MenuBody align=top
<<fullScreenMode ? 'height=100%' : 'border'>>
><body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
}
/* display our contents as a table */
"<table><tr><td width=<<indent>> > </td><td>";
for (local i = 1; i <= len; i++)
{
/*
* To get the alignment right, we have to print '>' on
* each and every line. However, we print it in the
* background color to make it invisible everywhere but
* in front of the current selection.
*/
if (selection != i)
"<font color=<<bgcolor>> >></font>";
else
">";
/* make each selection a plain (i.e. unhilighted) HREF */
"<a plain href=<<i>> ><<contents[i].title>></a><br>";
}
/* end the table */
"</td></tr></table>";
/* finish our display as appropriate */
if (statusLine.statusDispMode == StatusModeApi)
{
/* banner API - size the window to its contents */
if (!fullScreenMode)
contentsMenuBanner.sizeToContents();
}
else
{
/* <banner> tag - just close the tag */
"</banner>";
}
/* main input loop */
do
{
local key, events;
/*
* Read an event - don't allow real-time interruptions,
* since menus are meta-game interactions. Read an
* event rather than just a keystroke, because we want
* to let the user click on a menu item's HREF.
*/
events = inputManager.getEvent(nil, nil);
/* check the event type */
switch (events[1])
{
case InEvtHref:
/*
* the HREF's value is the selection number, or a
* 'previous' command
*/
if (events[2] == 'previous')
loc = M_PREV;
else
{
selection = toInteger(events[2]);
loc = M_SEL;
}
break;
case InEvtKey:
/* keystroke - convert any alphabetic to lower case */
key = events[2].toLower();
/* scan for a valid command key */
loc = topMenu.keyList.indexWhich(
{x: x.indexOf(key) != nil});
break;
}
/* handle arrow keys */
if (loc == M_UP)
{
selection--;
if (selection < 1)
selection = len;
}
else if (loc == M_DOWN)
{
selection++;
if (selection > len)
selection = 1;
}
} while (loc == nil);
/* if the player selected a sub-menu, invoke the selection */
while (loc == M_SEL
&& selection != 0
&& selection <= contents.length())
{
/*
* Invoke the sub-menu, checking for a QUIT result. If
* the user isn't quitting, we'll display our own menu
* again; in this case, update it now, in case something
* in the sub-menu changed our own contents.
*/
loc = contents[selection].showMenuHtml(topMenu);
/* see what we have */
switch (loc)
{
case M_UP:
/* they want to go directly to the previous menu */
loc = M_SEL;
--selection;
break;
case M_DOWN:
/* they want to go directly to the next menu */
loc = M_SEL;
++selection;
break;
// JJMcC: NEW PROCESSING
// if return code is M_PREVPREV means we were in the last chapter
// if fully read, recurse up until hit menu with unread elements
// (add loop exit condition below as well)
// note the value of 'loc' is returned when exiting this level
case M_PREVPREV:
if (!unread) { // ie this menu level fully read
// must top out at topMenu
if (location == topMenu) loc = M_PREV;
// otherwise carry M_PREVPREV up to next level
break;
} // if unread fall into M_PREV case
case M_PREV:
/* they want to return to this menu level */
loc = nil;
/* update our contents */
updateContents();
/* make sure we refresh the title area */
refreshTitle = true;
break;
}
}
} while (loc != M_QUIT && loc != M_PREV && loc != M_PREVPREV); // JJMcC
/* return the next status */
return loc;
}
;
There you go! Two changes that kept me from rewriting the thing whole hog.