Update docs

This commit is contained in:
Griatch 2022-09-17 18:30:20 +02:00
parent f05add7c5b
commit e8d08ad597
11 changed files with 751 additions and 98 deletions

View file

@ -192,7 +192,17 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
- New `at_server_init()` hook called before all other startup hooks for all
startup modes. Used for more generic overriding (volund)
- New `search` lock type used to completely hide an object from being found by
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper)
- Change setting `MULTISESSION_MODE` to now only control sessions, not how many
characters can be puppeted simultaneously. New settings now control that.
- Add new setting `AUTO_CREATE_CHARACTER_WITH_ACCOUNT`, a boolean deciding if
the new account should also get a matching character (legacy MUD style).
- Add new setting `AUTO_PUPPET_ON_LOGIN`, boolean deciding if one should
automatically puppet the last/available character on connection (legacy MUD style)
- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account
can run at the same time. Used to limit multi-playing.
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
## Evennia 0.9.5

View file

@ -114,7 +114,7 @@ something like `call:false()`.
in Commands). This is how to create entirely 'undetectable' in-game objects.
If not setting this lock excplicitly, all objects are assumed searchable.
Note that if you are aiming to make some _permanently invisible game system,
using a [Script](Scripts) is a better bet.
using a [Script](./Scripts.md) is a better bet.
- `get`- who may pick up the object and carry it around.
- `puppet` - who may "become" this object and control it as their "character".
- `attrcreate` - who may create new attributes on the object (default True)

View file

@ -51,7 +51,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
From here, you can use the default builder commands to create clothes
with which to test the system:
create a pretty shirt : evennia.contrib.game_systems.clothing.ContribClothing
create a pretty shirt : evennia.contrib.game_systems.clothing.Clothing
set shirt/clothing_type = 'top'
wear shirt

View file

@ -11,7 +11,7 @@ menu system `EvMenu` under the hood.
To install, add this to `mygame/server/conf/settings.py`:
CMDSET_UNLOGGEDIN = "evennia.contrib.base_systems.menu_login.UnloggedinCmdSet"
CONNECTION_SCREEN_MODULE = "evennia.contrib.base_systems.menu_login.connection_screens"
CONNECTION_SCREEN_MODULE = "contrib.base_systems.menu_login.connection_screens"
Reload the server and reconnect to see the changes.

View file

