More messing about with ShowMenu

Here we are again, folks!

Here's my attempt at overriding some of the core functions so that ShowMenu gives you more flexibility.

Sadly untested, due to not having a windows machine on which to run the desktop version of quest. So there may be typos in here. Would appreciate if anyone can tell me if this works; or wrap these functions in a library to make them easier to use.

Main features:

  • Functions similar to ShowMenu that allow you to display a numbered list, a bullet list, a list of options on one line, or return a single option so you can include the menu options within a paragraph.
  • Option to hide the menu after making a selection, or just disable the links.
  • Callback for if the menu is cancelled (so in your dialogue menu, you could make the guy yell "Hey, where are you going?" if the player walks off)
  • Attributes on the game object to control which kind of menu ShowMenu shows. Default should look just like the default one.
  • Player can type one of the menu options.
  • Let the player choose an option by typing just one word, or the start of a word (in the same way as partially naming an object for a command); game attribute to determine if this catches the names of menu options that are also commands.
Attributes of the game object
  • game.alwaysshowmenunumbers - boolean. If true, menu options that aren't in a numbered list will have a number in parentheses displayed after them.
  • game.showmenutype - string. Can be "number", "bullet", "flat", or some other string which will be used as a bullet when ShowMenu is called. ("flat" displays the menu as a single line using FormatList).
  • game.hidemenus - boolean. If set to false, choosing a menu option will remove the links, but won't actually vanish the menu.
  • game.showmenutryharder - boolean. If true, typing a word that appears in exactly one of the menu options will choose that option; or the start of a word (like when typing object names).
  • game.showmenuunresolvedcommand - boolean. As above, but this behaviour will be pushed into the unresolved command handler, so that it only happens if the word isn't also a command. This may cause bugs if you have a command which modifies game.unresolvedcommandhandler during play.
Menu display functions
  • StartShowMenu (caption) - Starts a menu, and displays the caption.
    • If you do this more than once in the same menu, subsequent ones will just print the caption.
    • If you miss this out, it will be called as soon as you call AddMenuOption (sorry, that's ugly, as you might want to generate options before actually printing them. But I can't see a way around it)
  • EndShowMenu (allowCancel, hideAfterMenu, callback)
    • Marks the end of the menu, and sets the callback.
    • hideAfterMenu - boolean. If true, everything between StartShowMenu and EndShowMenu will be hidden once an option is chosen. If false, the links will be disabled but remain on screen.
  • MenuCancelledCallback (script)
    • Set a script to be run if the menu is not called.
    • Note that this is a misleading function name, as I couldn't think of a better one.
    • In the case of a menu with allowCancel set to false, this will be run each time the player enters something that can't be parsed as a valid choice. If you call ClearMenu from within the callback, you also need to call MenuCancelledCallback(null).
    • The callback will get the parameters:
      • options - a dictionary of options the player didn't choose from
      • input - what the player actually entered
      • command - if applicable, the command that was executed instead of choosing a menu option (note that in this case, the callback will be run after the command)
  • AddMenuOption (option, result, displayNumber)
    • Returns a string containing all the HTML for a clickable link menu option. You need to use msg() to pass this to the player.
    • option - The option to display. Calls GetDisplayAlias() if it's an object, using linkcolour if set, and ToString() for any other type that isn't a string.
    • result - The value that the 'result' variable will be set to in the callback.
      • String - result in the callback will be the string
      • Object - result in the callback will be the object's name
      • Anything else - result in the callback will be option's name if it's an object, and option otherwise
    • displayNumber - boolean. If true, will allow the player to select the option by typing its number. Will display like "Would you like Red (1), Blue (2), or Yellow (3)?". If this is a format string instead, ! will be replaced by the option, and # by the number.
  • AddMenuNumberedList (options)
    • Returns the HTML for a numbered list, like the original ShowMenu displays. You can modify this string if you want before outputting it.
    • options - I think this should handle an options list in any of the formats ShowMenu supports
  • AddMenuBulletList (options, bullet)
    • Returns a bulleted list; as above, but without numbering the options
    • If game.alwaysshowmenunumbers is set, will add bracketed numbers after each option
    • bullet - string, to display before each option. If "", will return a HTML <ul> element instead.
  • AddMenuFlatList (options, lastjoiner)
    • Returns a list of options on a single line, generated using FormatList.
    • lastjoiner - you probably want "[Or]" or "[And]" here.
  • ShowMenu
    • Same parameters as the default one. This will call StartShowMenu, then either AddMenuNumberedList, AddMenuBulletList, or AddMenuFlatList (depending on the value of game.showmenutype), then EndShowMenu.
    • I've attempted to ensure that if none of the game attributes mentioned above are set, this will behave exactly the same as the standard ShowMenu function.
