More messing about with ShowMenu
mrangel
21 Mar 2018, 11:12Here 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 modifiesgame.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 callClearMenu
from within the callback, you also need to callMenuCancelledCallback(null)
. - The callback will get the parameters:
options
- a dictionary of options the player didn't choose frominput
- what the player actually enteredcommand
- 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, usinglinkcolour
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 beoption
's name if it's an object, andoption
otherwise
- String -
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.
- Returns a string containing all the HTML for a clickable link menu option. You need to use
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.
- Returns a list of options on a single line, generated using
ShowMenu
- Same parameters as the default one. This will call
StartShowMenu
, then either AddMenuNumberedList, AddMenuBulletList, or AddMenuFlatList (depending on the value ofgame.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.
- Same parameters as the default one. This will call
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>
hegemonkhan
21 Mar 2018, 11:45I 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
mrangel
21 Mar 2018, 11:51ShowMenu 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.

Doctor Agon
21 Mar 2018, 13:22Awesome 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.

Anonynn
22 Mar 2018, 19:11Awesome :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.