From e8d08ad5974086ac0c7f8fbbca20667af7d8700c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 17 Sep 2022 18:30:20 +0200 Subject: [PATCH] Update docs --- docs/source/Coding/Changelog.md | 12 +- docs/source/Components/Locks.md | 2 +- docs/source/Contribs/Contrib-Clothing.md | 2 +- docs/source/Contribs/Contrib-Menu-Login.md | 2 +- .../Part3/Beginner-Tutorial-Characters.md | 4 +- .../Part3/Beginner-Tutorial-Chargen.md | 656 +++++++++++++++++- .../Part3/Beginner-Tutorial-Rooms.md | 5 + docs/source/Setup/Settings-Default.md | 40 +- ...a.contrib.tutorials.evadventure.chargen.md | 10 + .../evennia.contrib.tutorials.evadventure.md | 1 + .../contrib/tutorials/evadventure/chargen.py | 115 ++- 11 files changed, 751 insertions(+), 98 deletions(-) create mode 100644 docs/source/api/evennia.contrib.tutorials.evadventure.chargen.md diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 103053b325..5b5f2c62b9 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -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 diff --git a/docs/source/Components/Locks.md b/docs/source/Components/Locks.md index 42728c7e91..78e163b36b 100644 --- a/docs/source/Components/Locks.md +++ b/docs/source/Components/Locks.md @@ -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) diff --git a/docs/source/Contribs/Contrib-Clothing.md b/docs/source/Contribs/Contrib-Clothing.md index acaa12ca39..21addd6ecb 100644 --- a/docs/source/Contribs/Contrib-Clothing.md +++ b/docs/source/Contribs/Contrib-Clothing.md @@ -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 diff --git a/docs/source/Contribs/Contrib-Menu-Login.md b/docs/source/Contribs/Contrib-Menu-Login.md index 927210e3cf..9c47216b6d 100644 --- a/docs/source/Contribs/Contrib-Menu-Login.md +++ b/docs/source/Contribs/Contrib-Menu-Login.md @@ -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. diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md index a1f16ff86f..ff63cd845d 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md @@ -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 diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md index a54603218d..6c299684bd 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md @@ -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. \ No newline at end of file diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md index e69de29bb2..76326866fa 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md @@ -0,0 +1,5 @@ +# In-game Rooms + +```{warning} +This part of the Beginner tutorial is still being developed. +``` \ No newline at end of file diff --git a/docs/source/Setup/Settings-Default.md b/docs/source/Setup/Settings-Default.md index baa2ee3c9e..ab9ecd9922 100644 --- a/docs/source/Setup/Settings-Default.md +++ b/docs/source/Setup/Settings-Default.md @@ -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() diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.chargen.md b/docs/source/api/evennia.contrib.tutorials.evadventure.chargen.md new file mode 100644 index 0000000000..41d36c79e2 --- /dev/null +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.chargen.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.tutorials.evadventure.chargen +==================================================== + +.. automodule:: evennia.contrib.tutorials.evadventure.chargen + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.md b/docs/source/api/evennia.contrib.tutorials.evadventure.md index 678a28c65d..d285637257 100644 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.md +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.md @@ -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 diff --git a/evennia/contrib/tutorials/evadventure/chargen.py b/evennia/contrib/tutorials/evadventure/chargen.py index 0df688ce36..25667c1924 100644 --- a/evennia/contrib/tutorials/evadventure/chargen.py +++ b/evennia/contrib/tutorials/evadventure/chargen.py @@ -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)