@ -384,8 +384,8 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
`race` as `charrace` thus matches in style.
We'd then need to expand our [rules module](Beginner-Tutorial-Rules.md) (and later
[character generation](Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
## Summary

View file

@ -3,12 +3,29 @@
In previous lessons we have established how a character looks. Now we need to give the player a
chance to create one.
In _Knave_, most of the character-generation is random. This means this tutorial can be pretty
compact while still showing the basic idea. What we will create is a menu looking like this:
## How it will work
We want to have chargen appear when we log in.
A fresh Evennia install will automatically create a new Character with the same name as your
Account when you log in. This is quick and simple and mimics older MUD styles. You could picture
doing this, and then customizing the Character in-place.
We will be a little more sophisticated though. We want the user to be able to create a character
using a menu when they log in.
We do this by editing `mygame/server/conf/settings.py` and adding the line
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
When doing this, connecting with the game with a new account will land you in "OOC" mode. The
ooc-version of `look` (sitting in the Account cmdset) will show a list of available characters
if you have any. You can also enter `charcreate` to make a new character. The `charcreate` is a
simple command coming with Evennia that just lets you make a new character with a given name and
description. We will later modify that to kick off our chargen. For now we'll just keep in mind
that's how we'll start off the menu.
In _Knave_, most of the character-generation is random. This means this tutorial can be pretty
compact while still showing the basic idea. What we will create is a menu looking like this:
```
Silas
@ -65,7 +82,632 @@ If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then
to the main node to see your new character, but this time the option to swap will no longer be
available (you can only do it once).
If you finally select the `Accept and create character` option, you will leave character
generation and start the game as this character.
If you finally select the `Accept and create character` option, the character will be created
and you'll leave the menu;
Character was created!
## Random tables
```{sidebar}
Full Knave random tables are found in
[evennia/contrib/tutorials/evadventure/random_tables.py](evennia.contrib.tutorials.evadventure.random_tables).
```
> Make a new module `mygame/evadventure/random_tables.py`.
Since most of _Knave_'s character generation is random we will need to roll on random tables
from the _Knave_ rulebook. While we added the ability to roll on a random table back in the
[Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
```
# in mygame/evadventure/random_tables.py
character_generation = {
"physique": [
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
],
"face": [
"bloated", "blunt", "bony", # ...
], # ...
}
```
The tables are just copied from the _Knave_ rules. We group the aspects in a dict
`character_generation` to separate chargen-only tables from other random tables we'll also
keep in here.
## Storing state of the menu
```{sidebar}
There is a full implementation of the chargen in [evennia/contrib/tutorials/evadventure/chargen.
py](evennia.contrib.tutorials.evadventure.chargen).
```
> create a new module `mygame/evadventure/chargen.py`.
During character generation we will need an entity to store/retain the changes, like a
'temporary character sheet'.
```python
# in mygame/evadventure/chargen.py
from .random_tables import chargen_table
from .rules import dice
class TemporaryCharacterSheet:
def __init__(self):
self.ability_changes = 0 # how many times we tried swap abilities
def _random_ability(self):
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
def generate(self):
# name will likely be modified later
self.name = dice.roll_random_table("1d282", chargen_table["name"])
# base attribute values
self.strength = self._random_ability()
self.dexterity = self._random_ability()
self.constitution = self._random_ability()
self.intelligence = self._random_ability()
self.wisdom = self._random_ability()
self.charisma = self._random_ability()
# physical attributes (only for rp purposes)
physique = dice.roll_random_table("1d20", chargen_table["physique"])
face = dice.roll_random_table("1d20", chargen_table["face"])
skin = dice.roll_random_table("1d20", chargen_table["skin"])
hair = dice.roll_random_table("1d20", chargen_table["hair"])
clothing = dice.roll_random_table("1d20", chargen_table["clothing"])
speech = dice.roll_random_table("1d20", chargen_table["speech"])
virtue = dice.roll_random_table("1d20", chargen_table["virtue"])
vice = dice.roll_random_table("1d20", chargen_table["vice"])
background = dice.roll_random_table("1d20", chargen_table["background"])
misfortune = dice.roll_random_table("1d20", chargen_table["misfortune"])
alignment = dice.roll_random_table("1d20", chargen_table["alignment"])
self.desc = (
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
f" and {clothing} clothing. You were a {background.title()}, but you were"
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
f" {alignment} alignment."
)
#
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
self.xp = 0
self.level = 1
# random equipment
self.armor = dice.roll_random_table("1d20", chargen_table["armor"])
_helmet_and_shield = dice.roll_random_table("1d20", chargen_table["helmets and shields"])
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
self.weapon = dice.roll_random_table("1d20", chargen_table["starting weapon"])
self.backpack = [
"ration",
"ration",
dice.roll_random_table("1d20", chargen_table["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_table["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_table["general gear 1"]),
dice.roll_random_table("1d20", chargen_table["general gear 2"]),
]
```
Here we have followed the _Knave_ rulebook to randomize abilities, description and equipment.
The `dice.roll()` and `dice.roll_random_table` methods now become very useful! Everything here
should be easy to follow.
The main difference from baseline _Knave_ is that we make a table of "starting weapon" (in Knave
you can pick whatever you like).
We also initialize `.ability_changes = 0`. Knave only allows us to swap the values of two
Abilities _once_. We will use this to know if it has been done or not.
### Showing the sheet
Now that we have our temporary character sheet, we should make it easy to visualize it.
```python
# in mygame/evadventure/chargen.py
_TEMP_SHEET = """
{name}
STR +{strength}
DEX +{dexterity}
CON +{constitution}
INT +{intelligence}
WIS +{wisdom}
CHA +{charisma}
{description}
Your belongings:
{equipment}
"""
class TemporaryCharacterSheet:
# ...
def show_sheet(self):
equipment = (
str(item)
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
if item
)
return _TEMP_SHEET.format(
name=self.name,
strength=self.strength,
dexterity=self.dexterity,
constitution=self.constitution,
intelligence=self.intelligence,
wisdom=self.wisdom,
charisma=self.charisma,
description=self.desc,
equipment=", ".join(equipment),
)
```
The new `show_sheet` method collect the data from the temporary sheet and return it in a pretty
form. Making a 'template' string like `_TEMP_SHEET` makes it easier to change things later if you want
to change how things look.
### Apply character
Once we are happy with our character, we need to actually create it with the stats we chose.
This is a bit more involved.
```python
# in mygame/evadventure/chargen.py
# ...
from .characters import EvAdventureCharacter
from evennia import create_object
from evennia.prototypes.spawner import spawn
class TemporaryCharacterSheet:
# ...
def apply(self):
# create character object with given abilities
new_character = create_object(
EvAdventureCharacter,
key=self.name,
attrs=(
("strength", self.strength),
("dexterity", self.dexterity),
("constitution", self.constitution),
("intelligence", self.intelligence),
("wisdom", self.wisdom),
("charisma", self.wisdom),
("hp", self.hp),
("hp_max", self.hp_max),
("desc", self.desc),
),
)
# spawn equipment (will require prototypes created before it works)
if self.weapon:
weapon = spawn(self.weapon)
new_character.equipment.move(weapon)
if self.shield:
shield = spawn(self.shield)
new_character.equipment.move(shield)
if self.armor:
armor = spawn(self.armor)
new_character.equipment.move(armor)
if self.helmet:
helmet = spawn(self.helmet)
new_character.equipment.move(helmet)
for item in self.backpack:
item = spawn(item)
new_character.equipment.store(item)
return new_character
```
We use `create_object` to create a new `EvAdventureCharacter`. We feed it with all relevant data
from the temporary character sheet. This is when these become an actual character.
```{sidebar}
A prototype is basically a `dict` describing how the object should be created. Since
it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
things from those prototypes.
```
Each piece of equipment is an object in in its own right. We will here assume that all game
items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
armor" etc.
We haven't actually created those prototypes yet, so for now we'll need to assume they are there.
Once a piece of equipment has been spawned, we make sure to move it into the `EquipmentHandler` we
created in the [Equipment lesson](./Beginner-Tutorial-Equipment.md).
## Initializing EvMenu
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
[EvMenu](../../../Components/EvMenu.md).
```python
# in mygame/evadventure/chargen.py
from evennia import EvMenu
# ...
# chargen menu
# this goes to the bottom of the module
def start_chargen(caller, session=None):
"""
This is a start point for spinning up the chargen from a command later.
"""
menutree = {} # TODO!
# this generates all random components of the character
tmp_character = TemporaryCharacterSheet()
tmp_character.generate()
EvMenu(caller, menutree, session=session, tmp_character=tmp_character)
```
This first function is what we will call from elsewhere (for example from a custom `charcreate`
command) to kick the menu into gear.
It takes the `caller` (the one to want to start the menu) and a `session` argument. The latter will help
track just which client-connection we are using (depending on Evennia settings, you could be
connecting with multiple clients).
We create a `TemporaryCharacterSheet` and call `.generate()` to make a random character. We then
feed all this into `EvMenu`.
The moment this happens, the user will be in the menu, there are no further steps needed.
The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump
between.
## Main Node: Choosing what to do
This is the first menu node. It will act as a central hub, from which one can choose different
actions.
```python
# in mygame/evadventure/chargen.py
# ...
# at the end of the module, but before the `start_chargen` function
def node_chargen(caller, raw_string, **kwargs):
tmp_character = kwargs["tmp_character"]
text = tmp_character.show_sheet()
options = [
{
"desc": "Change your name",
"goto": ("node_change_name", kwargs)
}
]
if tmp_character.ability_changes <= 0:
options.append(
{
"desc": "Swap two of your ability scores (once)",
"goto": ("node_swap_abilities", kwargs),
}
)
options.append(
{
"desc": "Accept and create character",
"goto": ("node_apply_character", kwargs)
},
)
return text, options
# ...
```
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While
not required, it helps you track what is a node and not.
Every menu-node, should accept `caller, raw_string, **kwargs` as arguments. Here `caller` is the
`caller` you passed into the `EvMenu` call. `raw_string` is the input given by the user in order
to _get to this node_, so currently empty. The `**kwargs` are all extra keyword arguments passed
into `EvMenu`. They can also be passed between nodes. In this case, we passed the
keyword `tmp_character` to `EvMenu`. We now have the temporary character sheet available in the
node!
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will
show to the user when looking at this node. The `options` are, well, what options should be
presented to move on from here to some other place.
For the text, we simply get a pretty-print of the temporary character sheet. A single option is
defined as a `dict` like this:
```python
{
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
"desc": "text to describe what happens when selecting option",.
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable)
}
```
Multiple option-dicts are returned in a list or tuple. The `goto` option-key is important to
understand. The job of this is to either point directly to another node (by giving its name), or
by pointing to a Python callable (like a function) _that then returns that name_. You can also
pass kwargs (as a dict). This will be made available as `**kwargs` in the callable or next node.
While an option can have a `key`, you can also skip it to just get a running number.
In our `node_chargen` node, we point to three nodes by name: `node_change_name`,
`node_swap_abilities`, and `node_apply_character`. We also make sure to pass along `kwargs`
to each node, since that contains our temporary character sheet.
The middle of these options only appear if we haven't already switched two abilities around - to
know this, we check the `.ability_changes` property to make sure it's still 0.
## Node: Changing your name
This is where you end up if you opted to change your name in `node_chargen`.
```python
# in mygame/evadventure/chargen.py
# ...
# after previous node
def _update_name(caller, raw_string, **kwargs):
"""
Used by node_change_name below to check what user
entered and update the name if appropriate.
"""
if raw_string:
tmp_character = kwargs["tmp_character"]
tmp_character.name = raw_string.lower().capitalize()
return "node_chargen", kwargs
def node_change_name(caller, raw_string, **kwargs):
"""
Change the random name of the character.
"""
tmp_character = kwargs["tmp_character"]
text = (
f"Your current name is |w{tmp_character.name}|n. "
"Enter a new name or leave empty to abort."
)
options = {
"key": "_default",
"goto": (_update_name, kwargs)
}
return text, options
```
There are two functions here - the menu node itself (`node_change_name`) and a
helper _goto_function_ (`_update_name`) to handle the user's input.
For the (single) option, we use a special `key` named `_default`. This makes this option
a catch-all: If the user enters something that does not match any other option, this is
the option that will be used.
Since we have no other options here, we will always use this option no matter what the user enters.
Also note that the `goto` part of the option points to the `_update_name` callable rather than to
the name of a node. It's important we keep passing `kwargs` along to it!
When a user writes anything at this node, the `_update_name` callable will be called. This has
the same arguments as a node, but it is _not_ a node - we will only use it to _figure out_ which
node to go to next.
In `_update_name` we now have a use for the `raw_string` argument - this is what was written by
the user on the previous node, remember? This is now either an empty string (meaning to ignore
it) or the new name of the character.
A goto-function like `_update_name` must return the name of the next node to use. It can also
optionally return the `kwargs` to pass into that node - we want to always do this, so we don't
loose our temporary character sheet. Here we will always go back to the `node_chargen`.
> Hint: If returning `None` from a goto-callable, you will always return to the last node you
> were at.
## Node: Swapping Abilities around
You get here by selecting the second option from the `node_chargen` node.
```python
# in mygame/evadventure/chargen.py
# ...
# after previous node
_ABILITIES = {
"STR": "strength",
"DEX": "dexterity",
"CON": "constitution",
"INT": "intelligence",
"WIS": "wisdom",
"CHA": "charisma",
}
def _swap_abilities(caller, raw_string, **kwargs):
"""
Used by node_swap_abilities to parse the user's input and swap ability
values.
"""
if raw_string:
abi1, *abi2 = raw_string.split(" ", 1)
if not abi2:
caller.msg("That doesn't look right.")
return None, kwargs
abi2 = abi2[0]
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
caller.msg("Not a familiar set of abilites.")
return None, kwargs
# looks okay = swap values. We need to convert STR to strength etc
tmp_character = kwargs["tmp_character"]
abi1 = _ABILITIES[abi1]
abi2 = _ABILITIES[abi2]
abival1 = getattr(tmp_character, abi1)
abival2 = getattr(tmp_character, abi2)
setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1
return "node_chargen", kwargs
def node_swap_abilities(caller, raw_string, **kwargs):
"""
One is allowed to swap the values of two abilities around, once.
"""
tmp_character = kwargs["tmp_character"]
text = f"""
Your current abilities:
STR +{tmp_character.strength}
DEX +{tmp_character.dexterity}
CON +{tmp_character.constitution}
INT +{tmp_character.intelligence}
WIS +{tmp_character.wisdom}
CHA +{tmp_character.charisma}
You can swap the values of two abilities around.
You can only do this once, so choose carefully!
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
"""
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
return text, options
```
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and
and a goto-callable helper (`_swap_abilities`). We catch everything the user writes on the
node (such as `WIS CON`) and feed it into the helper.
In `_swap_abilities`, we need to analyze the `raw_string` from the user to see what they
want to do.
Most code in the helper is validating the user didn't enter nonsense. If they did,
we use `caller.msg()` to tell them and then return `None, kwargs`, which re-runs the same node (the
name-selection) all over again.
Since we want users to be able to write "CON" instead of the longer "constitution", we need a
mapping `_ABILITIES` to easily convert between the two (it's stored as `consitution` on the
temporary character sheet). Once we know which abilities they want to swap, we do so and tick up
the `.ability_changes` counter. This means this option will no longer be available from the main
node.
Finally, we return to `node_chargen` again.
## Node: Creating the Character
We get here from the main node by opting to finish chargen.
```python
node_apply_character(caller, raw_string, **kwargs):
"""
End chargen and create the character. We will also puppet it.
"""
tmp_character = kwargs["tmp_character"]
new_character = tmp_character.apply(caller)
caller.account.db._playable_characters = [new_character]
text = "Character created!"
return text, None
```
When entering the node, we will take the Temporary character sheet and use its `.appy` method to
create a new Character with all equipment.
This is what is called an _end node_, because it returns `None` instead of options. After this,
the menu will exit. We will be back to the default character selection screen. The characters
found on that screen are the ones listed in the `_playable_characters` Attribute, so we need to
also the new character to it.
## Tying the nodes together
```python
def start_chargen(caller, session=None):
"""
This is a start point for spinning up the chargen from a command later.
"""
menutree = { # <----- can now add this!
"node_chargen": node_chargen,
"node_change_name": node_change_name,
"node_swap_abilities": node_swap_abilities,
"node_apply_character": node_apply_character
}
# this generates all random components of the character
tmp_character = TemporaryCharacterSheet()
tmp_character.generate()
EvMenu(caller, menutree, session=session,
startnode="node_chargen", # <-----
tmp_character=tmp_character)
```
Now that we have all the nodes, we add them to the `menutree` we left empty before. We only add
the nodes, _not_ the goto-helpers! The keys we set in the `menutree` dictionary are the names we
should use to point to nodes from inside the menu (and we did).
We also add a keyword argument `startnode` pointing to the `node_chargen` node. This tells EvMenu
to first jump into that node when the menu is starting up.
## Conclusions
This lesson taught us how to use `EvMenu` to make an interactive character generator. In an RPG
more complex than _Knave_, the menu would be bigger and more intricate, but the same principles
apply.
Together with the previous lessons we have now fished most of the basics around player
characters - how they store their stats, handle their equipment and how to create them.
In the next lesson we'll address how EvAdventure _Rooms_ work.

View file

@ -0,0 +1,5 @@
# In-game Rooms
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -268,7 +268,7 @@ EXTRA_LAUNCHER_COMMANDS = {}
MAX_CHAR_LIMIT = 6000
# The warning to echo back to users if they enter a very large string
MAX_CHAR_LIMIT_WARNING = (
"You entered a string that was too long. " "Please break it up into multiple parts."
"You entered a string that was too long. Please break it up into multiple parts."
)
# If this is true, errors and tracebacks from the engine will be
# echoed as text in-game as well as to the log. This can speed up
@ -557,8 +557,6 @@ BASE_SCRIPT_TYPECLASS = "typeclasses.scripts.Script"
# is Limbo (#2).
DEFAULT_HOME = "#2"
# The start position for new characters. Default is Limbo (#2).
# MULTISESSION_MODE = 0, 1 - used by default unloggedin create command
# MULTISESSION_MODE = 2, 3 - used by default character_create command
START_LOCATION = "#2"
# Lookups of Attributes, Tags, Nicks, Aliases can be aggressively
# cached to avoid repeated database hits. This often gives noticeable
@ -728,21 +726,31 @@ GLOBAL_SCRIPTS = {
######################################################################
# Different Multisession modes allow a player (=account) to connect to the
# game simultaneously with multiple clients (=sessions). In modes 0,1 there is
# only one character created to the same name as the account at first login.
# In modes 2,3 no default character will be created and the MAX_NR_CHARACTERS
# value (below) defines how many characters the default char_create command
# allow per account.
# 0 - single session, one account, one character, when a new session is
# connected, the old one is disconnected
# 1 - multiple sessions, one account, one character, each session getting
# the same data
# 2 - multiple sessions, one account, many characters, one session per
# character (disconnects multiplets)
# 3 - like mode 2, except multiple sessions can puppet one character, each
# game simultaneously with multiple clients (=sessions).
# 0 - single session per account (if reconnecting, disconnect old session)
# 1 - multiple sessions per account, all sessions share output
# 2 - multiple sessions per account, one session allowed per puppet
# 3 - multiple sessions per account, multiple sessions per puppet (share output)
# session getting the same data.
MULTISESSION_MODE = 0
# The maximum number of characters allowed by the default ooc char-creation command
# Whether we should create a character with the same name as the account when
# a new account is created. Together with AUTO_PUPPET_ON_LOGIN, this mimics
# a legacy MUD, where there is no difference between account and character.
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = True
# Whether an account should auto-puppet the last puppeted puppet when logging in. This
# will only work if the session/puppet combination can be determined (usually
# MULTISESSION_MODE 0 or 1), otherwise, the player will end up OOC. Use
# MULTISESSION_MODE=0, AUTO_CREATE_CHARACTER_WITH_ACCOUNT=True and this value to
# mimic a legacy mud with minimal difference between Account and Character. Disable
# this and AUTO_PUPPET to get a chargen/character select screen on login.
AUTO_PUPPET_ON_LOGIN = True
# How many *different* characters an account can puppet *at the same time*. A value
# above 1 only makes a difference together with MULTISESSION_MODE > 1.
MAX_NR_SIMULTANEOUS_PUPPETS = 1
# The maximum number of characters allowed by be created by the default ooc
# char-creation command. This can be seen as how big of a 'stable' of characters
# an account can have (not how many you can puppet at the same time). Set to
# None for no limit.
MAX_NR_CHARACTERS = 1
# The access hierarchy, in climbing order. A higher permission in the
# hierarchy includes access of all levels below it. Used by the perm()/pperm()

View file

@ -0,0 +1,10 @@
```{eval-rst}
evennia.contrib.tutorials.evadventure.chargen
====================================================
.. automodule:: evennia.contrib.tutorials.evadventure.chargen
:members:
:undoc-members:
:show-inheritance:
```

View file

@ -15,6 +15,7 @@ evennia.contrib.tutorials.evadventure
evennia.contrib.tutorials.evadventure.build_techdemo
evennia.contrib.tutorials.evadventure.build_world
evennia.contrib.tutorials.evadventure.characters
evennia.contrib.tutorials.evadventure.chargen
evennia.contrib.tutorials.evadventure.combat_turnbased
evennia.contrib.tutorials.evadventure.commands
evennia.contrib.tutorials.evadventure.dungeon

View file

@ -1,10 +1,12 @@
"""
EvAdventure character generation
EvAdventure character generation.
"""
from evennia import create_object
from evennia.prototypes.spawner import spawn
from evennia.utils.evmenu import EvMenu
from .characters import EvAdventureCharacter
from .random_tables import chargen_table
from .rules import dice
@ -34,7 +36,7 @@ Your belongings:
"""
class EvAdventureChargenStorage:
class TemporaryCharacterSheet:
"""
This collects all the rules for generating a new character. An instance of this class is used
to pass around the current character state during character generation and also applied to
@ -92,17 +94,15 @@ class EvAdventureChargenStorage:
alignment = dice.roll_random_table("1d20", chargen_table["alignment"])
self.desc = (
f"You are {physique} with a {face} face and {hair} hair, {speech} speech, "
f"and {clothing} clothing. "
f"You were a {background.title()}, but you were {misfortune} and ended up a knave. "
f"You are {virtue} but also {vice}. You are of the {alignment} alignment."
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
f" and {clothing} clothing. You were a {background.title()}, but you were"
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
f" {alignment} alignment."
)
# same for all
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
self.xp = 0
self.level = 1
# random equipment
self.armor = dice.roll_random_table("1d20", chargen_table["armor"])
@ -145,77 +145,47 @@ class EvAdventureChargenStorage:
equipment=", ".join(equipment),
)
def adjust_attribute(self, source_attribute, target_attribute, value):
def apply(self):
"""
Redistribute bonus from one attribute to another. The resulting values
must not be lower than +1 and not above +6.
Args:
source_attribute (enum.Ability): The name of the attribute to deduct bonus from,
like 'strength'
target_attribute (str): The attribute to give the bonus to, like 'dexterity'.
value (int): How much to change. This is always 1 for the current chargen.
Raises:
ValueError: On input error, using invalid values etc.
Notes:
We assume the strings are provided by the chargen, so we don't do
much input validation here, we do make sure we don't overcharge ourselves though.
Once the chargen is complete, call this create and set up the character.
"""
if source_attribute == target_attribute:
return
# we use getattr() to fetch the Ability of e.g. the .strength property etc
source_current = getattr(self, source_attribute.value, 1)
target_current = getattr(self, target_attribute.value, 1)
if source_current - value < 1:
raise ValueError(f"You can't reduce the {source_attribute} bonus below +1.")
if target_current + value > 6:
raise ValueError(f"You can't increase the {target_attribute} bonus above +6.")
# all is good, apply the change.
setattr(self, source_attribute.value, source_current - value)
setattr(self, target_attribute.value, target_current + value)
def apply(self, character):
"""
Once the chargen is complete, call this to transfer all the data to the character
permanently.
"""
character.key = self.name
character.strength = self.strength
character.dexterity = self.dexterity
character.constitution = self.constitution
character.intelligence = self.intelligence
character.wisdom = self.wisdom
character.charisma = self.charisma
character.hp = self.hp
character.level = self.level
character.xp = self.xp
character.db.desc = self.build_desc()
# creating character with given abilities
new_character = create_object(
EvAdventureCharacter,
key=self.name,
attrs=(
("strength", self.strength),
("dexterity", self.dexterity),
("constitution", self.constitution),
("intelligence", self.intelligence),
("wisdom", self.wisdom),
("charisma", self.wisdom),
("hp", self.hp),
("hp_max", self.hp_max),
("desc", self.desc),
),
)
# spawn equipment
if self.weapon:
weapon = spawn(self.weapon)
character.equipment.move(weapon)
new_character.equipment.move(weapon)
if self.shield:
shield = spawn(self.shield)
character.equipment.move(shield)
new_character.equipment.move(shield)
if self.armor:
armor = spawn(self.armor)
character.equipment.move(armor)
new_character.equipment.move(armor)
if self.helmet:
helmet = spawn(self.helmet)
character.equipment.move(helmet)
new_character.equipment.move(helmet)
for item in self.backpack:
item = spawn(item)
character.equipment.store(item)
new_character.equipment.store(item)
return new_character
# chargen menu
@ -266,8 +236,9 @@ def node_change_name(caller, raw_string, **kwargs):
"""
tmp_character = kwargs["tmp_character"]
text = (f"Your current name is |w{tmp_character.name}|n. "
"Enter a new name or leave empty to abort."
text = (
f"Your current name is |w{tmp_character.name}|n. Enter a new name or leave empty to abort."
)
options = {"key": "_default", "goto": (_update_name, kwargs)}
@ -301,6 +272,8 @@ def _swap_abilities(caller, raw_string, **kwargs):
setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1
return "node_chargen", kwargs
@ -339,9 +312,13 @@ def node_apply_character(caller, raw_string, **kwargs):
"""
tmp_character = kwargs["tmp_character"]
tmp_character.apply(caller)
new_character = tmp_character.apply(caller)
caller.msg("Character created!")
caller.account.db._playble_characters = [new_character]
text = "Character created!"
return text, None
def start_chargen(caller, session=None):
@ -357,6 +334,6 @@ def start_chargen(caller, session=None):
}
# this generates all random components of the character
tmp_character = EvAdventureChargenStorage()
tmp_character = TemporaryCharacterSheet()
EvMenu(caller, menutree, startnode="node_chargen", session=session, tmp_character=tmp_character)