The code
<function name="StartShowMenu" parameters="caption">
  if (not HasString(game, "menuoutputsection")) {
    game.menuoutputsection = StartNewOutputSection()
  }
  msg (caption)
  game.menuoptionskeys = NewStringList()
  game.menudisplayedoptions = NewStringDictionary()
  game.menucallback => {
    error ("Menu callback not set")
  }
</function>

<function name="MenuCancelledCallback" parameters="callback">
  game.menucancelcallback = callback
</function>

<function name="AddMenuOption" type="string" parameters="option, result, displayNumber">
  <![CDATA[
  if (not HasString(game, "menuoutputsection")) {
    StartShowMenu("")
  }
  if (TypeOf(option) = "object") {
    optionText = GetDisplayAlias(option)
    optionTag = option.name
    if (HasString(option, "linkcolour") and GetUIOption("UseGameColours") = "true") {
      colour = option.linkcolour
    }
    else {
      colour = GetLinkTextColour()
    }
    style = GetCurrentTextFormat(colour)
  }
  else if (TypeOf(option) = "string") {
    optionText = option
    optionTag = option
    style = GetCurrentLinkTextFormat()
  }
  else {
    optionText = ToString(option)
    optionTag = optionText
    style = ""
  }
  if (IsDefined("result")) {
    if (TypeOf(result) = "string") {
      if (LengthOf(result) > 0) {
        optionTag = result
      }
    }
    else if (TypeOf(result) = "object") {
      optionTag = result.name
    }
  }
  result = "<a class=\"cmdlink\" style=\"" + style + "\" onclick=\"ASLEvent('ShowMenuResponse','" + EscapeQuotes(optionTag) + "')\">" + optionText + "</a>"
  dictionary add (game.menudisplayedoptions, optionTag, optionText)
  if (TypeOf(displayNumber) = "string") {
    if (IndexOf (displayNumber, "!") > 0) {
      result = Replace (displayNumber, "!", result)
    }
    else {
      result = displayNumber + " " + result
    }
    if (IndexOf (result, "#") > 0) {
      list add (game.menuoptionskeys, optionTag)
      result = Replace (displaynumber, "#", ListCount(game.menuoptionskeys))
    }
  }
  else if (Equal(displayNumber, true) or GetBoolean(game, "alwaysshowmenunumbers")) {
    list add (game.menuoptionskeys, optionTag)
    result = result + " (" + ListCount(game.menuoptionskeys) + ")"
  }
  return (result)
  ]]>
</function>

<function name="EndShowMenu" parameters="allowCancel, hideAfterMenu, callback">
  if (not HasString(game, "menuoutputsection")) {
    error("Menu not started")
  }
  EndOutputSection (game.menuoutputsection)
  game.menuallowcancel = allowCancel
  game.menucallback = callback
  game.menuhideafter = hideAfterMenu
</function>

<function name="AddMenuNumberedList" parameters="options">
<![CDATA[
  if (TypeOf(options) = "object") {
    options = GetDirectChildren(options)
  }
  else if (TypeOf(options) = "string") {
    options = Split(options)
  }

  result = NewStringList()
  foreach (o, options) {
    optionText = o
    if (EndsWith(TypeOf(o), "dictionary")) {
      optionText = DictionaryItem(options, o)
    }
    list add (result, AddMenuOption(optionText, o, "#. "))
  }
  return (Join (result, "<br/>"))
]]>
</function>

