diff --git a/CHANGELOG.md b/CHANGELOG.md index d54222cd26..91b1d5b3da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,9 +87,10 @@ ### Contribs +- `Build Menu` (vincent-lg): New @edit command to edit object properties in a menu. +- `Field Fill` (Tim Ashley Jenkins): Wraps EvMenu for creating submittable forms. - `Health Bar` (Tim Ashley Jenkins): Easily create colorful bars/meters. -- `Tree select` (Fluttersprite): Wrapper around EvMenu to easier create - a common form of menu from a string. +- `Tree select` (Fluttersprite): Wrap EvMenu to create a common type of menu from a string. - `Turnbattle suite` (Tim Ashley Jenkins)- the old `turnbattle.py` was moved into its own `turnbattle/` package and reworked with many different flavors of combat systems: - `tb_basic` - The basic turnbattle system, with initiative/turn order attack/defense/damage. diff --git a/evennia/contrib/fieldfill.py b/evennia/contrib/fieldfill.py index 2595f4eaf9..b20f64cd66 100644 --- a/evennia/contrib/fieldfill.py +++ b/evennia/contrib/fieldfill.py @@ -12,8 +12,8 @@ is submitted, the form's data is submitted as a dictionary to any callable of your choice. The function that initializes the fillable form menu is fairly simple, and -includes the caller, the template for the form, and the callback which the form -data will be sent to upon submission: +includes the caller, the template for the form, and the callback(caller, result) to which the form +data will be sent to upon submission. init_fill_field(formtemplate, caller, formcallback) @@ -27,27 +27,27 @@ a brief character profile: {"fieldname":"Age", "fieldtype":"number"}, {"fieldname":"History", "fieldtype":"text"}, ] - + This will present the player with an EvMenu showing this basic form: - Name: - Age: - History: + Name: + Age: + History: While in this menu, the player can assign a new value to any field with the syntax = , like so: > name = Ashley Field 'Name' set to: Ashley - + Typing 'look' by itself will show the form and its current values. > look - Name: Ashley - Age: - History: - + Name: Ashley + Age: + History: + Number fields require an integer input, and will reject any text that can't be converted into an integer. @@ -55,18 +55,18 @@ be converted into an integer. Field 'Age' requires a number. > age = 31 Field 'Age' set to: 31 - + Form data is presented as an EvTable, so text of any length will wrap cleanly. > history = EVERY MORNING I WAKE UP AND OPEN PALM SLAM[...] Field 'History' set to: EVERY MORNING I WAKE UP AND[...] > look - Name: Ashley - Age: 31 - History: EVERY MORNING I WAKE UP AND OPEN PALM SLAM A VHS INTO THE SLOT. - IT'S CHRONICLES OF RIDDICK AND RIGHT THEN AND THERE I START DOING - THE MOVES ALONGSIDE WITH THE MAIN CHARACTER, RIDDICK. I DO EVERY + Name: Ashley + Age: 31 + History: EVERY MORNING I WAKE UP AND OPEN PALM SLAM A VHS INTO THE SLOT. + IT'S CHRONICLES OF RIDDICK AND RIGHT THEN AND THERE I START DOING + THE MOVES ALONGSIDE WITH THE MAIN CHARACTER, RIDDICK. I DO EVERY MOVE AND I DO EVERY MOVE HARD. When the player types 'submit' (or your specified submit command), the menu @@ -88,7 +88,7 @@ do this by specifying "min" and "max" values in your field's dictionary: {"fieldname":"Age", "fieldtype":"number", "min":18, "max":100}, {"fieldname":"History", "fieldtype":"text"} ] - + Now if the player tries to enter a value out of range, the form will not acept the given value. @@ -96,10 +96,10 @@ given value. Field 'Age' reqiures a minimum value of 18. > age = 900 Field 'Age' has a maximum value of 100. - + Setting 'min' and 'max' for a text field will instead act as a minimum or maximum character length for the player's input. - + There are lots of ways to present the form to the player - fields can have default values or show a custom message in place of a blank value, and player input can be verified by a custom function, allowing for a great deal of flexibility. There @@ -139,15 +139,16 @@ Optional: the field to False or True. """ -from evennia.utils import evmenu, evtable, delay, list_to_string +from evennia.utils import evmenu, evtable, delay, list_to_string, logger from evennia import Command from evennia.server.sessionhandler import SESSIONS + class FieldEvMenu(evmenu.EvMenu): """ Custom EvMenu type with its own node formatter - removes extraneous lines """ - + def node_formatter(self, nodetext, optionstext): """ Formats the entirety of the node. @@ -163,7 +164,7 @@ class FieldEvMenu(evmenu.EvMenu): """ # Only return node text, no options or separators return nodetext - + def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="", submitcmd="submit", borderstyle="cells", formhelptext=None, @@ -172,12 +173,12 @@ def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="", Initializes a menu presenting a player with a fillable form - once the form is submitted, the data will be passed as a dictionary to your chosen function. - + Args: formtemplate (list of dicts): The template for the form's fields. caller (obj): Player who will be filling out the form. formcallback (callable): Function to pass the completed form's data to. - + Options: pretext (str): Text to put before the form in the menu. posttext (str): Text to put after the form in the menu. @@ -191,38 +192,39 @@ def init_fill_field(formtemplate, caller, formcallback, pretext="", posttext="", similar application, you may want to generate the initial form data dynamically before calling init_fill_field. """ - + # Initialize form data from the template if none provided formdata = form_template_to_dict(formtemplate) if initial_formdata: formdata = initial_formdata - + # Provide default help text if none given - if formhelptext == None: - formhelptext = ("Available commands:|/" - "|w = :|n Set given field to new value, replacing the old value|/" - "|wclear :|n Clear the value in the given field, making it blank|/" - "|wlook|n: Show the form's current values|/" - "|whelp|n: Display this help screen|/" - "|wquit|n: Quit the form menu without submitting|/" - "|w%s|n: Submit this form and quit the menu" % submitcmd) - + if formhelptext is None: + formhelptext = ( + "Available commands:|/" + "|w = :|n Set given field to new value, replacing the old value|/" + "|wclear :|n Clear the value in the given field, making it blank|/" + "|wlook|n: Show the form's current values|/" + "|whelp|n: Display this help screen|/" + "|wquit|n: Quit the form menu without submitting|/" + "|w%s|n: Submit this form and quit the menu" % submitcmd) + # Pass kwargs to store data needed in the menu kwargs = { - "formdata":formdata, - "formtemplate": formtemplate, - "formcallback": formcallback, - "pretext": pretext, - "posttext": posttext, - "submitcmd": submitcmd, - "borderstyle": borderstyle, - "formhelptext": formhelptext + "formdata": formdata, + "formtemplate": formtemplate, + "formcallback": formcallback, + "pretext": pretext, + "posttext": posttext, + "submitcmd": submitcmd, + "borderstyle": borderstyle, + "formhelptext": formhelptext } - + # Initialize menu of selections FieldEvMenu(caller, "evennia.contrib.fieldfill", startnode="menunode_fieldfill", auto_look=False, persistent=persistent, **kwargs) - + def menunode_fieldfill(caller, raw_string, **kwargs): """ @@ -230,7 +232,7 @@ def menunode_fieldfill(caller, raw_string, **kwargs): allow a player to enter values into a fillable form. When the form is submitted, the form data is passed to a callback as a dictionary. """ - + # Retrieve menu info - taken from ndb if not persistent or db if persistent if not caller.db._menutree: formdata = caller.ndb._menutree.formdata @@ -250,16 +252,16 @@ def menunode_fieldfill(caller, raw_string, **kwargs): submitcmd = caller.db._menutree.submitcmd borderstyle = caller.db._menutree.borderstyle formhelptext = caller.db._menutree.formhelptext - + # Syntax error syntax_err = "Syntax: = |/Or: clear , help, look, quit|/'%s' to submit form" % submitcmd - + # Display current form data text = (display_formdata(formtemplate, formdata, pretext=pretext, posttext=posttext, borderstyle=borderstyle), formhelptext) options = ({"key": "_default", - "goto":"menunode_fieldfill"}) - + "goto": "menunode_fieldfill"}) + if raw_string: # Test for given 'submit' command if raw_string.lower().strip() == submitcmd: @@ -276,18 +278,18 @@ def menunode_fieldfill(caller, raw_string, **kwargs): caller.msg("The following blank fields require a value: %s" % list_to_string(blank_and_required)) text = (None, formhelptext) return text, options - + # If everything checks out, pass form data to the callback and end the menu! try: formcallback(caller, formdata) except Exception: - log_trace("Error in fillable form callback.") + logger.log_trace("Error in fillable form callback.") return None, None - + # Test for 'look' command if raw_string.lower().strip() == "look" or raw_string.lower().strip() == "l": return text, options - + # Test for 'clear' command cleartest = raw_string.lower().strip().split(" ", 1) if cleartest[0].lower() == "clear": @@ -296,16 +298,16 @@ def menunode_fieldfill(caller, raw_string, **kwargs): caller.msg(syntax_err) return text, options matched_field = None - + for key in formdata.keys(): if cleartest[1].lower() in key.lower(): matched_field = key - + if not matched_field: caller.msg("Field '%s' does not exist!" % cleartest[1]) text = (None, formhelptext) return text, options - + # Test to see if field can be cleared for field in formtemplate: if field["fieldname"] == matched_field and "cantclear" in field.keys(): @@ -313,13 +315,13 @@ def menunode_fieldfill(caller, raw_string, **kwargs): caller.msg("Field '%s' can't be cleared!" % matched_field) text = (None, formhelptext) return text, options - + # Clear the field - formdata.update({matched_field:None}) + formdata.update({matched_field: None}) caller.ndb._menutree.formdata = formdata caller.msg("Field '%s' cleared." % matched_field) return text, options - + if "=" not in raw_string: text = (None, formhelptext) caller.msg(syntax_err) @@ -329,25 +331,25 @@ def menunode_fieldfill(caller, raw_string, **kwargs): entry = raw_string.split("=", 1) fieldname = entry[0].strip() newvalue = entry[1].strip() - + # Syntax error if field name is too short or blank if len(fieldname) < 1: caller.msg(syntax_err) text = (None, formhelptext) return text, options - + # Attempt to match field name to field in form data matched_field = None for key in formdata.keys(): if fieldname.lower() in key.lower(): matched_field = key - + # No matched field - if matched_field == None: + if matched_field is None: caller.msg("Field '%s' does not exist!" % fieldname) text = (None, formhelptext) return text, options - + # Set new field value if match # Get data from template fieldtype = None @@ -369,21 +371,21 @@ def menunode_fieldfill(caller, raw_string, **kwargs): falsestr = field["falsestr"] if "verifyfunc" in field.keys(): verifyfunc = field["verifyfunc"] - + # Field type text verification if fieldtype == "text": # Test for max/min - if max_value != None: + if max_value is not None: if len(newvalue) > max_value: caller.msg("Field '%s' has a maximum length of %i characters." % (matched_field, max_value)) text = (None, formhelptext) return text, options - if min_value != None: + if min_value is not None: if len(newvalue) < min_value: caller.msg("Field '%s' reqiures a minimum length of %i characters." % (matched_field, min_value)) text = (None, formhelptext) return text, options - + # Field type number verification if fieldtype == "number": try: @@ -393,17 +395,17 @@ def menunode_fieldfill(caller, raw_string, **kwargs): text = (None, formhelptext) return text, options # Test for max/min - if max_value != None: + if max_value is not None: if newvalue > max_value: caller.msg("Field '%s' has a maximum value of %i." % (matched_field, max_value)) text = (None, formhelptext) return text, options - if min_value != None: + if min_value is not None: if newvalue < min_value: caller.msg("Field '%s' reqiures a minimum value of %i." % (matched_field, min_value)) text = (None, formhelptext) return text, options - + # Field type bool verification if fieldtype == "bool": if newvalue.lower() != truestr.lower() and newvalue.lower() != falsestr.lower(): @@ -414,7 +416,7 @@ def menunode_fieldfill(caller, raw_string, **kwargs): newvalue = True elif newvalue.lower() == falsestr.lower(): newvalue = False - + # Call verify function if present if verifyfunc: if verifyfunc(caller, newvalue) is False: @@ -429,65 +431,67 @@ def menunode_fieldfill(caller, raw_string, **kwargs): newvalue = False elif newvalue == 1: newvalue = True - + # If everything checks out, update form!! - formdata.update({matched_field:newvalue}) + formdata.update({matched_field: newvalue}) caller.ndb._menutree.formdata = formdata - + # Account for truestr and falsestr when updating a boolean form announced_newvalue = newvalue if newvalue is True: announced_newvalue = truestr elif newvalue is False: announced_newvalue = falsestr - + # Announce the new value to the player caller.msg("Field '%s' set to: %s" % (matched_field, str(announced_newvalue))) text = (None, formhelptext) - + return text, options + def form_template_to_dict(formtemplate): """ Initializes a dictionary of form data from the given list-of-dictionaries form template, as formatted above. - + Args: formtemplate (list of dicts): Tempate for the form to be initialized. - + Returns: formdata (dict): Dictionary of initalized form data. """ formdata = {} - + for field in formtemplate: # Value is blank by default fieldvalue = None if "default" in field: # Add in default value if present fieldvalue = field["default"] - formdata.update({field["fieldname"]:fieldvalue}) - + formdata.update({field["fieldname"]: fieldvalue}) + return formdata - + + def display_formdata(formtemplate, formdata, pretext="", posttext="", borderstyle="cells"): """ Displays a form's current data as a table. Used in the form menu. - + Args: formtemplate (list of dicts): Template for the form formdata (dict): Form's current data - + Options: pretext (str): Text to put before the form table. posttext (str): Text to put after the form table. borderstyle (str): EvTable's border style. """ - + formtable = evtable.EvTable(border=borderstyle, valign="t", maxwidth=80) field_name_width = 5 - + for field in formtemplate: new_fieldname = None new_fieldvalue = None @@ -496,12 +500,12 @@ def display_formdata(formtemplate, formdata, if len(field["fieldname"]) + 5 > field_name_width: field_name_width = len(field["fieldname"]) + 5 # Get field value - if formdata[field["fieldname"]] != None: + if formdata[field["fieldname"]] is not None: new_fieldvalue = str(formdata[field["fieldname"]]) # Use blank message if field is blank and once is present - if new_fieldvalue == None and "blankmsg" in field: + if new_fieldvalue is None and "blankmsg" in field: new_fieldvalue = "|x" + str(field["blankmsg"]) + "|n" - elif new_fieldvalue == None: + elif new_fieldvalue is None: new_fieldvalue = " " # Replace True and False values with truestr and falsestr from template if formdata[field["fieldname"]] is True and "truestr" in field: @@ -510,24 +514,24 @@ def display_formdata(formtemplate, formdata, new_fieldvalue = field["falsestr"] # Add name and value to table formtable.add_row(new_fieldname, new_fieldvalue) - + formtable.reformat_column(0, align="r", width=field_name_width) - + return pretext + "|/" + str(formtable) + "|/" + posttext -""" -EXAMPLE FUNCTIONS / COMMAND STARTS HERE -""" - + +# EXAMPLE FUNCTIONS / COMMAND STARTS HERE + + def verify_online_player(caller, value): """ Example 'verify function' that matches player input to an online character or else rejects their input as invalid. - + Args: caller (obj): Player entering the form data. value (str): String player entered into the form, to be verified. - + Returns: matched_character (obj or False): dbref to a currently logged in character object - reference to the object will be stored in @@ -538,7 +542,7 @@ def verify_online_player(caller, value): session_list = SESSIONS.get_sessions() char_list = [] matched_character = None - + # Get a list of online characters for session in session_list: if not session.logged_in: @@ -551,56 +555,57 @@ def verify_online_player(caller, value): for character in char_list: if value.lower() == character.key.lower(): matched_character = character - + # If input didn't match to a character if not matched_character: # Send the player an error message unique to this function caller.msg("No character matching '%s' is online." % value) # Returning False indicates the new value is not valid return False - - # Returning anything besides True or False will replace the player's input with the returned value - # In this case, the value becomes a reference to the character object - # You can store data besides strings and integers in the 'formdata' dictionary this way! + + # Returning anything besides True or False will replace the player's input with the returned + # value. In this case, the value becomes a reference to the character object. You can store data + # besides strings and integers in the 'formdata' dictionary this way! return matched_character # Form template for the example 'delayed message' form SAMPLE_FORM = [ -{"fieldname":"Character", - "fieldtype":"text", - "max":30, - "blankmsg":"(Name of an online player)", - "required":True, - "verifyfunc":verify_online_player - }, -{"fieldname":"Delay", - "fieldtype":"number", - "min":3, - "max":30, - "default":10, - "cantclear":True - }, -{"fieldname":"Message", - "fieldtype":"text", - "min":3, - "max":200, - "blankmsg":"(Message up to 200 characters)" - }, -{"fieldname":"Anonymous", - "fieldtype":"bool", - "truestr":"Yes", - "falsestr":"No", - "default":False - } -] - + {"fieldname": "Character", + "fieldtype": "text", + "max": 30, + "blankmsg": "(Name of an online player)", + "required": True, + "verifyfunc": verify_online_player + }, + {"fieldname": "Delay", + "fieldtype": "number", + "min": 3, + "max": 30, + "default": 10, + "cantclear": True + }, + {"fieldname": "Message", + "fieldtype": "text", + "min": 3, + "max": 200, + "blankmsg": "(Message up to 200 characters)" + }, + {"fieldname": "Anonymous", + "fieldtype": "bool", + "truestr": "Yes", + "falsestr": "No", + "default": False + } + ] + + class CmdTestMenu(Command): """ This test command will initialize a menu that presents you with a form. You can fill out the fields of this form in any order, and then type in 'send' to send a message to another online player, which will reach them after a delay you specify. - + Usage: = clear @@ -618,28 +623,30 @@ class CmdTestMenu(Command): """ pretext = "|cSend a delayed message to another player ---------------------------------------|n" posttext = ("|c--------------------------------------------------------------------------------|n|/" - "Syntax: type |c = |n to change the values of the form. Given|/" - "player must be currently logged in, delay is given in seconds. When you are|/" - "finished, type '|csend|n' to send the message.|/") + "Syntax: type |c = |n to change the values of the form. Given|/" + "player must be currently logged in, delay is given in seconds. When you are|/" + "finished, type '|csend|n' to send the message.|/") init_fill_field(SAMPLE_FORM, self.caller, init_delayed_message, pretext=pretext, posttext=posttext, submitcmd="send", borderstyle="none") + def sendmessage(obj, text): """ Callback to send a message to a player. - + Args: obj (obj): Player to message. text (str): Message. """ - obj.msg(text) + obj.msg(text) + def init_delayed_message(caller, formdata): """ Initializes a delayed message, using data from the example form. - + Args: caller (obj): Character submitting the message. formdata (dict): Data from submitted form. @@ -653,9 +660,8 @@ def init_delayed_message(caller, formdata): if formdata["Anonymous"] is True: sender = "anonymous" message = ("Message from %s: " % sender) + str(formdata["Message"]) - + caller.msg("Message sent to %s!" % player_to_message) # Make a deferred call to 'sendmessage' above. - deferred = delay(message_delay, sendmessage, player_to_message, message) + delay(message_delay, sendmessage, player_to_message, message) return -