Cleanup chargen contrib to style

This commit is contained in:
Griatch 2022-10-02 10:30:21 +02:00
parent 79336a4041
commit 93324f75d6
4 changed files with 281 additions and 126 deletions

View file

@ -1,26 +1,32 @@
# Character Creator contrib # Character Creator contrib
by InspectorCaracal
Commands for managing and initiating an in-game character-creation menu.
Contribution by InspectorCaracal, 2022
## Installation ## Installation
In your game folder `commands/default_cmdsets.py`, import and add `ContribCmdCharCreate` to your `AccountCmdSet`. In your game folder `commands/default_cmdsets.py`, import and add
`ContribCmdCharCreate` to your `AccountCmdSet`.
Example: Example:
```python ```python
from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate
class AccountCmdSet(default_cmds.AccountCmdSet): class AccountCmdSet(default_cmds.AccountCmdSet):
def at_cmdset_creation(self): def at_cmdset_creation(self):
super().at_cmdset_creation() super().at_cmdset_creation()
self.add(ContribCmdCharCreate) self.add(ContribCmdCharCreate)
``` ```
In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount` on your Account class. 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.) (Alternatively, you can copy the `at_look` method directly into your own class.)
Example: ### Example:
```python ```python
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount
@ -29,18 +35,23 @@ class Account(ContribChargenAccount):
``` ```
In your settings file `server/conf/settings.py`, add the following settings: In your settings file `server/conf/settings.py`, add the following settings:
```python ```python
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
AUTO_PUPPET_ON_LOGIN = 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`.) (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 By default, the new `charcreate` command will reference the example menu
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. provided by the contrib, so you can test it out before building your own menu.
You can reference
[the example menu here](github:develop/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, Once you have your own menu, just add it to your settings to use it. e.g. if your menu is in
you'd add the following to your settings file: `mygame/word/chargen_menu.py`, you'd add the following to your settings file:
```python ```python
CHARGEN_MENU = "world.chargen_menu" CHARGEN_MENU = "world.chargen_menu"
@ -50,9 +61,13 @@ CHARGEN_MENU = "world.chargen_menu"
### The EvMenu ### 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. 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: The example menu includes code, tips, and instructions for the following types
of decision nodes:
#### Informational Pages #### Informational Pages
@ -74,16 +89,24 @@ Allows players to choose from a selection of starting objects, which are then cr
#### Choosing a Name #### 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! 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 ### `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. 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` ### 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. 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.
```python ```python
for char in characters: for char in characters:
# contrib code starts here # contrib code starts here

View file

@ -15,10 +15,11 @@ and examples, including how to allow players to choose and confirm
character names from within the menu. character names from within the menu.
""" """
from random import choices
import string import string
from random import choices
from django.conf import settings from django.conf import settings
from evennia import DefaultAccount
from evennia.commands.default.muxcommand import MuxAccountCommand from evennia.commands.default.muxcommand import MuxAccountCommand
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.utils import create, search from evennia.utils import create, search
@ -30,16 +31,18 @@ try:
except AttributeError: except AttributeError:
_CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu" _CHARGEN_MENU = "evennia.contrib.rpg.character_creator.example_menu"
class ContribCmdCharCreate(MuxAccountCommand): class ContribCmdCharCreate(MuxAccountCommand):
""" """
create a new character create a new character
Begin creating a new character, or resume character creation for Begin creating a new character, or resume character creation for
an existing in-progress character. an existing in-progress character.
You can stop character creation at any time and resume where You can stop character creation at any time and resume where
you left off later. you left off later.
""" """
key = "charcreate" key = "charcreate"
locks = "cmd:pperm(Player) and is_ooc()" locks = "cmd:pperm(Player) and is_ooc()"
help_category = "General" help_category = "General"
@ -48,17 +51,17 @@ class ContribCmdCharCreate(MuxAccountCommand):
"create the new character" "create the new character"
account = self.account account = self.account
session = self.session session = self.session
# only one character should be in progress at a time, so we check for WIPs first # only one character should be in progress at a time, so we check for WIPs first
in_progress = [chara for chara in account.db._playable_characters if chara.db.chargen_step] in_progress = [chara for chara in account.db._playable_characters if chara.db.chargen_step]
if len(in_progress): if len(in_progress):
# we're continuing chargen for a WIP character # we're continuing chargen for a WIP character
new_character = in_progress[0] new_character = in_progress[0]
else: else:
# we're making a new character # we're making a new character
charmax = settings.MAX_NR_CHARACTERS charmax = settings.MAX_NR_CHARACTERS
if not account.is_superuser and ( if not account.is_superuser and (
account.db._playable_characters and len(account.db._playable_characters) >= charmax account.db._playable_characters and len(account.db._playable_characters) >= charmax
): ):
@ -67,18 +70,22 @@ class ContribCmdCharCreate(MuxAccountCommand):
return return
# create the new character object, with default settings # create the new character object, with default settings
start_location = ObjectDB.objects.get_id(settings.START_LOCATION) # start_location = ObjectDB.objects.get_id(settings.START_LOCATION)
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
permissions = settings.PERMISSION_ACCOUNT_DEFAULT permissions = settings.PERMISSION_ACCOUNT_DEFAULT
# generate a randomized key so the player can choose a character name later # 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, new_character = create.create_object(
location=None, _CHARACTER_TYPECLASS,
home=default_home, key=key,
permissions=permissions) location=None,
home=default_home,
permissions=permissions,
)
# only allow creator (and developers) to puppet this char # only allow creator (and developers) to puppet this char
new_character.locks.add( new_character.locks.add(
f"puppet:pid({account.id}) or perm(Developer) or pperm(Developer);delete:id({account.id}) or perm(Admin)" f"puppet:pid({account.id}) or perm(Developer) or"
f" pperm(Developer);delete:id({account.id}) or perm(Admin)"
) )
# initalize the new character to the beginning of the chargen menu # initalize the new character to the beginning of the chargen menu
new_character.db.chargen_step = "menunode_welcome" new_character.db.chargen_step = "menunode_welcome"
@ -97,14 +104,7 @@ class ContribCmdCharCreate(MuxAccountCommand):
# execute the ic command to start puppeting the character # execute the ic command to start puppeting the character
account.execute_cmd("ic {}".format(char.key)) account.execute_cmd("ic {}".format(char.key))
EvMenu(session, EvMenu(session, _CHARGEN_MENU, startnode=startnode, cmd_on_exit=finish_char_callback)
_CHARGEN_MENU,
startnode=startnode,
cmd_on_exit=finish_char_callback)
from django.conf import settings
from evennia import DefaultAccount
class ContribChargenAccount(DefaultAccount): class ContribChargenAccount(DefaultAccount):
@ -147,7 +147,9 @@ class ContribChargenAccount(DefaultAccount):
csessid = sess.sessid csessid = sess.sessid
addr = "{protocol} ({address})".format( addr = "{protocol} ({address})".format(
protocol=sess.protocol_key, protocol=sess.protocol_key,
address=isinstance(sess.address, tuple) and str(sess.address[0]) or str(sess.address), address=isinstance(sess.address, tuple)
and str(sess.address[0])
or str(sess.address),
) )
if session.sessid == csessid: if session.sessid == csessid:
result.append(f"\n |w* {isess+1}|n {addr}") result.append(f"\n |w* {isess+1}|n {addr}")
@ -163,19 +165,13 @@ class ContribChargenAccount(DefaultAccount):
result.append("\n |wcharcreate|n - create a new character") result.append("\n |wcharcreate|n - create a new character")
if characters: if characters:
result.append( result.append("\n |wchardelete <name>|n - delete a character (cannot be undone!)")
"\n |wchardelete <name>|n - delete a character (cannot be undone!)"
)
plural = "" if len(characters) == 1 else "s" plural = "" if len(characters) == 1 else "s"
result.append("\n |wic <character>|n - enter the game (|wooc|n to return here)") result.append("\n |wic <character>|n - enter the game (|wooc|n to return here)")
if is_su: if is_su:
result.append( result.append(f"\n\nAvailable character{plural} ({len(characters)}/unlimited):")
f"\n\nAvailable character{plural} ({len(characters)}/unlimited):"
)
else: else:
result.append( result.append(f"\n\nAvailable character{plural} ({len(characters)}/{charmax}):")
f"\n\nAvailable character{plural} ({len(characters)}/{charmax}):"
)
for char in characters: for char in characters:
if char.db.chargen_step: if char.db.chargen_step:
@ -189,11 +185,13 @@ class ContribChargenAccount(DefaultAccount):
sid = sess in sessions and sessions.index(sess) + 1 sid = sess in sessions and sessions.index(sess) + 1
if sess and sid: if sess and sid:
result.append( result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by you in session {sid})" f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] (played by"
f" you in session {sid})"
) )
else: else:
result.append( result.append(
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by someone else)" f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] (played by"
" someone else)"
) )
else: else:
# character is available # character is available

