Fix merge conflict

This commit is contained in:
Griatch 2024-01-08 20:37:53 +01:00
commit 0b234c792c
8 changed files with 189 additions and 180 deletions

View file

@ -63,7 +63,7 @@ This holds the server logs. When you do `evennia --log`, the evennia program is
This contains all configuration files of the Evennia server. These are regular Python modules which means that they must be extended with valid Python. You can also add logic to them if you wanted to. This contains all configuration files of the Evennia server. These are regular Python modules which means that they must be extended with valid Python. You can also add logic to them if you wanted to.
Common for the settings is that you generally will never them directly via their python-path; instead Evennia knows where they are and will read them to configure itself at startup. Common for the settings is that you generally will never import them directly via their python-path; instead Evennia knows where they are and will read them to configure itself at startup.
- `settings.py` - this is by far the most important file. It's nearly empty by default, rather you - `settings.py` - this is by far the most important file. It's nearly empty by default, rather you
are expected to copy&paste the changes you need from [evennia/default_settings.py](../../../Setup/Settings-Default.md). The default settings file is extensively documented. Importing/accessing the values in the settings file is done in a special way, like this: are expected to copy&paste the changes you need from [evennia/default_settings.py](../../../Setup/Settings-Default.md). The default settings file is extensively documented. Importing/accessing the values in the settings file is done in a special way, like this:
@ -83,7 +83,7 @@ Common for the settings is that you generally will never them directly via their
- `inlinefuncs.py` - [Inlinefuncs](../../../Concepts/Inline-Functions.md) are optional and limited 'functions' that can be embedded in any strings being sent to a player. They are written as `$funcname(args)` and are used to customize the output depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!` would show every player seeing that string the time given in their own time zone. The functions added to this module will become new inlinefuncs in the game. See also the [FuncParser](../../../Components/FuncParser.md). - `inlinefuncs.py` - [Inlinefuncs](../../../Concepts/Inline-Functions.md) are optional and limited 'functions' that can be embedded in any strings being sent to a player. They are written as `$funcname(args)` and are used to customize the output depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!` would show every player seeing that string the time given in their own time zone. The functions added to this module will become new inlinefuncs in the game. See also the [FuncParser](../../../Components/FuncParser.md).
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an [Inputfunc](InputFuncs) that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like button-presses or the request to update a health-bar. While most common cases are already covered, this is where one adds new functions to process new types of input. - `inputfucs.py` - When a command like `look` is received by the server, it is handled by an [Inputfunc](InputFuncs) that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like button-presses or the request to update a health-bar. While most common cases are already covered, this is where one adds new functions to process new types of input.
- `lockfuncs.py` - [Locks](../../../Components/Locks.md) and their component _LockFuncs_ restrict access to things in-game. Lock funcs are used in a mini-language to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying a given item, is bleeding or has a certain skill value. New functions added in this modules will become available for use in lock definitions. - `lockfuncs.py` - [Locks](../../../Components/Locks.md) and their component _LockFuncs_ restrict access to things in-game. Lock funcs are used in a mini-language to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying a given item, is bleeding or has a certain skill value. New functions added in this modules will become available for use in lock definitions.
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles the dynamic information automatically, this is were you set up the meta-info about your game, such as its theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory. - `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles the dynamic information automatically, this is where you set up the meta-info about your game, such as its theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place to add them. - `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place to add them.
- `server_services_plugins.py` - This allows to override internal server connection protocols. - `server_services_plugins.py` - This allows to override internal server connection protocols.
- `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts. - `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts.

View file

@ -188,7 +188,7 @@ class Sittable(Object):
- **Line 15**: We grab the `adjective` Attribute. Using `self.db.adjective or "on"` here means that if the Attribute is not set (is `None`/falsy) the default "on" string will be assumed. - **Line 15**: We grab the `adjective` Attribute. Using `self.db.adjective or "on"` here means that if the Attribute is not set (is `None`/falsy) the default "on" string will be assumed.
- **Lines 19,22,27,39, and 43**: We use this adjective to modify the return text we see. - **Lines 19,22,27,39, and 43**: We use this adjective to modify the return text we see.
`reload` the server. An advantage of using Attributes like this is that they can be modified on the fly, in-game. Let's look at a builder could use this by normal building commands (no need for `py`): `reload` the server. An advantage of using Attributes like this is that they can be modified on the fly, in-game. Let's look at how a builder could use this with normal building commands (no need for `py`):
``` ```
> set armchair/adjective = in > set armchair/adjective = in

