From cb2cb94082b5f8d0e3d9bd03ecd2656680a65f3f Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Fri, 19 Aug 2022 17:30:50 -0600 Subject: [PATCH 1/9] init --- .../base_systems/character_creator/README.md | 87 ++++ .../character_creator/character_creator.py | 205 ++++++++ .../character_creator/example_menu.py | 446 ++++++++++++++++++ .../base_systems/character_creator/tests.py | 32 ++ 4 files changed, 770 insertions(+) create mode 100644 evennia/contrib/base_systems/character_creator/README.md create mode 100644 evennia/contrib/base_systems/character_creator/character_creator.py create mode 100644 evennia/contrib/base_systems/character_creator/example_menu.py create mode 100644 evennia/contrib/base_systems/character_creator/tests.py diff --git a/evennia/contrib/base_systems/character_creator/README.md b/evennia/contrib/base_systems/character_creator/README.md new file mode 100644 index 0000000000..f45f957b82 --- /dev/null +++ b/evennia/contrib/base_systems/character_creator/README.md @@ -0,0 +1,87 @@ +# Character Creator contrib +by InspectorCaracal + +This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, where characters are not automatically created to match the account. + +## Installation + +In your game folder `commands/default_cmdsets.py`, import and add `ContribCmdCharCreate` to your `AccountCmdSet`. + +Example: +```py +from evennia.contrib.base_systems.character_creator.character_creator import ContribCmdCharCreate + +class AccountCmdSet(default_cmds.AccountCmdSet): + + def at_cmdset_creation(self): + super().at_cmdset_creation() + self.add(ContribCmdCharCreate) +``` + +In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount` on your Account class. + +(Alternatively, you can copy the `at_look` method directly into your own class.) + +Example: +```py +from evennia.contrib.base_systems.character_creator.character_creator import ContribChargenAccount + +class Account(ContribChargenAccount): + # your Account class code +``` + +Lastly, in your `settings.py` file, define `CHARGEN_MENU` to your character creation menu module's location. + +Example: +```py +CHARGEN_MENU = "evennia.contrib.base_systems.character_creator.example_menu" +``` + +## Usage + +### The EvMenu + +In order to use the contrib, you will need to create your own chargen EvMenu. The included `example_menu.py` gives a number of useful menu node techniques with basic attribute examples for you to reference. It can be run as-is as a tutorial for yourself/your devs, or used as base for your own menu. + +The example menu includes code, tips, and instructions for the following types of decision nodes: + +#### Informational Pages + +A small set of nodes that let you page through information on different choices before committing to one. + +#### Option Categories + +A pair of nodes which let you divide an arbitrary number of options into separate categories. + +The base node has a list of categories as the options, and the child node displays the actual character choices. + +#### Multiple Choice + +Allows players to select and deselect options from the list in order to choose more than one. + +#### Starting Objects + +Allows players to choose from a selection of starting objects, which are then created on chargen completion. + +#### Choosing a Name + +The contrib assumes the player will choose their name during character creation, so the necessary code for doing so is of course included! + + +### `charcreate` command + +The contrib overrides the character creation command - `charcreate` - to use a character creator menu, as well as supporting exiting/resuming the process. In addition, unlike the core command, it's designed for the character name to be chosen later on via the menu, so it won't parse any arguments passed to it. + +### Changes to `Account.at_look` + +The contrib version works mostly the same as core evennia, but adds an additional check to recognize an in-progress character. If you've modified your own `at_look` hook, it's an easy addition to make: just add this section to the playable character list loop. +```py + for char in characters: + # contrib code starts here + if char.db.chargen_step: + # currently in-progress character; don't display placeholder names + result.append("\n - |Yin progress|n (|wcharcreate|n to continue)") + continue + # the rest of your code continues here +``` + diff --git a/evennia/contrib/base_systems/character_creator/character_creator.py b/evennia/contrib/base_systems/character_creator/character_creator.py new file mode 100644 index 0000000000..2749d5dfe7 --- /dev/null +++ b/evennia/contrib/base_systems/character_creator/character_creator.py @@ -0,0 +1,205 @@ +""" +Character Creator contrib, by InspectorCaracal + +This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, +where characters are not automatically created to match the account. + +# Features + +The primary feature of this contrib is defining the name and attributes +of a new character through an EvMenu. It provides an alternate `charcreate` +command as well as a modified `at_look` method for your Account class. + +# Usage + +In order to use the contrib, you will need to create your own chargen +EvMenu. The included `example_menu.py` gives a number of useful techniques +and examples, including how to allow players to choose and confirm +character names from within the menu. + +""" +from random import choices +import string + +from django.conf import settings +from evennia.commands.default.muxcommand import MuxAccountCommand +from evennia.objects.models import ObjectDB +from evennia.utils import create, search +from evennia.utils.evmenu import EvMenu + + +class ContribCmdCharCreate(MuxAccountCommand): + """ + create a new character + + Begin creating a new character, or resume character creation for + an existing in-progress character. + + You can stop character creation at any time and resume where + you left off later. + """ + key = "charcreate" + locks = "cmd:pperm(Player) and is_ooc()" + help_category = "General" + + def func(self): + "create the new character" + account = self.account + session = self.session + + # only one character should be in progress at a time, so we check for WIPs first + in_progress = [chara for chara in account.playable if chara.db.chargen_step] + + if len(in_progress): + # we're continuing chargen for a WIP character + new_character = in_progress[0] + else: + # we're making a new character + charmax = settings.MAX_NR_CHARACTERS if settings.MULTISESSION_MODE > 1 else 1 + + if not account.is_superuser and ( + account.db._playable_characters and len(account.db._playable_characters) >= charmax + ): + plural = "" if charmax == 1 else "s" + self.msg(f"You may only create a maximum of {charmax} character{plural}.") + return + + # create the new character object, with default settings + start_location = ObjectDB.objects.get_id(settings.START_LOCATION) + default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) + permissions = settings.PERMISSION_ACCOUNT_DEFAULT + # generate a randomized key so the player can choose a character name later + key = ''.join(choices(string.ascii_letters + string.digits, k=10) + new_character = create.create_object(Character, key=key, + location=None, + home=home, + permissions=permissions) + # only allow creator (and developers) to puppet this char + new_character.locks.add( + f"puppet:pid({account.id}) or perm(Developer) or pperm(Developer);delete:id({account.id}) or perm(Admin)" + ) + # initalize the new character to the beginning of the chargen menu + new_character.db.chargen_step = "menunode_welcome" + account.db._playable_characters.append(new_character) + + # set the menu node to start at to the character's last saved step + startnode = new_character.db.chargen_step + # attach the character to the session, so the chargen menu can access it + session.new_char = new_character + + # this gets called every time the player exits the chargen menu + def finish_char_callback(session, menu): + char = session.new_char + if not char.db.chargen_step: + # this means character creation was completed - start playing! + # any additional first-time-playing code can go here as well + # e.g. creating generic default gear, + # changing the pre-logout location of the character so they log in to a specific spot, + # notifying admins of a new player, etc. + + # execute the ic command as the session to start puppeting the character + session.execute_cmd("ic {}".format(char.key)) + + EvMenu(session, + settings.CHARGEN_MENU, + startnode=startnode, + cmd_on_exit=finish_char_callback) + + +from django.conf import settings +from evennia import DefaultAccount + + +class ContribChargenAccount(DefaultAccount): + """ + A modified Account class that makes minor changes to the OOC look + output, to incorporate in-progress characters. + """ + + def at_look(self, target=None, session=None, **kwargs): + """ + Called by the OOC look command. It displays a list of playable + characters and should be mostly identical to the core method. + + Args: + target (Object or list, optional): An object or a list + objects to inspect. + session (Session, optional): The session doing this look. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + look_string (str): A prepared look string, ready to send + off to any recipient (usually to ourselves) + """ + + # list of targets - make list to disconnect from db + characters = list(tar for tar in target if tar) if target else [] + sessions = self.sessions.all() + is_su = self.is_superuser + + # text shown when looking in the ooc area + result = [f"Account |g{self.key}|n (you are Out-of-Character)"] + + nsess = len(sessions) + if nsess == 1: + result.append("\n\n|wConnected session:|n") + elif nsess > 1: + result.append(f"\n\n|wConnected sessions ({nsess}):|n") + for isess, sess in enumerate(sessions): + csessid = sess.sessid + addr = "{protocol} ({address})".format( + protocol=sess.protocol_key, + address=isinstance(sess.address, tuple) and str(sess.address[0]) or str(sess.address), + ) + if session.sessid == csessid: + result.append(f"\n |w* {isess+1}|n {addr}") + else + result.append(f"\n {isess+1} {addr}") + + result.append("\n\n |whelp|n - more commands") + result.append("\n |wpublic |n - talk on public channel") + + charmax = settings.MAX_NR_CHARACTERS + + if is_su or len(characters) < charmax: + result.append("\n |wcharcreate|n - create a new character") + + if characters: + result.append( + "\n |wdchardelete |n - delete a character (cannot be undone!)" + ) + plural = "" if len(characters) == 1 else "s" + result.append("\n |wic |n - enter the game (|wooc|n to return here)") + if is_su: + result.append( + f"\n\nAvailable character{plural} ({len(characters)}/unlimited):" + ) + else: + result.append( + f"\n\nAvailable character{plural} ({len(characters)}/{charmax}):" + ) + + for char in characters: + if char.db.chargen_step: + # currently in-progress character; don't display placeholder names + result.append("\n - |Yin progress|n (|wcharcreate|n to continue)") + continue + csessions = char.sessions.all() + if csessions: + for sess in csessions: + # character is already puppeted + sid = sess in sessions and sessions.index(sess) + 1 + if sess and sid: + result.append( + f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})" + ) + else: + result.append( + f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone else)" + ) + else: + # character is available + result.append(f"\n - {char.key} [{', '.join(char.permissions.all())}]") + look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68) + return look_string diff --git a/evennia/contrib/base_systems/character_creator/example_menu.py b/evennia/contrib/base_systems/character_creator/example_menu.py new file mode 100644 index 0000000000..b4d5c6b5af --- /dev/null +++ b/evennia/contrib/base_systems/character_creator/example_menu.py @@ -0,0 +1,446 @@ +""" +An example EvMenu for the character creator contrib. + +This menu is not intended to be a full character creator, but to demonstrate +several different useful node types for your own creator. Any of the different +techniques demonstrated here can be combined into a single decision point. + +## Informational Pages + +A set of nodes that let you page through information on different choices. + +The example shows how to have a single informational page for each option, but +you can expand that into sub-categories by setting the values to dictionaries +instead of simple strings and referencing the "Option Categories" nodes. + +## Option Categories + +A pair of nodes which let you divide your options into separate categories. +The base node has a list of categories as the options, which is passed to the +child node. That child node then presents the options for that category and +allows the player to choose one. + +## Multiple Choice + +Allows players to select and deselect options from the list in order to choose +more than one. The example has a requirement of choosing exactly 3 options, +but you can change it to a maximum or minimum number of required options - +or remove the requirement check entirely. + +## Starting Objects + +Allows players to choose from a selection of starting objects. + +When creating starting objects e.g. gear, it's best to actually create them +at the end, so you don't have to patch checks in for orphaned objects or +infinite-object player exploits. + +## Choosing a Name + +The contrib character create command assumes the player will choose their name +during character creation. This section validates name choices before confirming +them. + +## The End + +It might not seem like an important part, since the players don't make a decision +here, but the final node of character creation is where you finalize all of +the decisions players made earlier. Initializing skills, creating starting gear, +and other one-time method calls and set-up should be put here. +""" + +from evennia import create_object +from evennia.utils import dedent +from evennia.utils.evtable import EvTable +from evennia.prototypes.spawner import spawn + +import inflect +_INFLECT = inflect.engine() + +from typeclasses.characters import Character + +######################################################### +# Welcome Page +######################################################### + +def menunode_welcome(caller): + """Starting page.""" + text = dedent("""\ + |wWelcome to Character Creation!|n + + This is the starting node for all brand new characters. It's a good place to + remind players that they can exit the character creator and resume later, + especially if you're going to have a really long chargen process. + + A brief overview of the game could be a good idea here, too, or a link to your + game wiki if you have one. + """) + help = "You can explain the commands for exiting and resuming more specifically here." + options = [{"desc": "Let's begin!", "goto": "menunode_info_base"}] + return (text, help), options + + +######################################################### +# Informational Pages +######################################################### + +# Storing your information in a dictionary like this makes the menu nodes much cleaner, +# as well as making info easier to update. You can even import it from a different module, +# e.g. wherever you have the classes actually defined, so later updates only happen in one place. +_CLASS_INFO_DICT = { + # The keys here are the different options you can choose, and the values are the info pages + "warrior": dedent("""\ + Most warriors specialize in melee weapons, although ranged combat + is not unheard of. + + Warriors like to compete by beating each other up for fun. + """), + }, + "mage": dedent("""\ + Mages prefer less combative lines of work, such as showmanship or + selling enchanted charms. Those who choose to be a battle mage are + highly sought after by adventuring parties. + + Mage schools, being led by the most academic-minded of mages, are + notorious for intellectual snobbery. + """), + }, +} + +def menunode_info_base(caller): + """Base node for the informational choices.""" + # this is a base node for a decision, so we want to save the character's progress here + caller.new_char.db.chargen_step = "menunode_info_base" + + text = dedent("""\ + |wInformational Pages|n + + Sometimes you'll want to let players read more about options before choosing + one of them. This is especially useful for big choices like race or class. + """) + help = "A link to your wiki for more information on classes could be useful here." + options = [] + # Build your options from your info dict so you don't need to update this to add new options + for pclass in _CLASS_INFO_DICT.keys(): + options.append({"desc": f"Learn about the |c{pclass}|n class", "goto": ("menunode_info_class", { "selected_class": pclass })}) + return (text, help), options + +# putting your kwarg in the menu declaration helps keep track of what variables the node needs +def menunode_info_class(caller, raw_string, selected_class=None, **kwargs): + """Informational overview of a particular class""" + + # sometimes weird bugs happen - it's best to check for them rather than let the game break + if not selected_class: + # reset back to the previous step + caller.new_char.db.chargen_step = "menunode_welcome" + # print error to player and quit the menu + return "Something went wrong. Please try again." + + # Since you have all the info in a nice dict, you can just grab it to display here + text = _CLASS_INFO_DICT[pclass] + help = "If you want option-specific help, you can define it in your info dict and reference it." + options = [] + + # set an option for players to choose this class + options.append({"desc": f"Become {_INFLECT.an(selected_class)}", "goto": (_set_class, { "selected_class": selected_class })}) + + # once again build your options from the same info dict + for pclass in _CLASS_INFO_DICT.keys(): + # make sure you don't print the currently displayed page as an option + if pclass != selected_class: + options.append({"desc": f"Learn about the |c{pclass}|n class", "goto": ("menunode_info_class", { "selected_class": pclass })}) + return (text, help), options + + +def _set_class(caller, raw_string, selected_class=None, **kwargs): + """Set the character's species and define their available features.""" + # a class should always be selected here + if not selected_class: + # go back to the base node for this decision + return "menunode_info_base" + + char = caller.new_char + # any special code for setting this option would go here! + # but we'll just set an attribute + char.db.player_class = selected_class + + # move on to the next step! + return "menunode_categories" + + +######################################################### +# Option Categories +######################################################### + +# for these subcategory options, we make a dict of categories and option lists +_APPEARANCE_DICT = { + # the key is your category; the value is a list of options, in the order you want them to appear + "body type": [ "skeletal", "skinny", "slender", "slim", "athletic", "muscular", "broad", "round", "curvy", "stout", "chubby" ], + "height": [ "diminutive", "short", "average", "tall", "towering" ], + } + +def menunode_categories(caller, **kwargs): + """Base node for categorized options.""" + # this is a new decision step, so save your resume point here + caller.new_char.db.chargen_step = "menunode_categories" + + text = dedent("""\ + |wOption Categories|n + + Some character attributes are part of the same mechanic or decision, + but need to be divided up into sub-categories. Character appearance + details are an example of where this can be useful. + """) + + help = "Some helpful extra information on what's affected by these choices works well here." + options = [] + + # just like for informational categories, build the options off of a dictionary to make it easier to manage + for category in _APPEARANCE_DICT.keys(): + options.append({"desc": f"Choose your |c{category}|n", "goto": ("menunode_category_options", { "category": category })}) + + # since this node goes in and out of sub-nodes, you need an option to proceed to the next step + options.append({"key": "(Next)", "next", "n"), "desc": "Continue to the next step.", "goto": "menunode_multi_choice"}) + # once past the first decision, it's also a good idea to include a "back to previous step" option + options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_info_base"}) + return (text, help), options + +def menunode_category_options(caller, raw_string, category=None, **kwargs): + """Choosing an option within the categories.""" + if not category: + # this shouldn't have happened, so quit and retry + return "Something went wrong. Please try again." + + # for mechanics-related choices, you can combine this with the + # informational options approach to give specific info + text = "Choose your {category}:" + help = "This will define your {category}." + + options = [] + # build the list of options from the right category of your dictionary + for option in _APPEARANCE_DICT[category]: + options.append({"desc": option, "goto": (_set_category_opt, { "category": category, "value": option})}) + # always include a "back" option in case they aren't ready to pick yet + options.append({"key": ("(Back)", "back", "b"), "desc": f"Don't change {category}", "goto": "menunode_categories"}) + return (text, help), options + +def _set_category_opt(caller, raw_string, category, value, **kwargs): + """Set the option for a category""" + + # this is where you would put any more complex code involved in setting the option, + # but we're just doing simple attributes + caller.new_char.attributes.add(category, value) + + # go back to the base node for the categories choice to pick another + return "menunode_categories" + + +######################################################### +# Multiple Choice +######################################################### + +# it's not as important to make this a separate list, but like all the others, +# it's easier to read and to update if you do! +_SKILL_OPTIONS = [ "alchemy", "archery", "blacksmithing", "brawling", "dancing", "fencing", "pottery", "tailoring", "weaving" ] + +def menunode_multi_choice(caller, raw_string, **kwargs): + """A multiple-choice menu node.""" + char = caller.new_char + + # another decision, so save the resume point + char.db.chargen_step = "menunode_multi_choice" + + # in order to support picking up from where we left off, get the options from the character + # if they weren't passed in + # this is again just a simple attribute, but you could retrieve this list however + selected = kwargs.get("selected") or char.attributes.get("skill_list", []) + + text = dedent("""\ + |wMultiple Choice|n + + Sometimes you want players to be able to pick more than one option - + for example, starting skills. + + You can easily define it as a minimum, maximum, or exact number of + selected options. + """) + + help = "This is a good place to specify how many choices are allowed or required." + + options = [] + for option in _SKILL_OPTIONS: + # check if the option has been selected + if option in selected: + # if it's been selected, we want to highlight that + opt_desc = f"|y({option}) (selected)|n" + else: + opt_desc = option + options.append({"desc": opt_desc, "goto": ( _set_multichoice, {"selected": selected, "option": option, "add": add})}) + + # only display the Next option if the requirements are met! + # for this example, you need exactly 3 choices, but you can use an inequality for "no more than X", or "at least X" + if len(selected) == 3: + options.append({"key": ("(Next)", "next", "n"), "desc": "Continue to the next step", "goto": "menunode_choose_objects"}) + options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_categories"}) + + return (text, help), options + +def _set_multichoice(caller, raw_string, selected=[], **kwargs): + """saves the current choices to the character""" + # get the option being chosen + if option := kwargs.get("option"): + # if the option is already in the list, then we want to remove it + if option in selected: + selected.remove(option) + # otherwise, we're adding it + else: + selected.add(option) + + # now that the options are updated, save it to the character + # this is just setting an attribute but it could be anything + caller.new_char.attributes.add("skill_list", selected) + + # pass the list back so we don't need to retrieve it again + return ("menunode_multi_choice", {"selected": selected}) + + +######################################################### +# Starting Objects +######################################################### + +# for a real game, you would most likely want to build this list from +# your existing game prototypes module(s), but for a demo this'll do! +_EXAMPLE_PROTOTYPES = [ + # starter sword prototype + { + "key": "basic sword", + "desc": "This sword will do fine for stabbing things.", + "tags": [("sword", "weapon")], + }, + # starter staff prototype + { + "key": "basic staff", + "desc": "You could hit things with it, or maybe use it as a spell focus.", + "tags": [("staff", "weapon"),("staff", "focus")], + } +] + +# this method will be run to create the starting objects +def create_objects(character): + """do the actual object spawning""" + # since our example chargen saves the starting prototype to an attribute, we retrieve that here + proto = dict(character.db.starter_weapon) + # set the location to our character, so they actually have it + proto["location"] = character + # create the object + spawn(proto) + + +def menunode_choose_objects(caller, raw_string, **kwargs): + """Selecting objects to start with""" + char = caller.new_char + + # another decision, so save the resume point + char.db.chargen_step = "menunode_choose_objects" + + text = dedent("""\ + |wStarting Objects|n + + Whether it's a cosmetic outfit, a starting weapon, or a professional + tool kit, you probably want to let your players have a choice in + what objects they start out with. + """) + + help = "An overview of what the choice affects - whether it's purely aesthetic or mechanical, and whether you can change it later - are good here." + + options = [] + + for proto in _EXAMPLE_PROTOTYPES: + # use the key as the option description, but pass the whole prototype + options.append({"desc": f"Choose {_INFLECT.an(option['key'])", "goto": ( _set_object_choice, {"proto": proto})}) + + options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_multi_choice"}) + + return (text, help), options + +def _set_object_choice(caller, raw_string, proto, **kwargs): + """Save the selected starting object(s)""" + + # we DON'T want to actually create the object, yet! that way players can still go back and change their mind + # instead, we save what object was chosen - in this case, by saving the prototype dict to the character + caller.new_char.db.starter_weapon = proto + + # continue to the next step + return "menunode_select_name" + + +######################################################### +# Choosing a Name +######################################################### + +def menunode_choose_name(caller, raw_string, **kwargs): + """Name selection""" + + # another decision, so save the resume point + char.db.chargen_step = "menunode_choose_name" + + text = dedent("""/ + |wChoosing a Name|n + + Especially for roleplaying-centric games, being able to choose your + character's name after deciding everything else, instead of before, + is really useful. + + Enter a name here to check if it's available. + """) + help = "You'll have a chance to change your mind before confirming, even if the name is free." + # since this is a free-text field, we just have the one + options = [ { "key": "_default", "goto": "menunode_check_charname" } ] + return (text, help), options + +def menunode_check_charname(caller, raw_string, **kwargs): + """Check and confirm name choice""" + + # strip any extraneous whitespace from the raw text + # if you want to do any other validation on the name, e.g. no punctuation allowed, this is the place! + charname = raw_string.strip() + + # check to make sure that the name doesn't already exist + candidates = Character.objects.filter_family(db_key__iexact=charname) + if len(candidates): + # the name is already taken - loop this node with new input to enter another name + text = f"|w{charname}|n is unavailable.\n\nEnter a different name." + options = [ { "key": "_default", "goto": "menunode_check_charname" } ] + return text, options + else: + # it's free! set the character's key to the name to reserve it + caller.new_char.key = charname + text = f"|w{charname}|n is available! Confirm?" + # let players change their mind and go back to the name choice, if they want + options = [ + { "key": "Yes", "goto": "menunode_end" }, + { "key": "No", "goto": "menunode_choose_name" }, + ] + return text, options + + + +######################################################### +# The End +######################################################### + +def menunode_end(caller, raw_string): + """End-of-chargen cleanup.""" + char = caller.new_char + # since everything is finished and confirmed, we actually create the starting objects now + create_objects(char) + + # clear in-progress status + caller.new_char.attributes.remove("chargen_step") + text = dedent(""" + Congratulations! + + You have completed character creation. Enjoy the game! + """) + return text, None + diff --git a/evennia/contrib/base_systems/character_creator/tests.py b/evennia/contrib/base_systems/character_creator/tests.py new file mode 100644 index 0000000000..8e8bf4fba2 --- /dev/null +++ b/evennia/contrib/base_systems/character_creator/tests.py @@ -0,0 +1,32 @@ +from evennia.utils.test_resources import ( + BaseEvenniaTest, + BaseEvenniaCommandTest, + EvenniaCommandTest, +) # noqa +from . import character_creator + +class TestAccount(BaseEvenniaCommandTest): + def test_ooc_look(self): + if settings.MULTISESSION_MODE < 2: + self.call( + account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account + ) + if settings.MULTISESSION_MODE == 2: + # with no playable characters + self.call( + account.CmdOOCLook(), + "", + "Account TestAccount (you are Out-of-Character)", + caller=self.account, + ) + # with in-progress character + + + def test_char_create(self): + self.call( + character_creator.ContribCmdCharCreate(), + "Test1=Test char", + "Created new character Test1. Use ic Test1 to enter the game", + caller=self.account, + ) + From f3589e5f0378ebdda0ba9b83e8cbaf484ab572a5 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Fri, 19 Aug 2022 23:11:28 -0600 Subject: [PATCH 2/9] ready to try it --- .../character_creator/character_creator.py | 20 +++++-------- .../base_systems/character_creator/tests.py | 29 ++++++++++--------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/evennia/contrib/base_systems/character_creator/character_creator.py b/evennia/contrib/base_systems/character_creator/character_creator.py index 2749d5dfe7..aa20cb87cd 100644 --- a/evennia/contrib/base_systems/character_creator/character_creator.py +++ b/evennia/contrib/base_systems/character_creator/character_creator.py @@ -27,6 +27,7 @@ from evennia.objects.models import ObjectDB from evennia.utils import create, search from evennia.utils.evmenu import EvMenu +_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS class ContribCmdCharCreate(MuxAccountCommand): """ @@ -48,7 +49,7 @@ class ContribCmdCharCreate(MuxAccountCommand): session = self.session # only one character should be in progress at a time, so we check for WIPs first - in_progress = [chara for chara in account.playable if chara.db.chargen_step] + in_progress = [chara for chara in account.db._playable_characters if chara.db.chargen_step] if len(in_progress): # we're continuing chargen for a WIP character @@ -70,7 +71,7 @@ class ContribCmdCharCreate(MuxAccountCommand): permissions = settings.PERMISSION_ACCOUNT_DEFAULT # generate a randomized key so the player can choose a character name later key = ''.join(choices(string.ascii_letters + string.digits, k=10) - new_character = create.create_object(Character, key=key, + new_character = create.create_object(_CHARACTER_TYPECLASS, key=key, location=None, home=home, permissions=permissions) @@ -92,18 +93,13 @@ class ContribCmdCharCreate(MuxAccountCommand): char = session.new_char if not char.db.chargen_step: # this means character creation was completed - start playing! - # any additional first-time-playing code can go here as well - # e.g. creating generic default gear, - # changing the pre-logout location of the character so they log in to a specific spot, - # notifying admins of a new player, etc. - - # execute the ic command as the session to start puppeting the character - session.execute_cmd("ic {}".format(char.key)) + # execute the ic command to start puppeting the character + account.execute_cmd("ic {}".format(char.key)) EvMenu(session, - settings.CHARGEN_MENU, - startnode=startnode, - cmd_on_exit=finish_char_callback) + settings.CHARGEN_MENU, + startnode=startnode, + cmd_on_exit=finish_char_callback) from django.conf import settings diff --git a/evennia/contrib/base_systems/character_creator/tests.py b/evennia/contrib/base_systems/character_creator/tests.py index 8e8bf4fba2..45076d7162 100644 --- a/evennia/contrib/base_systems/character_creator/tests.py +++ b/evennia/contrib/base_systems/character_creator/tests.py @@ -1,8 +1,6 @@ -from evennia.utils.test_resources import ( - BaseEvenniaTest, - BaseEvenniaCommandTest, - EvenniaCommandTest, -) # noqa +from django.test import override_settings +from evennia.utils import inherits_from +from evennia.utils.test_resources import BaseEvenniaCommandTest from . import character_creator class TestAccount(BaseEvenniaCommandTest): @@ -12,21 +10,26 @@ class TestAccount(BaseEvenniaCommandTest): account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account ) if settings.MULTISESSION_MODE == 2: - # with no playable characters - self.call( + # test both normal output and also inclusion of in-progress character + account.db._playable_characters = [self.char1] + self.char1.db.chargen_step = "start" + output = self.call( account.CmdOOCLook(), "", "Account TestAccount (you are Out-of-Character)", caller=self.account, ) - # with in-progress character - + self.assertIn("|Yin progress|n", output) + @override_settings(CHARGEN_MENU="evennia.contrib.base_systems.example_menu") def test_char_create(self): + account = self.account + session = self.session self.call( character_creator.ContribCmdCharCreate(), - "Test1=Test char", - "Created new character Test1. Use ic Test1 to enter the game", - caller=self.account, + "", + caller=account, ) - + menu = session.ndb._menutree + self.assertNotNone(menu) + self.assertTrue(inherits_from(session.new_char, DefaultCharacter) ) From 9e54d7add9ad2a9019f153df49355287439664ca Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Thu, 25 Aug 2022 11:51:33 -0600 Subject: [PATCH 3/9] various fixes --- .../character_creator/character_creator.py | 8 ++--- .../character_creator/example_menu.py | 30 +++++++++---------- .../base_systems/character_creator/tests.py | 17 ++++++----- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/evennia/contrib/base_systems/character_creator/character_creator.py b/evennia/contrib/base_systems/character_creator/character_creator.py index aa20cb87cd..976b2b176e 100644 --- a/evennia/contrib/base_systems/character_creator/character_creator.py +++ b/evennia/contrib/base_systems/character_creator/character_creator.py @@ -70,10 +70,10 @@ class ContribCmdCharCreate(MuxAccountCommand): default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) permissions = settings.PERMISSION_ACCOUNT_DEFAULT # generate a randomized key so the player can choose a character name later - key = ''.join(choices(string.ascii_letters + string.digits, k=10) + key = ''.join(choices(string.ascii_letters + string.digits, k=10)) new_character = create.create_object(_CHARACTER_TYPECLASS, key=key, location=None, - home=home, + home=default_home, permissions=permissions) # only allow creator (and developers) to puppet this char new_character.locks.add( @@ -150,7 +150,7 @@ class ContribChargenAccount(DefaultAccount): ) if session.sessid == csessid: result.append(f"\n |w* {isess+1}|n {addr}") - else + else: result.append(f"\n {isess+1} {addr}") result.append("\n\n |whelp|n - more commands") @@ -163,7 +163,7 @@ class ContribChargenAccount(DefaultAccount): if characters: result.append( - "\n |wdchardelete |n - delete a character (cannot be undone!)" + "\n |wchardelete |n - delete a character (cannot be undone!)" ) plural = "" if len(characters) == 1 else "s" result.append("\n |wic |n - enter the game (|wooc|n to return here)") diff --git a/evennia/contrib/base_systems/character_creator/example_menu.py b/evennia/contrib/base_systems/character_creator/example_menu.py index b4d5c6b5af..5217b1431e 100644 --- a/evennia/contrib/base_systems/character_creator/example_menu.py +++ b/evennia/contrib/base_systems/character_creator/example_menu.py @@ -95,7 +95,6 @@ _CLASS_INFO_DICT = { Warriors like to compete by beating each other up for fun. """), - }, "mage": dedent("""\ Mages prefer less combative lines of work, such as showmanship or selling enchanted charms. Those who choose to be a battle mage are @@ -104,7 +103,6 @@ _CLASS_INFO_DICT = { Mage schools, being led by the most academic-minded of mages, are notorious for intellectual snobbery. """), - }, } def menunode_info_base(caller): @@ -137,7 +135,7 @@ def menunode_info_class(caller, raw_string, selected_class=None, **kwargs): return "Something went wrong. Please try again." # Since you have all the info in a nice dict, you can just grab it to display here - text = _CLASS_INFO_DICT[pclass] + text = _CLASS_INFO_DICT[selected_class] help = "If you want option-specific help, you can define it in your info dict and reference it." options = [] @@ -153,7 +151,6 @@ def menunode_info_class(caller, raw_string, selected_class=None, **kwargs): def _set_class(caller, raw_string, selected_class=None, **kwargs): - """Set the character's species and define their available features.""" # a class should always be selected here if not selected_class: # go back to the base node for this decision @@ -200,7 +197,7 @@ def menunode_categories(caller, **kwargs): options.append({"desc": f"Choose your |c{category}|n", "goto": ("menunode_category_options", { "category": category })}) # since this node goes in and out of sub-nodes, you need an option to proceed to the next step - options.append({"key": "(Next)", "next", "n"), "desc": "Continue to the next step.", "goto": "menunode_multi_choice"}) + options.append({"key": ("(Next)", "next", "n"), "desc": "Continue to the next step.", "goto": "menunode_multi_choice"}) # once past the first decision, it's also a good idea to include a "back to previous step" option options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_info_base"}) return (text, help), options @@ -213,8 +210,8 @@ def menunode_category_options(caller, raw_string, category=None, **kwargs): # for mechanics-related choices, you can combine this with the # informational options approach to give specific info - text = "Choose your {category}:" - help = "This will define your {category}." + text = f"Choose your {category}:" + help = f"This will define your {category}." options = [] # build the list of options from the right category of your dictionary @@ -265,17 +262,17 @@ def menunode_multi_choice(caller, raw_string, **kwargs): selected options. """) - help = "This is a good place to specify how many choices are allowed or required." + help = "This is a good place to specify how many choices are allowed or required. This example requires exactly 3." options = [] for option in _SKILL_OPTIONS: # check if the option has been selected if option in selected: # if it's been selected, we want to highlight that - opt_desc = f"|y({option}) (selected)|n" + opt_desc = f"|y{option} (selected)|n" else: opt_desc = option - options.append({"desc": opt_desc, "goto": ( _set_multichoice, {"selected": selected, "option": option, "add": add})}) + options.append({"desc": opt_desc, "goto": ( _set_multichoice, {"selected": selected, "option": option})}) # only display the Next option if the requirements are met! # for this example, you need exactly 3 choices, but you can use an inequality for "no more than X", or "at least X" @@ -294,7 +291,7 @@ def _set_multichoice(caller, raw_string, selected=[], **kwargs): selected.remove(option) # otherwise, we're adding it else: - selected.add(option) + selected.append(option) # now that the options are updated, save it to the character # this is just setting an attribute but it could be anything @@ -357,7 +354,7 @@ def menunode_choose_objects(caller, raw_string, **kwargs): for proto in _EXAMPLE_PROTOTYPES: # use the key as the option description, but pass the whole prototype - options.append({"desc": f"Choose {_INFLECT.an(option['key'])", "goto": ( _set_object_choice, {"proto": proto})}) + options.append({"desc": f"Choose {_INFLECT.an(proto['key'])}", "goto": ( _set_object_choice, {"proto": proto})}) options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_multi_choice"}) @@ -371,7 +368,7 @@ def _set_object_choice(caller, raw_string, proto, **kwargs): caller.new_char.db.starter_weapon = proto # continue to the next step - return "menunode_select_name" + return "menunode_choose_name" ######################################################### @@ -380,11 +377,12 @@ def _set_object_choice(caller, raw_string, proto, **kwargs): def menunode_choose_name(caller, raw_string, **kwargs): """Name selection""" + char = caller.new_char # another decision, so save the resume point char.db.chargen_step = "menunode_choose_name" - text = dedent("""/ + text = dedent("""\ |wChoosing a Name|n Especially for roleplaying-centric games, being able to choose your @@ -418,8 +416,8 @@ def menunode_check_charname(caller, raw_string, **kwargs): text = f"|w{charname}|n is available! Confirm?" # let players change their mind and go back to the name choice, if they want options = [ - { "key": "Yes", "goto": "menunode_end" }, - { "key": "No", "goto": "menunode_choose_name" }, + { "key": ("Yes", "y"), "goto": "menunode_end" }, + { "key": ("No", "n"), "goto": "menunode_choose_name" }, ] return text, options diff --git a/evennia/contrib/base_systems/character_creator/tests.py b/evennia/contrib/base_systems/character_creator/tests.py index 45076d7162..7cb562be40 100644 --- a/evennia/contrib/base_systems/character_creator/tests.py +++ b/evennia/contrib/base_systems/character_creator/tests.py @@ -1,6 +1,9 @@ +from django.conf import settings from django.test import override_settings +from evennia import DefaultCharacter from evennia.utils import inherits_from from evennia.utils.test_resources import BaseEvenniaCommandTest +from evennia.commands.default import account from . import character_creator class TestAccount(BaseEvenniaCommandTest): @@ -11,7 +14,7 @@ class TestAccount(BaseEvenniaCommandTest): ) if settings.MULTISESSION_MODE == 2: # test both normal output and also inclusion of in-progress character - account.db._playable_characters = [self.char1] + self.account.db._playable_characters = [self.char1] self.char1.db.chargen_step = "start" output = self.call( account.CmdOOCLook(), @@ -21,15 +24,13 @@ class TestAccount(BaseEvenniaCommandTest): ) self.assertIn("|Yin progress|n", output) - @override_settings(CHARGEN_MENU="evennia.contrib.base_systems.example_menu") + @override_settings(CHARGEN_MENU="evennia.contrib.base_systems.character_creator.example_menu") def test_char_create(self): - account = self.account - session = self.session self.call( character_creator.ContribCmdCharCreate(), "", - caller=account, + caller=self.account, ) - menu = session.ndb._menutree - self.assertNotNone(menu) - self.assertTrue(inherits_from(session.new_char, DefaultCharacter) ) + menu = self.session.ndb._menutree + self.assertNotEqual(menu, None) + self.assertTrue(inherits_from(self.session.new_char, DefaultCharacter) ) From 99105388467a7459ec3f630707f7686597d94454 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Thu, 25 Aug 2022 12:07:49 -0600 Subject: [PATCH 4/9] clarify multisession mode --- evennia/contrib/base_systems/character_creator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/character_creator/README.md b/evennia/contrib/base_systems/character_creator/README.md index f45f957b82..9807f3a0f1 100644 --- a/evennia/contrib/base_systems/character_creator/README.md +++ b/evennia/contrib/base_systems/character_creator/README.md @@ -1,7 +1,7 @@ # Character Creator contrib by InspectorCaracal -This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, where characters are not automatically created to match the account. +This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, where characters are not automatically created to match the account. To use this with lower modes, you'll need to implement your own solution for preventing the built-in automatic character creation. ## Installation From 2b360c5366881caf2cd53ffc130924c6161d673a Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 26 Sep 2022 12:34:42 -0600 Subject: [PATCH 5/9] cleanup and revisions --- .../character_creator/README.md | 21 +++--- .../character_creator/character_creator.py | 8 ++- .../character_creator/example_menu.py | 71 ++++++++++++++----- .../character_creator/tests.py | 0 4 files changed, 70 insertions(+), 30 deletions(-) rename evennia/contrib/{base_systems => rpg}/character_creator/README.md (82%) rename evennia/contrib/{base_systems => rpg}/character_creator/character_creator.py (97%) rename evennia/contrib/{base_systems => rpg}/character_creator/example_menu.py (88%) rename evennia/contrib/{base_systems => rpg}/character_creator/tests.py (100%) diff --git a/evennia/contrib/base_systems/character_creator/README.md b/evennia/contrib/rpg/character_creator/README.md similarity index 82% rename from evennia/contrib/base_systems/character_creator/README.md rename to evennia/contrib/rpg/character_creator/README.md index 9807f3a0f1..16965b7e9f 100644 --- a/evennia/contrib/base_systems/character_creator/README.md +++ b/evennia/contrib/rpg/character_creator/README.md @@ -8,8 +8,8 @@ This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, where ch In your game folder `commands/default_cmdsets.py`, import and add `ContribCmdCharCreate` to your `AccountCmdSet`. Example: -```py -from evennia.contrib.base_systems.character_creator.character_creator import ContribCmdCharCreate +```python +from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate class AccountCmdSet(default_cmds.AccountCmdSet): @@ -23,18 +23,21 @@ In your game folder `typeclasses/accounts.py`, import and inherit from `ContribC (Alternatively, you can copy the `at_look` method directly into your own class.) Example: -```py -from evennia.contrib.base_systems.character_creator.character_creator import ContribChargenAccount +```python +from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount class Account(ContribChargenAccount): # your Account class code ``` -Lastly, in your `settings.py` file, define `CHARGEN_MENU` to your character creation menu module's location. +By default, the new `charcreate` command will reference the example menu provided by the contrib, so you can test it +out before building your own menu. You can reference [the example menu here]() for ideas on how to build your own. -Example: -```py -CHARGEN_MENU = "evennia.contrib.base_systems.character_creator.example_menu" +Once you have your own menu, just add it to your settings to use it. e.g. if your menu is in mygame/word/chargen_menu.py, +you'd add the following to your settings file: + +```python +CHARGEN_MENU = "world.chargen_menu" ``` ## Usage @@ -75,7 +78,7 @@ The contrib overrides the character creation command - `charcreate` - to use a c ### Changes to `Account.at_look` The contrib version works mostly the same as core evennia, but adds an additional check to recognize an in-progress character. If you've modified your own `at_look` hook, it's an easy addition to make: just add this section to the playable character list loop. -```py +```python for char in characters: # contrib code starts here if char.db.chargen_step: diff --git a/evennia/contrib/base_systems/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py similarity index 97% rename from evennia/contrib/base_systems/character_creator/character_creator.py rename to evennia/contrib/rpg/character_creator/character_creator.py index 976b2b176e..4a8328f3cb 100644 --- a/evennia/contrib/base_systems/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -27,7 +27,11 @@ from evennia.objects.models import ObjectDB from evennia.utils import create, search from evennia.utils.evmenu import EvMenu -_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS +_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS +try: + _CHARGEN_MENU = settings.CHARGEN_MENU +except AttributeError: + _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu" class ContribCmdCharCreate(MuxAccountCommand): """ @@ -97,7 +101,7 @@ class ContribCmdCharCreate(MuxAccountCommand): account.execute_cmd("ic {}".format(char.key)) EvMenu(session, - settings.CHARGEN_MENU, + _CHARGEN_MENU, startnode=startnode, cmd_on_exit=finish_char_callback) diff --git a/evennia/contrib/base_systems/character_creator/example_menu.py b/evennia/contrib/rpg/character_creator/example_menu.py similarity index 88% rename from evennia/contrib/base_systems/character_creator/example_menu.py rename to evennia/contrib/rpg/character_creator/example_menu.py index 5217b1431e..2a1a6b175c 100644 --- a/evennia/contrib/base_systems/character_creator/example_menu.py +++ b/evennia/contrib/rpg/character_creator/example_menu.py @@ -76,7 +76,7 @@ def menunode_welcome(caller): game wiki if you have one. """) help = "You can explain the commands for exiting and resuming more specifically here." - options = [{"desc": "Let's begin!", "goto": "menunode_info_base"}] + options = {"desc": "Let's begin!", "goto": "menunode_info_base"} return (text, help), options @@ -231,7 +231,6 @@ def _set_category_opt(caller, raw_string, category, value, **kwargs): # go back to the base node for the categories choice to pick another return "menunode_categories" - ######################################################### # Multiple Choice ######################################################### @@ -301,6 +300,18 @@ def _set_multichoice(caller, raw_string, selected=[], **kwargs): return ("menunode_multi_choice", {"selected": selected}) +######################################################### +# Simple List Options +######################################################### + +# If you just want a straightforward list of options, without any of the +# back-and-forth navigation or modifying of option text, evennia has an +# easy to use decorator available: `@list_node` + +# For an example of how to use it, check out the documentation for +# evennia.utils.evmenu - there's lots of other useful EvMenu tools too! + + ######################################################### # Starting Objects ######################################################### @@ -381,47 +392,69 @@ def menunode_choose_name(caller, raw_string, **kwargs): # another decision, so save the resume point char.db.chargen_step = "menunode_choose_name" - - text = dedent("""\ + + # check if an error message was passed to the node. if so, you'll want to include it + # into your "name prompt" at the end of the node text. + if error := kwargs.get("error"): + prompt_text = f"{error}. Enter a different name." + else: + # there was no error, so just ask them to enter a name. + prompt_text = "Enter a name here to check if it's available." + + # this will print every time the player is prompted to choose a name, + # including the prompt text defined above + text = dedent(f"""\ |wChoosing a Name|n Especially for roleplaying-centric games, being able to choose your character's name after deciding everything else, instead of before, is really useful. - Enter a name here to check if it's available. + {prompt_text} """) + help = "You'll have a chance to change your mind before confirming, even if the name is free." # since this is a free-text field, we just have the one - options = [ { "key": "_default", "goto": "menunode_check_charname" } ] + options = { "key": "_default", "goto": "_check_charname" } return (text, help), options -def menunode_check_charname(caller, raw_string, **kwargs): +def _check_charname(caller, raw_string, **kwargs): """Check and confirm name choice""" # strip any extraneous whitespace from the raw text # if you want to do any other validation on the name, e.g. no punctuation allowed, this is the place! charname = raw_string.strip() + # aside from validation, the built-in normalization function from the caller's Account does + # some useful cleanup on the input, just in case they try something sneaky + charname = caller.account.normalize_username(charname) + # check to make sure that the name doesn't already exist candidates = Character.objects.filter_family(db_key__iexact=charname) if len(candidates): - # the name is already taken - loop this node with new input to enter another name - text = f"|w{charname}|n is unavailable.\n\nEnter a different name." - options = [ { "key": "_default", "goto": "menunode_check_charname" } ] - return text, options + # the name is already taken - report back with the error + return ("menunode_choose_name", {"error": f"|w{charname}|n is unavailable.\n\nEnter a different name."}) else: # it's free! set the character's key to the name to reserve it caller.new_char.key = charname - text = f"|w{charname}|n is available! Confirm?" - # let players change their mind and go back to the name choice, if they want - options = [ - { "key": ("Yes", "y"), "goto": "menunode_end" }, - { "key": ("No", "n"), "goto": "menunode_choose_name" }, - ] - return text, options + # continue on to the confirmation node + return "menunode_confirm_name" + +def menunode_confirm_name(caller, raw_string, **kwargs): + """Confirm the name choice""" + char = caller.new_char + + # since we reserved the name by assigning it, you can reference the character key + # if you have any extra validation or normalization that changed the player's input + # this also serves to show the player exactly what name they'll get + text = f"|w{char.key}|n is available! Confirm?" + # let players change their mind and go back to the name choice, if they want + options = [ + { "key": ("Yes", "y"), "goto": "menunode_end" }, + { "key": ("No", "n"), "goto": "menunode_choose_name" }, + ] + return text, options - ######################################################### # The End diff --git a/evennia/contrib/base_systems/character_creator/tests.py b/evennia/contrib/rpg/character_creator/tests.py similarity index 100% rename from evennia/contrib/base_systems/character_creator/tests.py rename to evennia/contrib/rpg/character_creator/tests.py From bcf5766c719739a7b2d99c7e9cf3c188b7dea049 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 26 Sep 2022 12:42:36 -0600 Subject: [PATCH 6/9] forgot to put in the path --- evennia/contrib/rpg/character_creator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/character_creator/README.md b/evennia/contrib/rpg/character_creator/README.md index 16965b7e9f..612e19aa5c 100644 --- a/evennia/contrib/rpg/character_creator/README.md +++ b/evennia/contrib/rpg/character_creator/README.md @@ -31,7 +31,7 @@ class Account(ContribChargenAccount): ``` By default, the new `charcreate` command will reference the example menu provided by the contrib, so you can test it -out before building your own menu. You can reference [the example menu here]() for ideas on how to build your own. +out before building your own menu. You can reference [the example menu here](/evennia/contrib/rpg/character_creator/example_menu.py) for ideas on how to build your own. Once you have your own menu, just add it to your settings to use it. e.g. if your menu is in mygame/word/chargen_menu.py, you'd add the following to your settings file: From 587ba06468d3490d4c3e5f4e90143bf52bb540c5 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 26 Sep 2022 16:41:30 -0600 Subject: [PATCH 7/9] move list_node mention to docstring --- .../rpg/character_creator/example_menu.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/rpg/character_creator/example_menu.py b/evennia/contrib/rpg/character_creator/example_menu.py index 2a1a6b175c..ab432b6028 100644 --- a/evennia/contrib/rpg/character_creator/example_menu.py +++ b/evennia/contrib/rpg/character_creator/example_menu.py @@ -27,6 +27,15 @@ more than one. The example has a requirement of choosing exactly 3 options, but you can change it to a maximum or minimum number of required options - or remove the requirement check entirely. +## Simple List Options + +If you just want a straightforward list of options, without any of the back-and-forth +navigation or modifying of option text, evennia has an easy to use decorator +available: `@list_node` + +For an example of how to use it, check out the documentation for evennia.utils.evmenu +- there's lots of other useful EvMenu tools too! + ## Starting Objects Allows players to choose from a selection of starting objects. @@ -300,18 +309,6 @@ def _set_multichoice(caller, raw_string, selected=[], **kwargs): return ("menunode_multi_choice", {"selected": selected}) -######################################################### -# Simple List Options -######################################################### - -# If you just want a straightforward list of options, without any of the -# back-and-forth navigation or modifying of option text, evennia has an -# easy to use decorator available: `@list_node` - -# For an example of how to use it, check out the documentation for -# evennia.utils.evmenu - there's lots of other useful EvMenu tools too! - - ######################################################### # Starting Objects ######################################################### From 6a07aaa7406edbfe9bc1f9e36d8e89c3fe0229c9 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Wed, 28 Sep 2022 12:25:03 -0600 Subject: [PATCH 8/9] update for new settings --- evennia/contrib/rpg/character_creator/README.md | 10 ++++++++-- .../contrib/rpg/character_creator/character_creator.py | 9 +++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/rpg/character_creator/README.md b/evennia/contrib/rpg/character_creator/README.md index 612e19aa5c..7b3bcede04 100644 --- a/evennia/contrib/rpg/character_creator/README.md +++ b/evennia/contrib/rpg/character_creator/README.md @@ -1,8 +1,6 @@ # Character Creator contrib by InspectorCaracal -This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, where characters are not automatically created to match the account. To use this with lower modes, you'll need to implement your own solution for preventing the built-in automatic character creation. - ## Installation In your game folder `commands/default_cmdsets.py`, import and add `ContribCmdCharCreate` to your `AccountCmdSet`. @@ -30,6 +28,14 @@ class Account(ContribChargenAccount): # your Account class code ``` +In your settings file `server/conf/settings.py`, add the following settings: +```python +AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False +AUTO_PUPPET_ON_LOGIN = False +``` + +(If you want to allow players to create more than one character, you can customize that with the setting `MAX_NR_CHARACTERS`.) + By default, the new `charcreate` command will reference the example menu provided by the contrib, so you can test it out before building your own menu. You can reference [the example menu here](/evennia/contrib/rpg/character_creator/example_menu.py) for ideas on how to build your own. diff --git a/evennia/contrib/rpg/character_creator/character_creator.py b/evennia/contrib/rpg/character_creator/character_creator.py index 4a8328f3cb..a2888a789e 100644 --- a/evennia/contrib/rpg/character_creator/character_creator.py +++ b/evennia/contrib/rpg/character_creator/character_creator.py @@ -1,9 +1,6 @@ """ Character Creator contrib, by InspectorCaracal -This contrib is designed to be used in MULTISESSION_MODE = 2 or higher, -where characters are not automatically created to match the account. - # Features The primary feature of this contrib is defining the name and attributes @@ -29,9 +26,9 @@ from evennia.utils.evmenu import EvMenu _CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS try: - _CHARGEN_MENU = settings.CHARGEN_MENU + _CHARGEN_MENU = settings.CHARGEN_MENU except AttributeError: - _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu" + _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu" class ContribCmdCharCreate(MuxAccountCommand): """ @@ -60,7 +57,7 @@ class ContribCmdCharCreate(MuxAccountCommand): new_character = in_progress[0] else: # we're making a new character - charmax = settings.MAX_NR_CHARACTERS if settings.MULTISESSION_MODE > 1 else 1 + charmax = settings.MAX_NR_CHARACTERS if not account.is_superuser and ( account.db._playable_characters and len(account.db._playable_characters) >= charmax From 765e8bc1e2157f5746b1d498bb0fcb40fc5e2f51 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Wed, 28 Sep 2022 14:02:12 -0600 Subject: [PATCH 9/9] fix stray quotes thanks sk33t3rson for the catch --- evennia/contrib/rpg/character_creator/example_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/rpg/character_creator/example_menu.py b/evennia/contrib/rpg/character_creator/example_menu.py index ab432b6028..f9496c4213 100644 --- a/evennia/contrib/rpg/character_creator/example_menu.py +++ b/evennia/contrib/rpg/character_creator/example_menu.py @@ -412,7 +412,7 @@ def menunode_choose_name(caller, raw_string, **kwargs): help = "You'll have a chance to change your mind before confirming, even if the name is free." # since this is a free-text field, we just have the one - options = { "key": "_default", "goto": "_check_charname" } + options = { "key": "_default", "goto": _check_charname } return (text, help), options def _check_charname(caller, raw_string, **kwargs):