View file

@ -24,7 +24,7 @@ allows the player to choose one.
Allows players to select and deselect options from the list in order to choose 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, 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 - but you can change it to a maximum or minimum number of required options -
or remove the requirement check entirely. or remove the requirement check entirely.
## Simple List Options ## Simple List Options
@ -58,32 +58,34 @@ the decisions players made earlier. Initializing skills, creating starting gear,
and other one-time method calls and set-up should be put here. and other one-time method calls and set-up should be put here.
""" """
from evennia import create_object import inflect
from evennia.prototypes.spawner import spawn
from evennia.utils import dedent from evennia.utils import dedent
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable
from evennia.prototypes.spawner import spawn from typeclasses.characters import Character
import inflect
_INFLECT = inflect.engine() _INFLECT = inflect.engine()
from typeclasses.characters import Character
######################################################### #########################################################
# Welcome Page # Welcome Page
######################################################### #########################################################
def menunode_welcome(caller): def menunode_welcome(caller):
"""Starting page.""" """Starting page."""
text = dedent("""\ text = dedent(
"""\
|wWelcome to Character Creation!|n |wWelcome to Character Creation!|n
This is the starting node for all brand new characters. It's a good place to 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, remind players that they can exit the character creator and resume later,
especially if you're going to have a really long chargen process. 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 A brief overview of the game could be a good idea here, too, or a link to your
game wiki if you have one. game wiki if you have one.
""") """
)
help = "You can explain the commands for exiting and resuming more specifically here." 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 return (text, help), options
@ -98,44 +100,57 @@ def menunode_welcome(caller):
# e.g. wherever you have the classes actually defined, so later updates only happen in one place. # e.g. wherever you have the classes actually defined, so later updates only happen in one place.
_CLASS_INFO_DICT = { _CLASS_INFO_DICT = {
# The keys here are the different options you can choose, and the values are the info pages # The keys here are the different options you can choose, and the values are the info pages
"warrior": dedent("""\ "warrior": dedent(
"""\
Most warriors specialize in melee weapons, although ranged combat Most warriors specialize in melee weapons, although ranged combat
is not unheard of. is not unheard of.
Warriors like to compete by beating each other up for fun. Warriors like to compete by beating each other up for fun.
"""), """
"mage": dedent("""\ ),
"mage": dedent(
"""\
Mages prefer less combative lines of work, such as showmanship or Mages prefer less combative lines of work, such as showmanship or
selling enchanted charms. Those who choose to be a battle mage are selling enchanted charms. Those who choose to be a battle mage are
highly sought after by adventuring parties. highly sought after by adventuring parties.
Mage schools, being led by the most academic-minded of mages, are Mage schools, being led by the most academic-minded of mages, are
notorious for intellectual snobbery. notorious for intellectual snobbery.
"""), """
),
} }
def menunode_info_base(caller): def menunode_info_base(caller):
"""Base node for the informational choices.""" """Base node for the informational choices."""
# this is a base node for a decision, so we want to save the character's progress here # 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" caller.new_char.db.chargen_step = "menunode_info_base"
text = dedent("""\ text = dedent(
"""\
|wInformational Pages|n |wInformational Pages|n
Sometimes you'll want to let players read more about options before choosing 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. 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." help = "A link to your wiki for more information on classes could be useful here."
options = [] options = []
# Build your options from your info dict so you don't need to update this to add new 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(): 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 })}) options.append(
{
"desc": f"Learn about the |c{pclass}|n class",
"goto": ("menunode_info_class", {"selected_class": pclass}),
}
)
return (text, help), options return (text, help), options
# putting your kwarg in the menu declaration helps keep track of what variables the node needs # 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): def menunode_info_class(caller, raw_string, selected_class=None, **kwargs):
"""Informational overview of a particular class""" """Informational overview of a particular class"""
# sometimes weird bugs happen - it's best to check for them rather than let the game break # sometimes weird bugs happen - it's best to check for them rather than let the game break
if not selected_class: if not selected_class:
# reset back to the previous step # reset back to the previous step
@ -149,13 +164,23 @@ def menunode_info_class(caller, raw_string, selected_class=None, **kwargs):
options = [] options = []
# set an option for players to choose this class # 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 })}) 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 # once again build your options from the same info dict
for pclass in _CLASS_INFO_DICT.keys(): for pclass in _CLASS_INFO_DICT.keys():
# make sure you don't print the currently displayed page as an option # make sure you don't print the currently displayed page as an option
if pclass != selected_class: if pclass != selected_class:
options.append({"desc": f"Learn about the |c{pclass}|n class", "goto": ("menunode_info_class", { "selected_class": pclass })}) options.append(
{
"desc": f"Learn about the |c{pclass}|n class",
"goto": ("menunode_info_class", {"selected_class": pclass}),
}
)
return (text, help), options return (text, help), options
@ -181,36 +206,71 @@ def _set_class(caller, raw_string, selected_class=None, **kwargs):
# for these subcategory options, we make a dict of categories and option lists # for these subcategory options, we make a dict of categories and option lists
_APPEARANCE_DICT = { _APPEARANCE_DICT = {
# the key is your category; the value is a list of options, in the order you want them to appear # 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" ], "body type": [
"height": [ "diminutive", "short", "average", "tall", "towering" ], "skeletal",
} "skinny",
"slender",
"slim",
"athletic",
"muscular",
"broad",
"round",
"curvy",
"stout",
"chubby",
],
"height": ["diminutive", "short", "average", "tall", "towering"],
}
def menunode_categories(caller, **kwargs): def menunode_categories(caller, **kwargs):
"""Base node for categorized options.""" """Base node for categorized options."""
# this is a new decision step, so save your resume point here # this is a new decision step, so save your resume point here
caller.new_char.db.chargen_step = "menunode_categories" caller.new_char.db.chargen_step = "menunode_categories"
text = dedent("""\ text = dedent(
"""\
|wOption Categories|n |wOption Categories|n
Some character attributes are part of the same mechanic or decision, Some character attributes are part of the same mechanic or decision,
but need to be divided up into sub-categories. Character appearance but need to be divided up into sub-categories. Character appearance
details are an example of where this can be useful. 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." help = "Some helpful extra information on what's affected by these choices works well here."
options = [] options = []
# just like for informational categories, build the options off of a dictionary to make it easier to manage # just like for informational categories, build the options off of a dictionary to make it
# easier to manage
for category in _APPEARANCE_DICT.keys(): for category in _APPEARANCE_DICT.keys():
options.append({"desc": f"Choose your |c{category}|n", "goto": ("menunode_category_options", { "category": category })}) 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 # 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(
# 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"}) "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 return (text, help), options
def menunode_category_options(caller, raw_string, category=None, **kwargs): def menunode_category_options(caller, raw_string, category=None, **kwargs):
"""Choosing an option within the categories.""" """Choosing an option within the categories."""
if not category: if not category:
@ -225,14 +285,23 @@ def menunode_category_options(caller, raw_string, category=None, **kwargs):
options = [] options = []
# build the list of options from the right category of your dictionary # build the list of options from the right category of your dictionary
for option in _APPEARANCE_DICT[category]: for option in _APPEARANCE_DICT[category]:
options.append({"desc": option, "goto": (_set_category_opt, { "category": category, "value": option})}) 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 # 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"}) options.append(
{
"key": ("(Back)", "back", "b"),
"desc": f"Don't change {category}",
"goto": "menunode_categories",
}
)
return (text, help), options return (text, help), options
def _set_category_opt(caller, raw_string, category, value, **kwargs): def _set_category_opt(caller, raw_string, category, value, **kwargs):
"""Set the option for a category""" """Set the option for a category"""
# this is where you would put any more complex code involved in setting the option, # this is where you would put any more complex code involved in setting the option,
# but we're just doing simple attributes # but we're just doing simple attributes
caller.new_char.attributes.add(category, value) caller.new_char.attributes.add(category, value)
@ -240,18 +309,30 @@ def _set_category_opt(caller, raw_string, category, value, **kwargs):
# go back to the base node for the categories choice to pick another # go back to the base node for the categories choice to pick another
return "menunode_categories" return "menunode_categories"
######################################################### #########################################################
# Multiple Choice # Multiple Choice
######################################################### #########################################################
# it's not as important to make this a separate list, but like all the others, # 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! # it's easier to read and to update if you do!
_SKILL_OPTIONS = [ "alchemy", "archery", "blacksmithing", "brawling", "dancing", "fencing", "pottery", "tailoring", "weaving" ] _SKILL_OPTIONS = [
"alchemy",
"archery",
"blacksmithing",
"brawling",
"dancing",
"fencing",
"pottery",
"tailoring",
"weaving",
]
def menunode_multi_choice(caller, raw_string, **kwargs): def menunode_multi_choice(caller, raw_string, **kwargs):
"""A multiple-choice menu node.""" """A multiple-choice menu node."""
char = caller.new_char char = caller.new_char
# another decision, so save the resume point # another decision, so save the resume point
char.db.chargen_step = "menunode_multi_choice" char.db.chargen_step = "menunode_multi_choice"
@ -260,17 +341,22 @@ def menunode_multi_choice(caller, raw_string, **kwargs):
# this is again just a simple attribute, but you could retrieve this list however # this is again just a simple attribute, but you could retrieve this list however
selected = kwargs.get("selected") or char.attributes.get("skill_list", []) selected = kwargs.get("selected") or char.attributes.get("skill_list", [])
text = dedent("""\ text = dedent(
"""\
|wMultiple Choice|n |wMultiple Choice|n
Sometimes you want players to be able to pick more than one option - Sometimes you want players to be able to pick more than one option -
for example, starting skills. for example, starting skills.
You can easily define it as a minimum, maximum, or exact number of You can easily define it as a minimum, maximum, or exact number of
selected options. selected options.
""") """
)
help = "This is a good place to specify how many choices are allowed or required. This example requires exactly 3." help = (
"This is a good place to specify how many choices are allowed or required. This example"
" requires exactly 3."
)
options = [] options = []
for option in _SKILL_OPTIONS: for option in _SKILL_OPTIONS:
@ -280,16 +366,32 @@ def menunode_multi_choice(caller, raw_string, **kwargs):
opt_desc = f"|y{option} (selected)|n" opt_desc = f"|y{option} (selected)|n"
else: else:
opt_desc = option opt_desc = option
options.append({"desc": opt_desc, "goto": ( _set_multichoice, {"selected": selected, "option": option})}) options.append(
{"desc": opt_desc, "goto": (_set_multichoice, {"selected": selected, "option": option})}
)
# only display the Next option if the requirements are met! # 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" # 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: if len(selected) == 3:
options.append({"key": ("(Next)", "next", "n"), "desc": "Continue to the next step", "goto": "menunode_choose_objects"}) options.append(
options.append({"key": ("(Back)", "back", "b"), "desc": "Go back to the previous step", "goto": "menunode_categories"}) {
"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 return (text, help), options
def _set_multichoice(caller, raw_string, selected=[], **kwargs): def _set_multichoice(caller, raw_string, selected=[], **kwargs):
"""saves the current choices to the character""" """saves the current choices to the character"""
# get the option being chosen # get the option being chosen
@ -326,10 +428,11 @@ _EXAMPLE_PROTOTYPES = [
{ {
"key": "basic staff", "key": "basic staff",
"desc": "You could hit things with it, or maybe use it as a spell focus.", "desc": "You could hit things with it, or maybe use it as a spell focus.",
"tags": [("staff", "weapon"),("staff", "focus")], "tags": [("staff", "weapon"), ("staff", "focus")],
} },
] ]
# this method will be run to create the starting objects # this method will be run to create the starting objects
def create_objects(character): def create_objects(character):
"""do the actual object spawning""" """do the actual object spawning"""
@ -339,7 +442,7 @@ def create_objects(character):
proto["location"] = character proto["location"] = character
# create the object # create the object
spawn(proto) spawn(proto)
def menunode_choose_objects(caller, raw_string, **kwargs): def menunode_choose_objects(caller, raw_string, **kwargs):
"""Selecting objects to start with""" """Selecting objects to start with"""
@ -347,34 +450,52 @@ def menunode_choose_objects(caller, raw_string, **kwargs):
# another decision, so save the resume point # another decision, so save the resume point
char.db.chargen_step = "menunode_choose_objects" char.db.chargen_step = "menunode_choose_objects"
text = dedent("""\ text = dedent(
"""\
|wStarting Objects|n |wStarting Objects|n
Whether it's a cosmetic outfit, a starting weapon, or a professional 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 tool kit, you probably want to let your players have a choice in
what objects they start out with. 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."
)
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 = [] options = []
for proto in _EXAMPLE_PROTOTYPES: for proto in _EXAMPLE_PROTOTYPES:
# use the key as the option description, but pass the whole prototype # use the key as the option description, but pass the whole prototype
options.append({"desc": f"Choose {_INFLECT.an(proto['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"}) options.append(
{
"key": ("(Back)", "back", "b"),
"desc": "Go back to the previous step",
"goto": "menunode_multi_choice",
}
)
return (text, help), options return (text, help), options
def _set_object_choice(caller, raw_string, proto, **kwargs): def _set_object_choice(caller, raw_string, proto, **kwargs):
"""Save the selected starting object(s)""" """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 # we DON'T want to actually create the object, yet! that way players can still go back and
# instead, we save what object was chosen - in this case, by saving the prototype dict to the character # 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 caller.new_char.db.starter_weapon = proto
# continue to the next step # continue to the next step
return "menunode_choose_name" return "menunode_choose_name"
@ -383,6 +504,7 @@ def _set_object_choice(caller, raw_string, proto, **kwargs):
# Choosing a Name # Choosing a Name
######################################################### #########################################################
def menunode_choose_name(caller, raw_string, **kwargs): def menunode_choose_name(caller, raw_string, **kwargs):
"""Name selection""" """Name selection"""
char = caller.new_char char = caller.new_char
@ -393,33 +515,37 @@ def menunode_choose_name(caller, raw_string, **kwargs):
# check if an error message was passed to the node. if so, you'll want to include it # 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. # into your "name prompt" at the end of the node text.
if error := kwargs.get("error"): if error := kwargs.get("error"):
prompt_text = f"{error}. Enter a different name." prompt_text = f"{error}. Enter a different name."
else: else:
# there was no error, so just ask them to enter a name. # there was no error, so just ask them to enter a name.
prompt_text = "Enter a name here to check if it's available." 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, # this will print every time the player is prompted to choose a name,
# including the prompt text defined above # including the prompt text defined above
text = dedent(f"""\ text = dedent(
f"""\
|wChoosing a Name|n |wChoosing a Name|n
Especially for roleplaying-centric games, being able to choose your Especially for roleplaying-centric games, being able to choose your
character's name after deciding everything else, instead of before, character's name after deciding everything else, instead of before,
is really useful. is really useful.
{prompt_text} {prompt_text}
""") """
)
help = "You'll have a chance to change your mind before confirming, even if the name is free." 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 # 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 return (text, help), options
def _check_charname(caller, raw_string, **kwargs): def _check_charname(caller, raw_string, **kwargs):
"""Check and confirm name choice""" """Check and confirm name choice"""
# strip any extraneous whitespace from the raw text # 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! # if you want to do any other validation on the name, e.g. no punctuation allowed, this
# is the place!
charname = raw_string.strip() charname = raw_string.strip()
# aside from validation, the built-in normalization function from the caller's Account does # aside from validation, the built-in normalization function from the caller's Account does
@ -430,13 +556,17 @@ def _check_charname(caller, raw_string, **kwargs):
candidates = Character.objects.filter_family(db_key__iexact=charname) candidates = Character.objects.filter_family(db_key__iexact=charname)
if len(candidates): if len(candidates):
# the name is already taken - report back with the error # 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."}) return (
"menunode_choose_name",
{"error": f"|w{charname}|n is unavailable.\n\nEnter a different name."},
)
else: else:
# it's free! set the character's key to the name to reserve it # it's free! set the character's key to the name to reserve it
caller.new_char.key = charname caller.new_char.key = charname
# continue on to the confirmation node # continue on to the confirmation node
return "menunode_confirm_name" return "menunode_confirm_name"
def menunode_confirm_name(caller, raw_string, **kwargs): def menunode_confirm_name(caller, raw_string, **kwargs):
"""Confirm the name choice""" """Confirm the name choice"""
char = caller.new_char char = caller.new_char
@ -447,9 +577,9 @@ def menunode_confirm_name(caller, raw_string, **kwargs):
text = f"|w{char.key}|n is available! Confirm?" text = f"|w{char.key}|n is available! Confirm?"
# let players change their mind and go back to the name choice, if they want # let players change their mind and go back to the name choice, if they want
options = [ options = [
{ "key": ("Yes", "y"), "goto": "menunode_end" }, {"key": ("Yes", "y"), "goto": "menunode_end"},
{ "key": ("No", "n"), "goto": "menunode_choose_name" }, {"key": ("No", "n"), "goto": "menunode_choose_name"},
] ]
return text, options return text, options
@ -457,6 +587,7 @@ def menunode_confirm_name(caller, raw_string, **kwargs):
# The End # The End
######################################################### #########################################################
def menunode_end(caller, raw_string): def menunode_end(caller, raw_string):
"""End-of-chargen cleanup.""" """End-of-chargen cleanup."""
char = caller.new_char char = caller.new_char
@ -465,10 +596,11 @@ def menunode_end(caller, raw_string):
# clear in-progress status # clear in-progress status
caller.new_char.attributes.remove("chargen_step") caller.new_char.attributes.remove("chargen_step")
text = dedent(""" text = dedent(
"""
Congratulations! Congratulations!
You have completed character creation. Enjoy the game! You have completed character creation. Enjoy the game!
""") """
)
return text, None return text, None

View file

@ -1,11 +1,13 @@
from django.conf import settings from django.conf import settings
from django.test import override_settings from django.test import override_settings
from evennia import DefaultCharacter from evennia import DefaultCharacter
from evennia.commands.default import account
from evennia.utils import inherits_from from evennia.utils import inherits_from
from evennia.utils.test_resources import BaseEvenniaCommandTest from evennia.utils.test_resources import BaseEvenniaCommandTest
from evennia.commands.default import account
from . import character_creator from . import character_creator
class TestAccount(BaseEvenniaCommandTest): class TestAccount(BaseEvenniaCommandTest):
def test_ooc_look(self): def test_ooc_look(self):
if settings.MULTISESSION_MODE < 2: if settings.MULTISESSION_MODE < 2:
@ -33,4 +35,4 @@ class TestAccount(BaseEvenniaCommandTest):
) )
menu = self.session.ndb._menutree menu = self.session.ndb._menutree
self.assertNotEqual(menu, None) self.assertNotEqual(menu, None)
self.assertTrue(inherits_from(self.session.new_char, DefaultCharacter) ) self.assertTrue(inherits_from(self.session.new_char, DefaultCharacter))