View file

@ -430,7 +430,7 @@ commands).
Hello World Hello World
[py mode - quit() to exit] [py mode - quit() to exit]
Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after the `>>>`). For brevity in this tutorual we'll turn the echo off. First exit `py` and then start again with the `/noecho` flag. Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after the `>>>`). For brevity in this tutorial we'll turn the echo off. First exit `py` and then start again with the `/noecho` flag.
> quit() > quit()
Closing the Python console. Closing the Python console.
@ -465,7 +465,7 @@ Let's try to define a function:
Some important things above: Some important things above:
- Definining a function with `def` means we are starting a new code block. Python works so that you mark the content - Defining a function with `def` means we are starting a new code block. Python works so that you mark the content
of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order
for Python to know it's part of the function body. for Python to know it's part of the function body.
- We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just - We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just

View file

@ -308,7 +308,7 @@ For this tutorial, we will show how to add a simple state-machine AI for monster
### Are NPCs and mobs different entities? How do they differ? ### Are NPCs and mobs different entities? How do they differ?
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally different these days it's often easier to just make NPCs and mobs essentially the same thing. "Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally different. These days it's often easier to just make NPCs and mobs essentially the same thing.
**EvAdventure Answer** **EvAdventure Answer**

View file

@ -1,15 +1,15 @@
# Character Generation # Character Generation
In previous lessons we have established how a character looks. Now we need to give the player a chance to create one. In previous lessons we have established how a character looks. Now we need to give the player a chance to create one.
## How it will work ## How it will work
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. 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 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 We do this by editing `mygame/server/conf/settings.py` and adding the line
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False 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. 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.
@ -18,7 +18,7 @@ compact while still showing the basic idea. What we will create is a menu lookin
``` ```
Silas Silas
STR +1 STR +1
DEX +2 DEX +2
@ -28,32 +28,32 @@ WIS +1
CHA +2 CHA +2
You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing. You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing.
You were a herbalist, but you were pursued and ended up a knave. You are honest but also You were a herbalist, but you were pursued and ended up a knave. You are honest but also
suspicious. You are of the neutral alignment. suspicious. You are of the neutral alignment.
Your belongings: Your belongings:
Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch, Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch,
tinderbox, chisel, whistle tinderbox, chisel, whistle
---------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------
1. Change your name 1. Change your name
2. Swap two of your ability scores (once) 2. Swap two of your ability scores (once)
3. Accept and create character 3. Accept and create character
``` ```
If you select 1, you get a new menu node: If you select 1, you get a new menu node:
``` ```
Your current name is Silas. Enter a new name or leave empty to abort. Your current name is Silas. Enter a new name or leave empty to abort.
----------------------------------------------------------------------------------------- -----------------------------------------------------------------------------------------
``` ```
You can now enter a new name. When pressing return you'll get back to the first menu node You can now enter a new name. When pressing return you'll get back to the first menu node
showing your character, now with the new name. showing your character, now with the new name.
If you select 2, you go to another menu node: If you select 2, you go to another menu node:
``` ```
Your current abilities: Your current abilities:
STR +1 STR +1
DEX +2 DEX +2
@ -70,25 +70,25 @@ To swap the values of e.g. STR and INT, write 'STR INT'. Empty to abort.
``` ```
If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back 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 enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back 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, the character will be created and you'll leave the menu; If you finally select the `Accept and create character` option, the character will be created and you'll leave the menu;
Character was created! Character was created!
## Random tables ## Random tables
```{sidebar} ```{sidebar}
Full Knave random tables are found in Full Knave random tables are found in
[evennia/contrib/tutorials/evadventure/random_tables.py](../../../api/evennia.contrib.tutorials.evadventure.random_tables.md). [evennia/contrib/tutorials/evadventure/random_tables.py](../../../api/evennia.contrib.tutorials.evadventure.random_tables.md).
``` ```
> Make a new module `mygame/evadventure/random_tables.py`. > 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 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 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. [Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
``` ```
# in mygame/evadventure/random_tables.py # in mygame/evadventure/random_tables.py
chargen_tables = { chargen_tables = {
"physique": [ "physique": [
@ -97,42 +97,42 @@ chargen_tables = {
"statuesque", "stout", "tiny", "towering", "willowy", "wiry", "statuesque", "stout", "tiny", "towering", "willowy", "wiry",
], ],
"face": [ "face": [
"bloated", "blunt", "bony", # ... "bloated", "blunt", "bony", # ...
], # ... ], # ...
} }
``` ```
The tables are just copied from the _Knave_ rules. We group the aspects in a dict 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 `character_generation` to separate chargen-only tables from other random tables we'll also
keep in here. keep in here.
## Storing state of the menu ## Storing state of the menu
```{sidebar} ```{sidebar}
There is a full implementation of the chargen in There is a full implementation of the chargen in
[evennia/contrib/tutorials/evadventure/chargen.py](../../../api/evennia.contrib.tutorials.evadventure.chargen.md). [evennia/contrib/tutorials/evadventure/chargen.py](../../../api/evennia.contrib.tutorials.evadventure.chargen.md).
``` ```
> create a new module `mygame/evadventure/chargen.py`. > create a new module `mygame/evadventure/chargen.py`.
During character generation we will need an entity to store/retain the changes, like a During character generation we will need an entity to store/retain the changes, like a
'temporary character sheet'. 'temporary character sheet'.
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
from .random_tables import chargen_tables from .random_tables import chargen_tables
from .rules import dice from .rules import dice
class TemporaryCharacterSheet: class TemporaryCharacterSheet:
def _random_ability(self): def _random_ability(self):
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6")) return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
def __init__(self): def __init__(self):
self.ability_changes = 0 # how many times we tried swap abilities self.ability_changes = 0 # how many times we tried swap abilities
# name will likely be modified later # name will likely be modified later
self.name = dice.roll_random_table("1d282", chargen_tables["name"]) self.name = dice.roll_random_table("1d282", chargen_tables["name"])
@ -164,7 +164,7 @@ class TemporaryCharacterSheet:
f" {alignment} alignment." f" {alignment} alignment."
) )
# #
self.hp_max = max(5, dice.roll("1d8")) self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max self.hp = self.hp_max
self.xp = 0 self.xp = 0
@ -189,19 +189,19 @@ class TemporaryCharacterSheet:
] ]
``` ```
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. 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). 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 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. Abilities _once_. We will use this to know if it has been done or not.
### Showing the sheet ### Showing the sheet
Now that we have our temporary character sheet, we should make it easy to visualize it. Now that we have our temporary character sheet, we should make it easy to visualize it.
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
_TEMP_SHEET = """ _TEMP_SHEET = """
{name} {name}
@ -214,15 +214,15 @@ WIS +{wisdom}
CHA +{charisma} CHA +{charisma}
{description} {description}
Your belongings: Your belongings:
{equipment} {equipment}
""" """
class TemporaryCharacterSheet: class TemporaryCharacterSheet:
# ... # ...
def show_sheet(self): def show_sheet(self):
equipment = ( equipment = (
str(item) str(item)
@ -248,22 +248,22 @@ The new `show_sheet` method collect the data from the temporary sheet and return
### Apply character ### Apply character
Once we are happy with our character, we need to actually create it with the stats we chose. Once we are happy with our character, we need to actually create it with the stats we chose.
This is a bit more involved. This is a bit more involved.
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
# ... # ...
from .characters import EvAdventureCharacter from .characters import EvAdventureCharacter
from evennia import create_object from evennia import create_object
from evennia.prototypes.spawner import spawn from evennia.prototypes.spawner import spawn
class TemporaryCharacterSheet: class TemporaryCharacterSheet:
# ... # ...
def apply(self): def apply(self):
# create character object with given abilities # create character object with given abilities
@ -279,58 +279,58 @@ class TemporaryCharacterSheet:
("charisma", self.wisdom), ("charisma", self.wisdom),
("hp", self.hp), ("hp", self.hp),
("hp_max", self.hp_max), ("hp_max", self.hp_max),
("desc", self.desc), ("desc", self.desc),
), ),
) )
# spawn equipment (will require prototypes created before it works) # spawn equipment (will require prototypes created before it works)
if self.weapon: if self.weapon:
weapon = spawn(self.weapon) weapon = spawn(self.weapon)
new_character.equipment.move(weapon) new_character.equipment.move(weapon)
if self.shield: if self.shield:
shield = spawn(self.shield) shield = spawn(self.shield)
new_character.equipment.move(shield) new_character.equipment.move(shield)
if self.armor: if self.armor:
armor = spawn(self.armor) armor = spawn(self.armor)
new_character.equipment.move(armor) new_character.equipment.move(armor)
if self.helmet: if self.helmet:
helmet = spawn(self.helmet) helmet = spawn(self.helmet)
new_character.equipment.move(helmet) new_character.equipment.move(helmet)
for item in self.backpack: for item in self.backpack:
item = spawn(item) item = spawn(item)
new_character.equipment.store(item) new_character.equipment.store(item)
return new_character 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. 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} ```{sidebar}
A prototype is basically a `dict` describing how the object should be created. Since 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) it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
things from those prototypes. things from those prototypes.
``` ```
Each piece of equipment is an object in in its own right. We will here assume that all game 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 items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
armor" etc. 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). 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 ## Initializing EvMenu
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
[EvMenu](../../../Components/EvMenu.md). [EvMenu](../../../Components/EvMenu.md).
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
from evennia import EvMenu from evennia import EvMenu
# ... # ...
# chargen menu # chargen menu
# this goes to the bottom of the module # this goes to the bottom of the module
@ -346,7 +346,13 @@ def start_chargen(caller, session=None):
# this generates all random components of the character # this generates all random components of the character
tmp_character = TemporaryCharacterSheet() tmp_character = TemporaryCharacterSheet()
EvMenu(caller, menutree, session=session, startnode="node_chargen", startnode_input=("", {"tmp_character": tmp_character})) EvMenu(
caller,
menutree,
session=session,
startnode="node_chargen",
startnode_input=("", {"tmp_character": tmp_character}),
)
``` ```
@ -354,25 +360,25 @@ This first function is what we will call from elsewhere (for example from a cust
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). 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 feed all this into `EvMenu`. The `startnode` and `startnode_input` keywords makes sure to enter the menu at the "node_chargen" node (which we will create below) and call it with with the provided arguments. We create a `TemporaryCharacterSheet` and feed all this into `EvMenu`. The `startnode` and `startnode_input` keywords makes sure to enter the menu at the "node_chargen" node (which we will create below) and call it with with the provided arguments.
The moment this happens, the user will be in the menu, there are no further steps needed. 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. The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump between.
## Main Node: Choosing what to do ## 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 This is the first menu node. It will act as a central hub, from which one can choose different
actions. actions.
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
# ... # ...
# at the end of the module, but before the `start_chargen` function # at the end of the module, but before the `start_chargen` function
def node_chargen(caller, raw_string, **kwargs): def node_chargen(caller, raw_string, **kwargs):
tmp_character = kwargs["tmp_character"] tmp_character = kwargs["tmp_character"]
@ -380,20 +386,20 @@ def node_chargen(caller, raw_string, **kwargs):
options = [ options = [
{ {
"desc": "Change your name", "desc": "Change your name",
"goto": ("node_change_name", kwargs) "goto": ("node_change_name", kwargs)
} }
] ]
if tmp_character.ability_changes <= 0: if tmp_character.ability_changes <= 0:
options.append( options.append(
{ {
"desc": "Swap two of your ability scores (once)", "desc": "Swap two of your ability scores (once)",
"goto": ("node_swap_abilities", kwargs), "goto": ("node_swap_abilities", kwargs),
} }
) )
options.append( options.append(
{ {
"desc": "Accept and create character", "desc": "Accept and create character",
"goto": ("node_apply_character", kwargs) "goto": ("node_apply_character", kwargs)
}, },
) )
@ -403,42 +409,42 @@ def node_chargen(caller, raw_string, **kwargs):
# ... # ...
``` ```
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While 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. 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! 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!
> Note that we created the menu with the `startnode="node_chargen"` and the tuple `startnode_input=("", {"tmp_character": tmp_character})`. Assuming we register the above function as the node `"node_chargen"`, it will start out being called as `node_chargen(caller, "", tmp_character=tmp_character)` (EvMenu will add `caller` on its own). This is one way we can pass outside data into the menu as it starts. > Note that above we created the menu with the `startnode="node_chargen"` and the tuple `startnode_input=("", {"tmp_character": tmp_character})`. Assuming we register the above function as the node `"node_chargen"`, it will start out being called as `node_chargen(caller, "", tmp_character=tmp_character)` (EvMenu will add `caller` on its own). This is one way we can pass outside data into the menu as it starts.
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will 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 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. 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: 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 ```python
{ {
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number "key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
"desc": "text to describe what happens when selecting option",. "desc": "text to describe what happens when selecting option",.
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable) "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 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 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 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. 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. 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`, 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` `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. 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. 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 ## Node: Changing your name
This is where you end up if you opted to change your name in `node_chargen`. This is where you end up if you opted to change your name in `node_chargen`.
@ -447,11 +453,11 @@ This is where you end up if you opted to change your name in `node_chargen`.
# ... # ...
# after previous node # after previous node
def _update_name(caller, raw_string, **kwargs): def _update_name(caller, raw_string, **kwargs):
""" """
Used by node_change_name below to check what user Used by node_change_name below to check what user
entered and update the name if appropriate. entered and update the name if appropriate.
""" """
@ -471,38 +477,38 @@ def node_change_name(caller, raw_string, **kwargs):
text = ( text = (
f"Your current name is |w{tmp_character.name}|n. " f"Your current name is |w{tmp_character.name}|n. "
"Enter a new name or leave empty to abort." "Enter a new name or leave empty to abort."
) )
options = { options = {
"key": "_default", "key": "_default",
"goto": (_update_name, kwargs) "goto": (_update_name, kwargs)
} }
return text, options return text, options
``` ```
There are two functions here - the menu node itself (`node_change_name`) and a 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. 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 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 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. 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 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! 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 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 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. 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. 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 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 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`. 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 > Hint: If returning `None` from a goto-callable, you will always return to the last node you
> were at. > were at.
## Node: Swapping Abilities around ## Node: Swapping Abilities around
@ -510,11 +516,11 @@ loose our temporary character sheet. Here we will always go back to the `node_ch
You get here by selecting the second option from the `node_chargen` node. You get here by selecting the second option from the `node_chargen` node.
```python ```python
# in mygame/evadventure/chargen.py # in mygame/evadventure/chargen.py
# ... # ...
# after previous node # after previous node
_ABILITIES = { _ABILITIES = {
"STR": "strength", "STR": "strength",
@ -542,24 +548,24 @@ def _swap_abilities(caller, raw_string, **kwargs):
if abi1 not in _ABILITIES or abi2 not in _ABILITIES: if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
caller.msg("Not a familiar set of abilites.") caller.msg("Not a familiar set of abilites.")
return None, kwargs return None, kwargs
# looks okay = swap values. We need to convert STR to strength etc # looks okay = swap values. We need to convert STR to strength etc
tmp_character = kwargs["tmp_character"] tmp_character = kwargs["tmp_character"]
abi1 = _ABILITIES[abi1] abi1 = _ABILITIES[abi1]
abi2 = _ABILITIES[abi2] abi2 = _ABILITIES[abi2]
abival1 = getattr(tmp_character, abi1) abival1 = getattr(tmp_character, abi1)
abival2 = getattr(tmp_character, abi2) abival2 = getattr(tmp_character, abi2)
setattr(tmp_character, abi1, abival2) setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1) setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1 tmp_character.ability_changes += 1
return "node_chargen", kwargs return "node_chargen", kwargs
def node_swap_abilities(caller, raw_string, **kwargs): def node_swap_abilities(caller, raw_string, **kwargs):
""" """
One is allowed to swap the values of two abilities around, once. One is allowed to swap the values of two abilities around, once.
""" """
@ -582,81 +588,84 @@ To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
""" """
options = {"key": "_default", "goto": (_swap_abilities, kwargs)} options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
return text, options return text, options
``` ```
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and 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 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. 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 In `_swap_abilities`, we need to analyze the `raw_string` from the user to see what they
want to do. want to do.
Most code in the helper is validating the user didn't enter nonsense. If they did, 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. 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. 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. Finally, we return to `node_chargen` again.
## Node: Creating the Character ## Node: Creating the Character
We get here from the main node by opting to finish chargen. We get here from the main node by opting to finish chargen.
```python ```python
node_apply_character(caller, raw_string, **kwargs): node_apply_character(caller, raw_string, **kwargs):
""" """
End chargen and create the character. We will also puppet it. End chargen and create the character. We will also puppet it.
""" """
tmp_character = kwargs["tmp_character"] tmp_character = kwargs["tmp_character"]
new_character = tmp_character.apply(caller) new_character = tmp_character.apply(caller)
caller.account.add_character(new_character) caller.account.add_character(new_character)
text = "Character created!" text = "Character created!"
return text, None 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. When entering the node, we will take the Temporary character sheet and use its `.apply` 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. 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 ## Tying the nodes together
```python ```python
def start_chargen(caller, session=None): def start_chargen(caller, session=None):
""" """
This is a start point for spinning up the chargen from a command later. This is a start point for spinning up the chargen from a command later.
""" """
menutree = { # <----- can now add this! menutree = { # <----- can now add this!
"node_chargen": node_chargen, "node_chargen": node_chargen,
"node_change_name": node_change_name, "node_change_name": node_change_name,
"node_swap_abilities": node_swap_abilities, "node_swap_abilities": node_swap_abilities,
"node_apply_character": node_apply_character "node_apply_character": node_apply_character,
} }
# this generates all random components of the character # this generates all random components of the character
tmp_character = TemporaryCharacterSheet() tmp_character = TemporaryCharacterSheet()
EvMenu(caller, menutree, session=session, EvMenu(
startnode="node_chargen", # <----- caller,
tmp_character=tmp_character) menutree,
session=session,
startnode="node_chargen", # <-- make sure it's set!
startnode_input=("", {"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). 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. 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 ## 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. 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 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. 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. In the next lesson we'll address how EvAdventure _Rooms_ work.

View file

@ -523,7 +523,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
self.equipment.add(moved_object) self.equipment.add(moved_object)
``` ```
At this means that the equipmenthandler will check the NPC, and since it's not a equippable thing, an `EquipmentError` will be raised, failing the creation. Since we want to be able to create npcs etc easily, we will handle this error with a `try...except` statement like so: This means that the equipmenthandler will check the NPC, and since it's not a equippable thing, an `EquipmentError` will be raised, failing the creation. Since we want to be able to create npcs etc easily, we will handle this error with a `try...except` statement like so:
```python ```python
# mygame/evadventure/characters.py # mygame/evadventure/characters.py

View file

@ -111,7 +111,7 @@ You may also begin viewing the real-time log immediately by adding `-l/--log` to
## Server Configuration ## Server Configuration
Your server's configuration file is `mygame/server/settings.py`. It's empty by default. Copy and paste **only** the settings you want/need from the [default settings file](./Settings-Default.md) to your server's `settings.py`. See the [Settings](./Settings.md) documentation for more information before configuring your server at this time. Your server's configuration file is `mygame/server/conf/settings.py`. It's empty by default. Copy and paste **only** the settings you want/need from the [default settings file](./Settings-Default.md) to your server's `settings.py`. See the [Settings](./Settings.md) documentation for more information before configuring your server at this time.
## Register with the Evennia Game Index (optional) ## Register with the Evennia Game Index (optional)

View file

@ -21,7 +21,7 @@ to give you a menu with options.
## Starting Evennia ## Starting Evennia
Evennia consists of two components, the Evennia [Portal and Server](../Components/Portal-And-Server.md). Briefly, the *Server* is what is running the mud. It handles all game-specific things but doesn't care exactly how players connect, only that they have. The *Portal* is a gateay to which players connect. It knows everything about telnet, ssh, webclient protocols etc but very little about the game. Both are required for a functioning game. Evennia consists of two components, the Evennia [Portal and Server](../Components/Portal-And-Server.md). Briefly, the *Server* is what is running the mud. It handles all game-specific things but doesn't care exactly how players connect, only that they have. The *Portal* is a gateway to which players connect. It knows everything about telnet, ssh, webclient protocols etc but very little about the game. Both are required for a functioning game.
evennia start evennia start