<function name="AddMenuBulletList" parameters="options, bullet">
<![CDATA[
  if (not TypeOf(bullet) = "string") {
    bullet = ""
  }
  if (TypeOf(options) = "object") {
    options = GetDirectChildren(options)
  }
  else if (TypeOf(options) = "string") {
    options = Split(options)
  }

  result = NewStringList()
  foreach (o, options) {
    optionText = o
    if (EndsWith(TypeOf(o), "dictionary")) {
      optionText = DictionaryItem(options, o)
    }
    list add (result, bullet + AddMenuOption(optionText, o, false))
  }
  if (bullet = "") {
    return ("<ul><li>" + Join(result, "</li>\n<li>") + "</li></ul>\n")
  }
  else {
    return (Join (result, "<br/>"))
  }
]]>
</function>

<function name="AddMenuFlatList" parameters="options, lastjoiner">
<![CDATA[
  if (TypeOf(options) = "object") {
    options = GetDirectChildren(options)
  }
  else if (TypeOf(options) = "string") {
    options = Split(options)
  }

  result = NewStringList()
  foreach (o, options) {
    optionText = o
    if (EndsWith(TypeOf(o), "dictionary")) {
      optionText = DictionaryItem(options, o)
    }
    list add (result, "· " + AddMenuOption(optionText, o, false))
  }
  return (FormatList(result, ",", lastjoiner, ""))
]]>
</function>

<function name="ShowMenu" parameters="caption, options, allowCancel, callback">
  StartShowMenu(caption)
  type = "number"
  if (Equal (game.alwaysshowmenunumbers, false)) {
    type = "bullet"
  }
  if (HasString (game, "showmenutype")) {
    type = LCase(game.showmenutype)
  }
  if (type = "bullet") {
    msg (AddMenuBulletList(options), "")
  }
  else if (type = "flat") {
    msg (AddMenuFlatList(options, "[Or]"))
  }
  else if (type = "number") {
    msg (AddMenuNumberedList(options))
  }
  else {
    msg (AddMenuBulletList(options), type)
  }
  if (not HasBoolean(game, "hidemenus")) {
    game.hidemenus = true
  }
  EndShowMenu(allowCancel, game.hidemenus, callback)
</function>

<function name="HandleMenuTextResponse" parameters="input" type="boolean">
  <![CDATA[
  handled = false
  if (IsInt(input)) {
    number = ToInt(input)
    if (number > 0 and number <= ListCount(game.menuoptionskeys)) {
      ShowMenuResponse(StringListItem(game.menuoptionskeys, number - 1))
      return (true)
    }
  }
  else if(HasAttribute(game, "menudisplayedoptions")) {
    foreach (option, game.menudisplayedoptions) {
      if (LCase(Trim(StringDictionaryItem(game.menudisplayedoptions, option))) = LCase(Trim(input))) {
        ShowMenuResponse(option)
        game.menucancelcallback = null
        return (true)
      }
    }
    if (GetBoolean(game, "showmenutryharder") or not GetBoolean(game, "menuallowcancel")) {
      handled = HandleMenuTextHarder (game, input)
      return (handled)
    }
    else if (GetBoolean(game, "showmenuunresolvedcommand")) {
      if (HasScript(game, "unresolvedcommandhandler")) {
        game.unresolvedcommandbackup = game.unresolvedcommandhandler
      }
      SetTurnTimeout(0) {
        if (HasScript (showmenu_data_object, "menucancelcallback")) {
          params = NewDictionary()
          dictionary add (params, "options", showmenu_data_object.menudisplayedoptions)
          dictionary add (params, "input", game.pov.currentcommand)
          dictionary add (params, "command", game.pov.currentcommandpattern)
          do (showmenu_data_object, "menucancelcallback", params)
        }
        ClearMenu()
        if (HasScript(game, "unresolvedcommandbackup")) {
          game.unresolvedcommandhandler = game.unresolvedcommandbackup
          game.unresolvedcommandbackup = null
        }
      }
      game.unresolvedcommandhandler => {
        handled = HandleMenuTextHarder (showmenu_data_object, command)
        if (HasScript(game, "unresolvedcommandbackup")) {
          game.unresolvedcommandhandler = game.unresolvedcommandbackup
          game.unresolvedcommandbackup = null
        }
        if (not handled) {
          if (HasScript(game, "unresolvedcommandhandler")) {
            params = NewDictionary()
            dictionary add(params, "command", command)
            do (game, "unresolvedcommandhandler", params)
          } else {
            msg (Template("UnrecognisedCommand"))
          }
        }
      }
    }
  }
  return (false)
  ]]>
