From 94e9b4370ec007ad014929f9f79d212629400b99 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 15:01:51 -0700 Subject: [PATCH 01/12] Add simple menu tree selection contrib This contrib module allows developers to generate an EvMenu instance with options sourced from a multi-line string, which supports categories, back and forth menu navigation, option descriptions, and passing selections to custom callbacks. This allows for easier dynamic menus and much faster deployment of simple menu trees which does not require the manual definition of menu nodes and option dictionary-lists. --- evennia/contrib/tree_select.py | 522 +++++++++++++++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 evennia/contrib/tree_select.py diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py new file mode 100644 index 0000000000..56305340b3 --- /dev/null +++ b/evennia/contrib/tree_select.py @@ -0,0 +1,522 @@ +""" +Easy menu selection tree + +Contrib - Tim Ashley Jenkins 2017 + +This module allows you to create and initialize an entire branching EvMenu +instance with nothing but a multi-line string passed to one function. + +EvMenu is incredibly powerful and flexible, but using it for simple menus +can often be fairly cumbersome - a simple menu that can branch into five +categories would require six nodes, each with options represented as a list +of dictionaries. + +This module provides a function, init_tree_selection, which acts as a frontend +for EvMenu, dynamically sourcing the options from a multi-line string you provide. +For example, if you define a string as such: + + TEST_MENU = '''Foo + Bar + Baz + Qux''' + +And then use TEST_MENU as the 'treestr' source when you call init_tree_selection +on a player: + + init_tree_selection(TEST_MENU, caller, callback) + +The player will be presented with an EvMenu, like so: + + ___________________________ + + Make your selection: + ___________________________ + + Foo + Bar + Baz + Qux + +Making a selection will pass the selection's key to the specified callback as a +string along with the caller, as well as the index of the selection (the line number +on the source string) along with the source string for the tree itself. + +In addition to specifying selections on the menu, you can also specify categories. +Categories are indicated by putting options below it preceded with a '-' character. +If a selection is a category, then choosing it will bring up a new menu node, prompting +the player to select between those options, or to go back to the previous menu. In +addition, categories are marked by default with a '[+]' at the end of their key. Both +this marker and the option to go back can be disabled. + +Categories can be nested in other categories as well - just go another '-' deeper. You +can do this as many times as you like. There's no hard limit to the number of +categories you can go down. + +For example, let's add some more options to our menu, turning 'Foo' into a category. + + TEST_MENU = '''Foo + Bar + -You've got to know + --When to hold em + --When to fold em + --When to walk away + Baz + Qux''' + +Now when we call the menu, we can see that 'Foo' has become a category instead of a +selectable option. + + _______________________________ + + Make your selection: + _______________________________ + + Foo + Bar [+] + Baz + Qux + +Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it. + + ________________________________________________________________ + + Bar + ________________________________________________________________ + + You've got to know [+] + << Go Back: Return to the previous menu. + +Just the one option, which is a category itself, and the option to go back, which will +take us back to the previous menu. Let's select 'You've got to know'. + + ________________________________________________________________ + + You've got to know + ________________________________________________________________ + + When to hold em + When to fold em + When to walk away + << Go Back: Return to the previous menu. + +Now we see the three options listed under it, too. We can select one of them or use 'Go +Back' to return to the 'Bar' menu we were just at before. It's very simple to make a +branching tree of selections! + +One last thing - you can set the descriptions for the various options simply by adding a +':' character followed by the description to the option's line. For example, let's add a +description to 'Baz' in our menu: + + TEST_MENU = '''Foo + Bar + -You've got to know + --When to hold em + --When to fold em + --When to walk away + Baz: Look at this one: the best option. + Qux''' + +Now we see that the Baz option has a description attached that's separate from its key: + + _______________________________________________________________ + + Make your selection: + _______________________________________________________________ + + Foo + Bar [+] + Baz: Look at this one: the best option. + Qux + +And that's all there is to it! For simple branching-tree selections, using this system is +much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic +options much easier - since the source of the menu tree is just a string, you could easily +generate that string procedurally before passing it to the init_tree_selection function. +For example, if a player casts a spell or does an attack without specifying a target, instead +of giving them an error, you could present them with a list of valid targets to select by +generating a multi-line string of targets and passing it to init_tree_selection, with the +callable performing the maneuver once a selection is made. + +This selection system only works for simple branching trees - doing anything really complicated +like jumping between categories or prompting for arbitrary input would still require a full +EvMenu implementation. For simple selections, however, I'm sure you will find using this function +to be much easier! + +Included in this module is a sample menu and function which will let a player change the color +of their name - feel free to mess with it to get a feel for how this system works by importing +this module in your game's default_cmdsets.py module and adding CmdNameColor to your default +character's command set. +""" + +from evennia.utils import evmenu +from evennia import Command + +def init_tree_selection(treestr, caller, callback, + index=None, mark_category=True, go_back=True, + cmd_on_exit="look", + start_text="Make your selection:"): + """ + Prompts a player to select an option from a menu tree given as a multi-line string. + + Args: + treestr (str): Multi-lne string representing menu options + caller (obj): Player to initialize the menu for + callback (callable): Function to run when a selection is made. Must take 4 args: + treestr (str): Menu tree string given above + caller (obj): Caller given above + index (int): Index of final selection + selection (str): Key of final selection + + Options: + index (int or None): Index to start the menu at, or None for top level + mark_category (bool): If True, marks categories with a [+] symbol in the menu + go_back (bool): If True, present an option to go back to previous categories + start_text (str): Text to display at the top level of the menu + cmd_on_exit(str): Command to enter when the menu exits - 'look' by default + + + Notes: + This function will initialize an instance of EvMenu with options generated + dynamically from the source string, and passes the menu user's selection to + a function of your choosing. The EvMenu is made of a single, repeating node, + which will call itself over and over at different levels of the menu tree as + categories are selected. + + Once a non-category selection is made, the user's selection will be passed to + the given callable, both as a string and as an index number. The index is given + to ensure every selection has a unique identifier, so that selections with the + same key in different categories can be distinguished between. + + The menus called by this function are not persistent and cannot perform + complicated tasks like prompt for arbitrary input or jump multiple category + levels at once - you'll have to use EvMenu itself if you want to take full + advantage of its features. + """ + + # Pass kwargs to store data needed in the menu + kwargs = { + "index":index, + "mark_category":mark_category, + "go_back":go_back, + "treestr":treestr, + "callback":callback, + "start_text":start_text + } + + # Initialize menu of selections + evmenu.EvMenu(caller, "evennia.contrib.tree_select", startnode="menunode_treeselect", + startnode_input=None, cmd_on_exit=cmd_on_exit, **kwargs) + +def dashcount(entry): + """ + Counts the number of dashes at the beginning of a string. This + is needed to determine the depth of options in categories. + + Args: + entry (str): String to count the dashes at the start of + + Returns: + dashes (int): Number of dashes at the start + """ + dashes = 0 + for char in entry: + if char == "-": + dashes += 1 + else: + return dashes + return dashes + +def is_category(treestr, index): + """ + Determines whether an option in a tree string is a category by + whether or not there are additional options below it. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Which line of the string to test + + Returns: + is_category (bool): Whether the option is a category + """ + opt_list = treestr.split('\n') + # Not a category if it's the last one in the list + if index == len(opt_list) - 1: + return False + # Not a category if next option is not one level deeper + return not bool(dashcount(opt_list[index+1]) != dashcount(opt_list[index]) + 1) + +def parse_opts(treestr, category_index=None): + """ + Parses a tree string and given index into a list of options. If + category_index is none, returns all the options at the top level of + the menu. If category_index corresponds to a category, returns a list + of options under that category. If category_index corresponds to + an option that is not a category, it's a selection and returns True. + + Args: + treestr (str): Multi-line string representing menu options + category_index (int): Index of category or None for top level + + Returns: + kept_opts (list or True): Either a list of options in the selected + category or True if a selection was made + """ + dash_depth = 0 + opt_list = treestr.split('\n') + kept_opts = [] + + # If a category index is given + if category_index != None: + # If given index is not a category, it's a selection - return True. + if not is_category(treestr, category_index): + return True + # Otherwise, change the dash depth to match the new category. + dash_depth = dashcount(opt_list[category_index]) + 1 + # Delete everything before the category index + opt_list = opt_list [category_index+1:] + + # Keep every option (referenced by index) at the appropriate depth + cur_index = 0 + for option in opt_list: + if dashcount(option) == dash_depth: + if category_index == None: + kept_opts.append((cur_index, option[dash_depth:])) + else: + kept_opts.append((cur_index + category_index + 1, option[dash_depth:])) + # Exits the loop if leaving a category + if dashcount(option) < dash_depth: + return kept_opts + cur_index += 1 + return kept_opts + +def index_to_selection(treestr, index, desc=False): + """ + Given a menu tree string and an index, returns the corresponding selection's + name as a string. If 'desc' is set to True, will return the selection's + description as a string instead. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Index to convert to selection key or description + + Options: + desc (bool): If true, returns description instead of key + + Returns: + selection (str): Selection key or description if 'desc' is set + """ + opt_list = treestr.split('\n') + # Fetch the given line + selection = opt_list[index] + # Strip out the dashes at the start + selection = selection[dashcount(selection):] + # Separate out description, if any + if ":" in selection: + # Split string into key and description + selection = selection.split(':', 1) + selection[1] = selection[1].strip(" ") + else: + # If no description given, set description to None + selection = [selection, None] + if not desc: + return selection[0] + else: + return selection[1] + +def go_up_one_category(treestr, index): + """ + Given a menu tree string and an index, returns the category that the given option + belongs to. Used for the 'go back' option. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Index to determine the parent category of + + Returns: + parent_category (int): Index of parent category + """ + opt_list = treestr.split('\n') + # Get the number of dashes deep the given index is + dash_level = dashcount(opt_list[index]) + # Delete everything after the current index + opt_list = opt_list[:index+1] + + + # If there's no dash, return 'None' to return to base menu + if dash_level == 0: + return None + current_index = index + # Go up through each option until we find one that's one category above + for selection in reversed(opt_list): + if dashcount(selection) == dash_level - 1: + return current_index + current_index -= 1 + +def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): + """ + Takes a list of options processed by parse_opts and turns it into + a list/dictionary of menu options for use in menunode_treeselect. + + Args: + treestr (str): Multi-line string representing menu options + optlist (list): List of options to convert to EvMenu's option format + index (int): Index of current category + mark_category (bool): Whether or not to mark categories with [+] + go_back (bool): Whether or not to add an option to go back in the menu + + Returns: + menuoptions (list of dicts): List of menu options formatted for use + in EvMenu, each passing a different "newindex" kwarg that changes + the menu level or makes a selection + """ + + menuoptions = [] + cur_index = 0 + for option in optlist: + index_to_add = optlist[cur_index][0] + menuitem = {} + keystr = index_to_selection(treestr, index_to_add) + if mark_category and is_category(treestr, index_to_add): + # Add the [+] to the key if marking categories, and the key by itself as an alias + menuitem["key"] = [keystr + " [+]", keystr] + else: + menuitem["key"] = keystr + # Get the option's description + desc = index_to_selection(treestr, index_to_add, desc=True) + if desc: + menuitem["desc"] = desc + # Passing 'newindex' as a kwarg to the node is how we move through the menu! + menuitem["goto"] = ["menunode_treeselect", {"newindex":index_to_add}] + menuoptions.append(menuitem) + cur_index += 1 + # Add option to go back, if needed + if index != None and go_back == True: + gobackitem = {"key":["<< Go Back", "go back", "back"], + "desc":"Return to the previous menu.", + "goto":["menunode_treeselect", {"newindex":go_up_one_category(treestr, index)}]} + menuoptions.append(gobackitem) + return menuoptions + +def menunode_treeselect(caller, raw_string, **kwargs): + """ + This is the repeating menu node that handles the tree selection. + """ + + # If 'newindex' is in the kwargs, change the stored index. + if "newindex" in kwargs: + caller.ndb._menutree.index = kwargs["newindex"] + + # Retrieve menu info + index = caller.ndb._menutree.index + mark_category = caller.ndb._menutree.mark_category + go_back = caller.ndb._menutree.go_back + treestr = caller.ndb._menutree.treestr + callback = caller.ndb._menutree.callback + start_text = caller.ndb._menutree.start_text + + # List of options if index is 'None' or category, or 'True' if a selection + optlist = parse_opts(treestr, category_index=index) + + # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. + if optlist == True: + selection = index_to_selection(treestr, index) + callback(caller, treestr, index, selection) + + # Returning None, None ends the menu. + return None, None + + # Otherwise, convert optlist to a list of menu options. + else: + options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back) + if index == None: + # Use start_text for the menu text on the top level + text = start_text + else: + # Use the category name and description (if any) as the menu text + if index_to_selection(treestr, index, desc=True) != None: + text = "|w" + index_to_selection(treestr, index) + "|n: " + index_to_selection(treestr, index, desc=True) + else: + text = "|w" + index_to_selection(treestr, index) + "|n" + return text, options + +# The rest of this module is for the example menu and command! It'll change the color of your name. + +""" +Here's an example string that you can initialize a menu from. Note the dashes at +the beginning of each line - that's how menu option depth and hierarchy is determined. +""" + +NAMECOLOR_MENU = """Set name color: Choose a color for your name! +-Red shades: Various shades of |511red|n +--Red: |511Set your name to Red|n +--Pink: |533Set your name to Pink|n +--Maroon: |301Set your name to Maroon|n +-Orange shades: Various shades of |531orange|n +--Orange: |531Set your name to Orange|n +--Brown: |321Set your name to Brown|n +--Sienna: |420Set your name to Sienna|n +-Yellow shades: Various shades of |551yellow|n +--Yellow: |551Set your name to Yellow|n +--Gold: |540Set your name to Gold|n +--Dandelion: |553Set your name to Dandelion|n +-Green shades: Various shades of |141green|n +--Green: |141Set your name to Green|n +--Lime: |350Set your name to Lime|n +--Forest: |032Set your name to Forest|n +-Blue shades: Various shades of |115blue|n +--Blue: |115Set your name to Blue|n +--Cyan: |155Set your name to Cyan|n +--Navy: |113Set your name to Navy|n +-Purple shades: Various shades of |415purple|n +--Purple: |415Set your name to Purple|n +--Lavender: |535Set your name to Lavender|n +--Fuchsia: |503Set your name to Fuchsia|n +Remove name color: Remove your name color, if any""" + +class CmdNameColor(Command): + """ + Set or remove a special color on your name. Just an example for the + easy menu selection tree contrib. + """ + + key = "namecolor" + + def func(self): + # This is all you have to do to initialize a menu! + init_tree_selection(TEST_MENU, self.caller, + change_name_color, + start_text="Name color options:") + +def change_name_color(caller, treestr, index, selection): + """ + Changes a player's name color. + + Args: + caller (obj): Character whose name to color. + treestr (str): String for the color change menu - unused + index (int): Index of menu selection - unused + selection (str): Selection made from the name color menu - used + to determine the color the player chose. + """ + + # Store the caller's uncolored name + if not caller.db.uncolored_name: + caller.db.uncolored_name = caller.key + + # Dictionary matching color selection names to color codes + colordict = { "Red":"|511", "Pink":"|533", "Maroon":"|301", + "Orange":"|531", "Brown":"|321", "Sienna":"|420", + "Yellow":"|551", "Gold":"|540", "Dandelion":"|553", + "Green":"|141", "Lime":"|350", "Forest":"|032", + "Blue":"|115", "Cyan":"|155", "Navy":"|113", + "Purple":"|415", "Lavender":"|535", "Fuchsia":"|503"} + + # I know this probably isn't the best way to do this. It's just an example! + if selection == "Remove name color": # Player chose to remove their name color + caller.key = caller.db.uncolored_name + caller.msg("Name color removed.") + elif selection in colordict: + newcolor = colordict[selection] # Retrieve color code based on menu selection + caller.key = newcolor + caller.db.uncolored_name + "|n" # Add color code to caller's name + caller.msg(newcolor + ("Name color changed to %s!" % selection) + "|n") + From fc16898db318bdec2d0cb0c7d4531371590423d8 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 16:21:32 -0700 Subject: [PATCH 02/12] Added unit tests for tree_select contrib --- evennia/contrib/tests.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 79a61ce65d..e2148fa0da 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1206,6 +1206,36 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() +# Test tree select + +from evennia.contrib import tree_select + +TREE_MENU_TESTSTR = """Foo +Bar +-Baz +--Baz 1 +--Baz 2 +-Qux""" + +class TestTreeSelectFunc(EvenniaTest): + + def test_tree_functions(self): + # Dash counter + self.assertTrue(tree_select.dashcount("--test") == 2) + # Is category + self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) == True) + # Parse options + self.assertTrue(tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) == [(3, "Baz 1"), (4, "Baz 2")]) + # Index to selection + self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz") + # Go up one category + self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2) + # Option list to menu options + test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) + optlist_to_menu_expected_result = [{'goto': ['menunode_treeselect', {'newindex': 3}], 'key': 'Baz 1'}, + {'goto': ['menunode_treeselect', {'newindex': 4}], 'key': 'Baz 2'}, + {'goto': ['menunode_treeselect', {'newindex': 1}], 'key': ['<< Go Back', 'go back', 'back'], 'desc': 'Return to the previous menu.'}] + self.assertTrue(tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) == optlist_to_menu_expected_result) # Test of the unixcommand module From 9a047a6362b016de64fe22322f86ec5d73cfcee7 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 16:24:11 -0700 Subject: [PATCH 03/12] Add tree select to README.md --- evennia/contrib/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 63abb2f713..52e63b7b1a 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -50,6 +50,9 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. +* Tree Select (FlutterSprite 2017) - A simple system for creating a + branching EvMenu with selection options sourced from a single + multi-line string. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. * UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. From 5ea86d86fa498396c74c9b0bf4027871b1c7d98a Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 19:16:43 -0700 Subject: [PATCH 04/12] Fix typo in documentation --- evennia/contrib/tree_select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 56305340b3..a92cd6c2a7 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -52,7 +52,7 @@ Categories can be nested in other categories as well - just go another '-' deepe can do this as many times as you like. There's no hard limit to the number of categories you can go down. -For example, let's add some more options to our menu, turning 'Foo' into a category. +For example, let's add some more options to our menu, turning 'Bar' into a category. TEST_MENU = '''Foo Bar @@ -63,7 +63,7 @@ For example, let's add some more options to our menu, turning 'Foo' into a categ Baz Qux''' -Now when we call the menu, we can see that 'Foo' has become a category instead of a +Now when we call the menu, we can see that 'Bar' has become a category instead of a selectable option. _______________________________ From 9ab3d278755492877d68c2b0e3e7d273cfa78772 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 5 Nov 2017 18:36:08 -0800 Subject: [PATCH 05/12] Adds health bar module Adds a versatile function that will return a given current and maximum value as a "health bar" rendered with ANSI or xterm256 background color codes. This function has many options, such as being able to specify the length of the bar, its colors (including changing color depending on how full the bar is), what text is included inside the bar and how the text is justified within it. --- evennia/contrib/health_bar.py | 103 ++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 evennia/contrib/health_bar.py diff --git a/evennia/contrib/health_bar.py b/evennia/contrib/health_bar.py new file mode 100644 index 0000000000..c3d4af1c52 --- /dev/null +++ b/evennia/contrib/health_bar.py @@ -0,0 +1,103 @@ +""" +Health Bar + +Contrib - Tim Ashley Jenkins 2017 + +The function provided in this module lets you easily display visual +bars or meters - "health bar" is merely the most obvious use for this, +though these bars are highly customizable and can be used for any sort +of appropriate data besides player health. + +Today's players may be more used to seeing statistics like health, +stamina, magic, and etc. displayed as bars rather than bare numerical +values, so using this module to present this data this way may make it +more accessible. Keep in mind, however, that players may also be using +a screen reader to connect to your game, which will not be able to +represent the colors of the bar in any way. By default, the values +represented are rendered as text inside the bar which can be read by +screen readers. + +The health bar will account for current values above the maximum or +below 0, rendering them as a completely full or empty bar with the +values displayed within. +""" + +def display_meter(cur_value, max_value, + length=30, fill_color=["R", "Y", "G"], + empty_color="B", text_color="w", + align="left", pre_text="", post_text="", + show_values=True): + """ + Represents a current and maximum value given as a "bar" rendered with + ANSI or xterm256 background colors. + + Args: + cur_value (int): Current value to display + max_value (int): Maximum value to display + + Options: + length (int): Length of meter returned, in characters + fill_color (list): List of color codes for the full portion + of the bar, sans any sort of prefix - both ANSI and xterm256 + colors are usable. When the bar is empty, colors toward the + start of the list will be chosen - when the bar is full, colors + towards the end are picked. You can adjust the 'weights' of + the changing colors by adding multiple entries of the same + color - for example, if you only want the bar to change when + it's close to empty, you could supply ['R','Y','G','G','G'] + empty_color (str): Color code for the empty portion of the bar. + text_color (str): Color code for text inside the bar. + align (str): "left", "right", or "center" - alignment of text in the bar + pre_text (str): Text to put before the numbers in the bar + post_text (str): Text to put after the numbers in the bar + show_values (bool): If true, shows the numerical values represented by + the bar. It's highly recommended you keep this on, especially if + there's no info given in pre_text or post_text, as players on screen + readers will be unable to read the graphical aspect of the bar. + """ + # Start by building the base string. + num_text = "" + if show_values: + num_text = "%i / %i" % (cur_value, max_value) + bar_base_str = pre_text + num_text + post_text + # Cut down the length of the base string if needed + if len(bar_base_str) > length: + bar_base_str = bar_base_str[:length] + # Pad and align the bar base string + if align == "right": + bar_base_str = bar_base_str.rjust(length, " ") + elif align == "center": + bar_base_str = bar_base_str.center(length, " ") + else: + bar_base_str = bar_base_str.ljust(length, " ") + + if max_value < 1: # Prevent divide by zero + max_value = 1 + if cur_value < 0: # Prevent weirdly formatted 'negative bars' + cur_value = 0 + if cur_value > max_value: # Display overfull bars correctly + cur_value = max_value + + # Now it's time to determine where to put the color codes. + percent_full = float(cur_value) / float(max_value) + split_index = round(float(length) * percent_full) + # Determine point at which to split the bar + split_index = int(split_index) + + # Separate the bar string into full and empty portions + full_portion = bar_base_str[:split_index] + empty_portion = bar_base_str[split_index:] + + # Pick which fill color to use based on how full the bar is + fillcolor_index = (float(len(fill_color)) * percent_full) + fillcolor_index = int(round(fillcolor_index)) - 1 + fillcolor_code = "|[" + fill_color[fillcolor_index] + + # Make color codes for empty bar portion and text_color + emptycolor_code = "|[" + empty_color + textcolor_code = "|" + text_color + + # Assemble the final bar + final_bar = fillcolor_code + textcolor_code + full_portion + "|n" + emptycolor_code + textcolor_code + empty_portion + "|n" + + return final_bar From 8d5e167a8dff7a2362ceb8d41b37475e3b06753f Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 5 Nov 2017 18:42:55 -0800 Subject: [PATCH 06/12] Add unit tests for health_bar contrib --- evennia/contrib/tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 79a61ce65d..4f4ac8c4bf 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -651,6 +651,15 @@ class TestGenderSub(CommandTest): char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1) txt = "Test |p gender" self.assertEqual(gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender") + +# test health bar contrib + +from evennia.contrib import health_bar + +class TestHealthBar(EvenniaTest): + def test_healthbar(self): + expected_bar_str = "|[G|w |n|[B|w test24 / 200test |n" + self.assertTrue(health_bar.display_meter(24, 200, length=40, pre_text="test", post_text="test", align="center") == expected_bar_str) # test mail contrib From 4a554a44094ba31c1b545f761213ccb56efbf30e Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 01:56:35 -0800 Subject: [PATCH 07/12] Add mention of how the callback is used --- evennia/contrib/tree_select.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index a92cd6c2a7..2eab9450d7 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -127,7 +127,16 @@ Now we see that the Baz option has a description attached that's separate from i Bar [+] Baz: Look at this one: the best option. Qux + +Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call +your specified callback with the selection, like so: + + callback(TEST_MENU, caller, 0, "Foo") +The index of the selection is given along with a string containing the selection's key. +That way, if you have two selections in the menu with the same key, you can still +differentiate between them. + And that's all there is to it! For simple branching-tree selections, using this system is much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic options much easier - since the source of the menu tree is just a string, you could easily From ebe7c6f4b3df77bcb75aee77b3556ace365a9b8b Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 02:21:49 -0800 Subject: [PATCH 08/12] Fix order of args for the callback in documentation --- evennia/contrib/tree_select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 2eab9450d7..d3befd9614 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -131,7 +131,7 @@ Now we see that the Baz option has a description attached that's separate from i Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call your specified callback with the selection, like so: - callback(TEST_MENU, caller, 0, "Foo") + callback(caller, TEST_MENU, 0, "Foo") The index of the selection is given along with a string containing the selection's key. That way, if you have two selections in the menu with the same key, you can still @@ -171,8 +171,8 @@ def init_tree_selection(treestr, caller, callback, treestr (str): Multi-lne string representing menu options caller (obj): Player to initialize the menu for callback (callable): Function to run when a selection is made. Must take 4 args: - treestr (str): Menu tree string given above caller (obj): Caller given above + treestr (str): Menu tree string given above index (int): Index of final selection selection (str): Key of final selection From 7d10570424b8e8818b52de32bbafdd58a733ac02 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:46:59 -0800 Subject: [PATCH 09/12] Catch callback errors with logger --- evennia/contrib/tree_select.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index d3befd9614..85970f58b4 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -158,6 +158,7 @@ character's command set. """ from evennia.utils import evmenu +from evennia.utils.logger import log_trace from evennia import Command def init_tree_selection(treestr, caller, callback, @@ -429,7 +430,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. if optlist == True: selection = index_to_selection(treestr, index) - callback(caller, treestr, index, selection) + try: + callback(caller, treestr, index, selection) + except: + log_trace("Error in tree selection callback.") # Returning None, None ends the menu. return None, None From 53d8536744f5a24587d9b4878c3893f97ad807e9 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:51:53 -0800 Subject: [PATCH 10/12] Update tree_select.py --- evennia/contrib/tree_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 85970f58b4..8aca99ec9e 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -432,7 +432,7 @@ def menunode_treeselect(caller, raw_string, **kwargs): selection = index_to_selection(treestr, index) try: callback(caller, treestr, index, selection) - except: + except Exception: log_trace("Error in tree selection callback.") # Returning None, None ends the menu. From f0630535e0dbc9f04342d22857661213b2143236 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:58:23 -0800 Subject: [PATCH 11/12] Fix variable in example menu function I changed this while making unit tests and forgot to change it back. Whoops! --- evennia/contrib/tree_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 8aca99ec9e..5b8f038e33 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -496,7 +496,7 @@ class CmdNameColor(Command): def func(self): # This is all you have to do to initialize a menu! - init_tree_selection(TEST_MENU, self.caller, + init_tree_selection(NAMECOLOR_MENU, self.caller, change_name_color, start_text="Name color options:") From cc398f985117f6f8d2f81429b43c77b539444588 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 29 Nov 2017 19:32:50 +0100 Subject: [PATCH 12/12] Remove some spurious spaces --- evennia/contrib/tree_select.py | 140 ++++++++++++++++----------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 5b8f038e33..d2854fc83f 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -28,14 +28,14 @@ on a player: The player will be presented with an EvMenu, like so: ___________________________ - + Make your selection: ___________________________ - - Foo - Bar - Baz - Qux + + Foo + Bar + Baz + Qux Making a selection will pass the selection's key to the specified callback as a string along with the caller, as well as the index of the selection (the line number @@ -62,7 +62,7 @@ For example, let's add some more options to our menu, turning 'Bar' into a categ --When to walk away Baz Qux''' - + Now when we call the menu, we can see that 'Bar' has become a category instead of a selectable option. @@ -71,34 +71,34 @@ selectable option. Make your selection: _______________________________ - Foo - Bar [+] - Baz - Qux - + Foo + Bar [+] + Baz + Qux + Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it. ________________________________________________________________ Bar ________________________________________________________________ - - You've got to know [+] - << Go Back: Return to the previous menu. - + + You've got to know [+] + << Go Back: Return to the previous menu. + Just the one option, which is a category itself, and the option to go back, which will take us back to the previous menu. Let's select 'You've got to know'. ________________________________________________________________ - + You've got to know ________________________________________________________________ - - When to hold em - When to fold em - When to walk away + + When to hold em + When to fold em + When to walk away << Go Back: Return to the previous menu. - + Now we see the three options listed under it, too. We can select one of them or use 'Go Back' to return to the 'Bar' menu we were just at before. It's very simple to make a branching tree of selections! @@ -115,24 +115,24 @@ description to 'Baz' in our menu: --When to walk away Baz: Look at this one: the best option. Qux''' - + Now we see that the Baz option has a description attached that's separate from its key: _______________________________________________________________ Make your selection: _______________________________________________________________ - - Foo - Bar [+] - Baz: Look at this one: the best option. - Qux + + Foo + Bar [+] + Baz: Look at this one: the best option. + Qux Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call your specified callback with the selection, like so: callback(caller, TEST_MENU, 0, "Foo") - + The index of the selection is given along with a string containing the selection's key. That way, if you have two selections in the menu with the same key, you can still differentiate between them. @@ -167,7 +167,7 @@ def init_tree_selection(treestr, caller, callback, start_text="Make your selection:"): """ Prompts a player to select an option from a menu tree given as a multi-line string. - + Args: treestr (str): Multi-lne string representing menu options caller (obj): Player to initialize the menu for @@ -176,33 +176,33 @@ def init_tree_selection(treestr, caller, callback, treestr (str): Menu tree string given above index (int): Index of final selection selection (str): Key of final selection - + Options: index (int or None): Index to start the menu at, or None for top level mark_category (bool): If True, marks categories with a [+] symbol in the menu go_back (bool): If True, present an option to go back to previous categories start_text (str): Text to display at the top level of the menu cmd_on_exit(str): Command to enter when the menu exits - 'look' by default - - + + Notes: This function will initialize an instance of EvMenu with options generated dynamically from the source string, and passes the menu user's selection to a function of your choosing. The EvMenu is made of a single, repeating node, which will call itself over and over at different levels of the menu tree as categories are selected. - + Once a non-category selection is made, the user's selection will be passed to the given callable, both as a string and as an index number. The index is given to ensure every selection has a unique identifier, so that selections with the same key in different categories can be distinguished between. - + The menus called by this function are not persistent and cannot perform complicated tasks like prompt for arbitrary input or jump multiple category levels at once - you'll have to use EvMenu itself if you want to take full advantage of its features. - """ - + """ + # Pass kwargs to store data needed in the menu kwargs = { "index":index, @@ -212,7 +212,7 @@ def init_tree_selection(treestr, caller, callback, "callback":callback, "start_text":start_text } - + # Initialize menu of selections evmenu.EvMenu(caller, "evennia.contrib.tree_select", startnode="menunode_treeselect", startnode_input=None, cmd_on_exit=cmd_on_exit, **kwargs) @@ -221,10 +221,10 @@ def dashcount(entry): """ Counts the number of dashes at the beginning of a string. This is needed to determine the depth of options in categories. - + Args: entry (str): String to count the dashes at the start of - + Returns: dashes (int): Number of dashes at the start """ @@ -240,11 +240,11 @@ def is_category(treestr, index): """ Determines whether an option in a tree string is a category by whether or not there are additional options below it. - + Args: treestr (str): Multi-line string representing menu options index (int): Which line of the string to test - + Returns: is_category (bool): Whether the option is a category """ @@ -262,11 +262,11 @@ def parse_opts(treestr, category_index=None): the menu. If category_index corresponds to a category, returns a list of options under that category. If category_index corresponds to an option that is not a category, it's a selection and returns True. - + Args: treestr (str): Multi-line string representing menu options category_index (int): Index of category or None for top level - + Returns: kept_opts (list or True): Either a list of options in the selected category or True if a selection was made @@ -274,7 +274,7 @@ def parse_opts(treestr, category_index=None): dash_depth = 0 opt_list = treestr.split('\n') kept_opts = [] - + # If a category index is given if category_index != None: # If given index is not a category, it's a selection - return True. @@ -284,7 +284,7 @@ def parse_opts(treestr, category_index=None): dash_depth = dashcount(opt_list[category_index]) + 1 # Delete everything before the category index opt_list = opt_list [category_index+1:] - + # Keep every option (referenced by index) at the appropriate depth cur_index = 0 for option in opt_list: @@ -298,20 +298,20 @@ def parse_opts(treestr, category_index=None): return kept_opts cur_index += 1 return kept_opts - + def index_to_selection(treestr, index, desc=False): """ Given a menu tree string and an index, returns the corresponding selection's name as a string. If 'desc' is set to True, will return the selection's description as a string instead. - + Args: treestr (str): Multi-line string representing menu options index (int): Index to convert to selection key or description - + Options: desc (bool): If true, returns description instead of key - + Returns: selection (str): Selection key or description if 'desc' is set """ @@ -332,16 +332,16 @@ def index_to_selection(treestr, index, desc=False): return selection[0] else: return selection[1] - + def go_up_one_category(treestr, index): """ Given a menu tree string and an index, returns the category that the given option belongs to. Used for the 'go back' option. - + Args: treestr (str): Multi-line string representing menu options index (int): Index to determine the parent category of - + Returns: parent_category (int): Index of parent category """ @@ -350,7 +350,7 @@ def go_up_one_category(treestr, index): dash_level = dashcount(opt_list[index]) # Delete everything after the current index opt_list = opt_list[:index+1] - + # If there's no dash, return 'None' to return to base menu if dash_level == 0: @@ -361,25 +361,25 @@ def go_up_one_category(treestr, index): if dashcount(selection) == dash_level - 1: return current_index current_index -= 1 - + def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): """ Takes a list of options processed by parse_opts and turns it into a list/dictionary of menu options for use in menunode_treeselect. - + Args: treestr (str): Multi-line string representing menu options optlist (list): List of options to convert to EvMenu's option format index (int): Index of current category mark_category (bool): Whether or not to mark categories with [+] go_back (bool): Whether or not to add an option to go back in the menu - + Returns: menuoptions (list of dicts): List of menu options formatted for use in EvMenu, each passing a different "newindex" kwarg that changes the menu level or makes a selection """ - + menuoptions = [] cur_index = 0 for option in optlist: @@ -410,12 +410,12 @@ def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): def menunode_treeselect(caller, raw_string, **kwargs): """ This is the repeating menu node that handles the tree selection. - """ - + """ + # If 'newindex' is in the kwargs, change the stored index. if "newindex" in kwargs: caller.ndb._menutree.index = kwargs["newindex"] - + # Retrieve menu info index = caller.ndb._menutree.index mark_category = caller.ndb._menutree.mark_category @@ -423,10 +423,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): treestr = caller.ndb._menutree.treestr callback = caller.ndb._menutree.callback start_text = caller.ndb._menutree.start_text - + # List of options if index is 'None' or category, or 'True' if a selection optlist = parse_opts(treestr, category_index=index) - + # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. if optlist == True: selection = index_to_selection(treestr, index) @@ -434,10 +434,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): callback(caller, treestr, index, selection) except Exception: log_trace("Error in tree selection callback.") - + # Returning None, None ends the menu. return None, None - + # Otherwise, convert optlist to a list of menu options. else: options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back) @@ -485,7 +485,7 @@ NAMECOLOR_MENU = """Set name color: Choose a color for your name! --Lavender: |535Set your name to Lavender|n --Fuchsia: |503Set your name to Fuchsia|n Remove name color: Remove your name color, if any""" - + class CmdNameColor(Command): """ Set or remove a special color on your name. Just an example for the @@ -503,7 +503,7 @@ class CmdNameColor(Command): def change_name_color(caller, treestr, index, selection): """ Changes a player's name color. - + Args: caller (obj): Character whose name to color. treestr (str): String for the color change menu - unused @@ -511,11 +511,11 @@ def change_name_color(caller, treestr, index, selection): selection (str): Selection made from the name color menu - used to determine the color the player chose. """ - + # Store the caller's uncolored name if not caller.db.uncolored_name: caller.db.uncolored_name = caller.key - + # Dictionary matching color selection names to color codes colordict = { "Red":"|511", "Pink":"|533", "Maroon":"|301", "Orange":"|531", "Brown":"|321", "Sienna":"|420", @@ -523,7 +523,7 @@ def change_name_color(caller, treestr, index, selection): "Green":"|141", "Lime":"|350", "Forest":"|032", "Blue":"|115", "Cyan":"|155", "Navy":"|113", "Purple":"|415", "Lavender":"|535", "Fuchsia":"|503"} - + # I know this probably isn't the best way to do this. It's just an example! if selection == "Remove name color": # Player chose to remove their name color caller.key = caller.db.uncolored_name