</function>

<function name="HandleMenuTextHarder" parameters="menu, input">
  possibilities = NewStringList()
  options = menu.menudisplayedoptions
  foreach (option, options) {
    list add (possibilities, option)
  }
  foreach (word, Split(LCase(input), " ")) {
    stillpossible = NewStringList()
    foreach (option, possibilities) {
      words_in_option = Split(LCase(StringDictionaryItem(options, option)), " ")
      found = false
      foreach (w, words_in_option) {
        if (StartsWith(w, word)) {
          found = true
        }
      }
      if (found) {
        list add (stillpossible, option)
      }
    }
    possibilities = stillpossible
  }
  if (ListCount (possibilities) = 1) {
    ShowMenuResponse(possibilities[0])
    ClearMenu()
    return (true)
  }
  else if (HasScript(menu, "menucancelcallback")) {
    params = NewDictionary()
    dictionary add (params, "options", options)
    dictionary add (params, "input", input)
    do (menu, "menucancelcallback", params)
    if (GetBoolean(game, "menuallowcancel")) {
      ClearMenu()
    }
  }
  return (false)
</function>
  
<function name="ClearMenu">
  if (HasString(game, "menuoutputsection")) {
    if (GetBoolean(game, "menuhideafter")) {
      HideOutputSection(game.menuoutputsection)
    }
    else {
      JS.eval("name = '"+game.menuoutputsection+"';EndOutputSection(name);$('.' + name + ' .cmdlink').attr('onclick', '');")
    }
    game.menuoutputsection = null
  }

  // If we're running an unresolved command script to check player input against menu options
  // we need to move all the menu data into a new object
  // to ensure that it doesn't screw up if the player entered a command that creates a new menu
  //
  // But destroy it the next time ClearMenu is called

  if (not GetObject("showmenu_data_object") = null) {
    destroy ("showmenu_data_object")
  }

  foreach (attr, Split("menuoptionskeys;menudisplayedoptions;menuoutputsection;menucallback;menuallowcancel;menucancelcallback")) {
    if (HasAttribute(game, attr)) {
      if (GetBoolean(game, "showmenuunresolvedcommand")) {
        if (GetObject("showmenu_data_object") = null) {
          create ("showmenu_data_object")
        }
        set (showmenu_data_object, attr, GetAttribute(game, attr))
      }
      set (game, attr, null)
    }
  }
</function>

I personally like using numbers for input (and like using typed-in input), as much as possible / whenever possible, as it's the least amount of typing possible, to help with arithritis, carpal tunnel syndrome, etc stuff, lol, and it's also easy and fast too.

So, I'm trying to create my own menu system using numbers for typed-in input (and thus menu coding), though I'm still fumbling my way through it, as I'm not quite at your level, mrangel, lol.


anyways, awesome code/library thread/post, as I'll be studying it for help/ideas for my own menu system (crediting you of course as/if I use any of it: concrete code or concept/design ideas), hehe


ShowMenu allows you to type in the numbers, which is pretty neat.
I just tweaked it so that it allows you to click the option or type the number or type the start of the option text.
(as well as allowing some different stylistic choices)

This isn't very neat code, and I wouldn't be surprised if there's still errors in there.


Awesome coding as always, like the names of some of your attributes too.

Being a bit silly here, so please forgive me.

alwaysshowmenunumbers - always show me nun umbers - I do like the look of an umber on a nun too.
showmenutryharder - show me nutry harder - Can't work out if that's a nut-tree or you're just boasting.

Seriously though, good piece of coding.


Awesome :D I love this Mr.Angel, especially customizing different messages that can be printed if the player cancels the menu or something. So cool! It definitely would be great too to have the option of turning on or off the numbered lists. Man, I wish I was talented >.<

Anonynn.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums