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 678ba706c0..98ff3d0e82 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.md @@ -16,12 +16,7 @@ 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. +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: @@ -202,12 +197,9 @@ 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 Abilities _once_. We will use this to know if it has been done or not. @@ -260,9 +252,7 @@ class TemporaryCharacterSheet: ``` -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. +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 @@ -321,8 +311,7 @@ class TemporaryCharacterSheet: 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} A prototype is basically a `dict` describing how the object should be created. Since @@ -334,9 +323,7 @@ Each piece of equipment is an object in in its own right. We will here assume th 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). +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 @@ -374,12 +361,9 @@ def start_chargen(caller, session=None): 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). +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`. +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. @@ -432,12 +416,7 @@ def node_chargen(caller, raw_string, **kwargs): 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! +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 @@ -465,8 +444,7 @@ 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. +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 @@ -518,8 +496,7 @@ 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. +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! @@ -528,9 +505,7 @@ When a user writes anything at this node, the `_update_name` callable will be ca 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. +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 @@ -628,14 +603,9 @@ In `_swap_abilities`, we need to analyze the `raw_string` from the user to see w 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. +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. @@ -658,13 +628,9 @@ node_apply_character(caller, raw_string, **kwargs): 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 `.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. +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 @@ -692,18 +658,13 @@ This is a start point for spinning up the chargen from a command later. ``` -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 -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 characters - how they store their stats, handle their equipment and how to create them. diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Equipment.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Equipment.md index dde2745035..6c12c2f770 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Equipment.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Equipment.md @@ -1,11 +1,8 @@ # Handling Equipment -In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`. -All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items -than you have slot-space for. Also items wielded or worn count towards the slots. +In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`. All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items than you have slot-space for. Also items wielded or worn count towards the slots. -We still need to track what the character is using however: What weapon they have readied affects the damage -they can do. The shield, helmet and armor they use affects their defense. +We still need to track what the character is using however: What weapon they have readied affects the damage they can do. The shield, helmet and armor they use affects their defense. We have already set up the possible 'wear/wield locations' when we defined our Objects [in the previous lesson](./Beginner-Tutorial-Objects.md). This is what we have in `enums.py`: @@ -25,8 +22,7 @@ class WieldLocation(Enum): HEAD = "head" # helmets ``` -Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none). -The BACKPACK is special - it contains any number of items (up to the maximum slot usage). +Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none). The BACKPACK is special - it contains any number of items (up to the maximum slot usage). ## EquipmentHandler that saves @@ -36,12 +32,9 @@ The BACKPACK is special - it contains any number of items (up to the maximum slo If you want to understand more about behind how Evennia uses handlers, there is a [dedicated tutorial](../../Tutorial-Persistent-Handler.md) talking about the principle. ``` -In default Evennia, everything you pick up will end up "inside" your character object (that is, have -you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us' -when we pick them up, but we will add more functionality using an _Equipment handler_. +In default Evennia, everything you pick up will end up "inside" your character object (that is, have you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us' when we pick them up, but we will add more functionality using an _Equipment handler_. -A handler is (for our purposes) an object that sits "on" another entity, containing functionality -for doing one specific thing (managing equipment, in our case). +A handler is (for our purposes) an object that sits "on" another entity, containing functionality for doing one specific thing (managing equipment, in our case). This is the start of our handler: @@ -104,35 +97,23 @@ After reloading the server, the equipment-handler will now be accessible on char character.equipment -The `@lazy_property` works such that it will not load the handler until someone actually tries to -fetch it with `character.equipment`. When that -happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what -enters `__init__` as `.obj` in the `EquipmentHandler` code above. +The `@lazy_property` works such that it will not load the handler until someone actually tries to fetch it with `character.equipment`. When that happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what enters `__init__` as `.obj` in the `EquipmentHandler` code above. -So we now have a handler on the character, and the handler has a back-reference to the character it sits -on. +So we now have a handler on the character, and the handler has a back-reference to the character it sits on. Since the handler itself is just a regular Python object, we need to use the `Character` to store -our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember -them even after reloading. +our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember them even after reloading. -Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named -[Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with +Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named [Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with other Attributes. -Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only -have one item except `WieldLocation.BACKPACK`, which is a list. +Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only have one item except `WieldLocation.BACKPACK`, which is a list. ## Connecting the EquipmentHandler -Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the -object that moves, on the source-location and on its destination. This is the same for all moving things - -whether it's a character moving between rooms or an item being dropping from your hand to the ground. +Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the object that moves, on the source-location and on its destination. This is the same for all moving things - whether it's a character moving between rooms or an item being dropping from your hand to the ground. -We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md), -or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll -find out what hooks Evennia will call. Here `self` is the object being moved from -`source_location` to `destination`: +We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md), or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll find out what hooks Evennia will call. Here `self` is the object being moved from `source_location` to `destination`: 1. `self.at_pre_move(destination)` (abort if return False) @@ -145,9 +126,7 @@ find out what hooks Evennia will call. Here `self` is the object being moved fro 8. `destination.at_object_receive(self, source_location)` 9. `self.at_post_move(source_location)` -All of these hooks can be overridden to customize movement behavior. In this case we are interested in -controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as -them 'carrying' it. We have three good hook-candidates to use for this. +All of these hooks can be overridden to customize movement behavior. In this case we are interested in controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as them 'carrying' it. We have three good hook-candidates to use for this. - `.at_pre_object_receive` - used to check if you can actually pick something up, or if your equipment-store is full. - `.at_object_receive` - used to add the item to the equipmenthandler @@ -187,16 +166,13 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): self.equipment.remove(moved_object) ``` -Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`, -`.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before -we can use this, we need to go actually adding those methods. +Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`, `.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before we can use this, we need to go actually adding those methods. ## Expanding the Equipmenthandler ## `.validate_slot_usage` Let's start with implementing the first method we came up with above, `validate_slot_usage`: - ```python # mygame/evadventure/equipment.py @@ -249,15 +225,11 @@ The `@property` decorator turns a method into a property so you don't need to 'c That is, you can access `.max_slots` instead of `.max_slots()`. In this case, it's just a little less to type. ``` -We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current -slots being in use. Let's figure out how they work. +We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current slots being in use. Let's figure out how they work. ### `.max_slots` -For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we -put this handler on. `getattr` is a Python method for retrieving a named property on an object. -The `Enum` `Ability.CON.value` is the string `Constitution` (check out the -[first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall). +For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we put this handler on. `getattr` is a Python method for retrieving a named property on an object. The `Enum` `Ability.CON.value` is the string `Constitution` (check out the [first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall). So to be clear, @@ -276,24 +248,18 @@ which is the same as doing something like this: your_character.Constitution + 10 ``` -In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there -should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just -return 1. +In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just return 1. ### `.count_slots` -In this helper we use two Python tools - the `sum()` function and a -[list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former -simply adds the values of any iterable together. The latter is a more efficient way to create a list: +In this helper we use two Python tools - the `sum()` function and a [list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former simply adds the values of any iterable together. The latter is a more efficient way to create a list: new_list = [item for item in some_iterable if condition] all_above_5 = [num for num in range(10) if num > 5] # [6, 7, 8, 9] all_below_5 = [num for num in range(10) if num < 5] # [0, 1, 2, 3, 4] -To make it easier to understand, try reading the last line above as "for every number in the range 0-9, -pick all with a value below 5 and make a list of them". You can also embed such comprehensions -directly in a function call like `sum()` without using `[]` around it. +To make it easier to understand, try reading the last line above as "for every number in the range 0-9, pick all with a value below 5 and make a list of them". You can also embed such comprehensions directly in a function call like `sum()` without using `[]` around it. In `count_slots` we have this code: @@ -305,10 +271,7 @@ wield_usage = sum( ) ``` -We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()` -to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can -be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is. -If it is _not_ in the backpack, get its size and add it to the list. Sum over all these +We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()` to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is. If it is _not_ in the backpack, get its size and add it to the list. Sum over all these sizes". A less compact but maybe more readonable way to write this would be: @@ -327,14 +290,11 @@ together. ### Validating slots -With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry. -We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size -would be too much for us. +With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry. We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size would be too much for us. ## `.add` and `.remove` -We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever -it is (even if it was in your hands). +We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever it is (even if it was in your hands). ```python # mygame/evadventure/equipment.py @@ -381,14 +341,11 @@ In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it the item within (if any). If we gave `BACKPACK` as the slot, we empty the backpack and return all items. -Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will -be lost after a server reload. +Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will be lost after a server reload. ## Moving things around -With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment -location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method -on the `EquipmentHandler` to do this: +With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method on the `EquipmentHandler` to do this: ```python # mygame/evadventure/equipment.py @@ -437,9 +394,7 @@ class EquipmentHandler: self._save() ``` -Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where -it goes. So we just need to move the object to that slot, replacing whatever is in that place -from before. Anything we replace goes back to the backpack. +Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where it goes. So we just need to move the object to that slot, replacing whatever is in that place from before. Anything we replace goes back to the backpack. ## Get everything @@ -477,15 +432,12 @@ Here we get all the equipment locations and add their contents together into a l ## Weapon and armor -It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded -and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is -in which wield-slot and to add up armor slots manually every time you need to know. +It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is in which wield-slot and to add up armor slots manually every time you need to know. ```python # mygame/evadventure/equipment.py -from .objects import WeaponEmptyHand from .enums import WieldLocation, Ability # ... @@ -516,14 +468,12 @@ class EquipmentHandler: weapon = slots[WieldLocation.TWO_HANDS] if not weapon: weapon = slots[WieldLocation.WEAPON_HAND] - if not weapon: - weapon = WeaponEmptyHand() + # if we still don't have a weapon, we return None here return weapon ``` -In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head), -and grab their `armor` Attribute. We then `sum()` them all up. +In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head), and grab their `armor` Attribute. We then `sum()` them all up. In `.weapon()`, we simply check which of the possible weapon slots (weapon-hand or two-hands) have something in them. If not we fall back to the 'fake' weapon `WeaponEmptyHand` which is just a 'dummy' @@ -589,11 +539,8 @@ class TestEquipment(BaseEvenniaTest): ## Summary -_Handlers_ are useful for grouping functionality together. Now that we spent our time making the -`EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all -the details for us. As long as we call its methods, the details can be forgotten about. +_Handlers_ are useful for grouping functionality together. Now that we spent our time making the `EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all the details for us. As long as we call its methods, the details can be forgotten about. We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia. -With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character -generation - where players get to make their own character! \ No newline at end of file +With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character generation - where players get to make their own character! \ No newline at end of file diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md index 3b168de8b8..15526db336 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md @@ -7,9 +7,7 @@ Looking at _Knave_'s item lists, we can get some ideas of what we need to track: - `size` - this is how many 'slots' the item uses in the character's inventory. - `value` - a base value if we want to sell or buy the item. -- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be -worn on the head and a shield in the shield hand. Some items can't be used this way at all, but -only belong in the backpack. +- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be worn on the head and a shield in the shield hand. Some items can't be used this way at all, but only belong in the backpack. - `obj_type` - Which 'type' of item this is. @@ -57,8 +55,7 @@ a full set of objects implemented. ```
-We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add -child classes to represent the relevant types: +We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add child classes to represent the relevant types: ```python # mygame/evadventure/objects.py @@ -81,14 +78,42 @@ class EvAdventureObject(DefaultObject): # this can be either a single type or a list of types (for objects able to be # act as multiple). This is used to tag this object during creation. obj_type = ObjType.GEAR - + + # default evennia hooks + def at_object_creation(self): """Called when this object is first created. We convert the .obj_type property to a database tag.""" for obj_type in make_iter(self.obj_type): self.tags.add(self.obj_type.value, category="obj_type") - + + def get_display_header(self, looker, **kwargs): + """The top of the description""" + return "" + + def get_display_desc(self, looker, **kwargs) + """The main display - show object stats""" + return get_obj_stats(self, owner=looker) + + # custom evadventure methods + + def has_obj_type(self, objtype): + """Check if object is of a certain type""" + return objtype.value in make_iter(self.obj_type) + + def at_pre_use(self, *args, **kwargs): + """Called before use. If returning False, can't be used""" + return True + + def use(self, *args, **kwargs): + """Use this object, whatever that means""" + pass + + def post_use(self, *args, **kwargs): + """Always called after use.""" + pass + def get_help(self): """Get any help text for this item""" return "No help for this item" @@ -106,23 +131,13 @@ class EvAdventureObject(DefaultObject): value = 0 ``` -The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to -make a new class for it. We can't change it on the fly because the change would only be in memory and -be lost on next server reload. +The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to make a new class for it. We can't change it on the fly because the change would only be in memory and be lost on next server reload. -Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we -create the object (or later), and the Attributes will remember our changes to that object indefinitely. +Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we create the object (or later), and the Attributes will remember our changes to that object indefinitely. -To make this a little more efficient, we use `autocreate=False`. Normally when you create a -new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at -the same time. So normally, the object would be created along with two Attributes `size` and `value`. -With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as -long as your object has `size=1` no database `Attribute` will be created at all. This saves time and -resources when creating large number of objects. +To make this a little more efficient, we use `autocreate=False`. Normally when you create a new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at the same time. So normally, the object would be created along with two Attributes `size` and `value`. With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as long as your object has `size=1` no database `Attribute` will be created at all. This saves time and resources when creating large number of objects. -The drawback is that since no Attribute is created you can't refer to it -with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query -the database for all objects with `size=1`, since most objects would not yet have an in-database +The drawback is that since no Attribute is created you can't refer to it with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query the database for all objects with `size=1`, since most objects would not yet have an in-database `size` Attribute to search for. In our case, we'll only refer to these properties as `obj.size` etc, and have no need to find @@ -130,11 +145,9 @@ all objects of a particular size. So we should be safe. ### Creating tags in `at_object_creation` -The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is -first created. +The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is first created. -We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the -object like this means you can later efficiently find all objects of a given type (or combination of +We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the object like this means you can later efficiently find all objects of a given type (or combination of types) with Evennia's search functions: ```python @@ -145,9 +158,7 @@ types) with Evennia's search functions: all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type") ``` -We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the -evennia utility library to make sure we don't balk at either. This means you could have a Shield that -is also Magical, for example. +We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the evennia utility library to make sure we don't balk at either. This means you could have a Shield that is also Magical, for example. ## Other object types @@ -176,8 +187,7 @@ class EvAdventureTreasure(EvAdventureObject): ## Consumables -A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used -anymore. An example would be a health potion. +A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used anymore. An example would be a health potion. ```python @@ -192,14 +202,20 @@ class EvAdventureConsumable(EvAdventureObject): value = AttributeProperty(0.25, autocreate=False) uses = AttributeProperty(1, autocreate=False) - def at_pre_use(self, user, *args, **kwargs): + def at_pre_use(self, user, target=None, *args, **kwargs): """Called before using. If returning False, abort use.""" - return uses > 0 - - def at_use(self, user, *args, **kwargs): + if target and user.location != target.location: + user.msg("You are not close enough to the target!") + return False + + if self.uses <= 0: + user.msg(f"|w{self.key} is used up.|n") + return False + + def use(self, user, *args, **kwargs): """Called when using the item""" - pass - + pass + def at_post_use(self. user, *args, **kwargs): """Called after using the item""" # detract a usage, deleting the item if used up. @@ -209,12 +225,13 @@ class EvAdventureConsumable(EvAdventureObject): self.delete() ``` -What exactly each consumable does will vary - we will need to implement children of this class -later, overriding `at_use` with different effects. +In `at_pre_use` we check if we have specified a target (heal someone else or throw a fire bomb at an enemy?), making sure we are in the same location. We also make sure we have `usages` left. In `at_post_use` we make sure to tick off usages. + +What exactly each consumable does will vary - we will need to implement children of this class later, overriding `at_use` with different effects. ## Weapons -All weapons need properties that describe how efficient they are in battle. +All weapons need properties that describe how efficient they are in battle. To 'use' a weapon means to attack with it, so we can let the weapon itself handle all logic around performing an attack. Having the attack code on the weapon also means that if we in the future wanted a weapon doing something special on-attack (for example, a vampiric sword that heals the attacker when hurting the enemy), we could easily add that on the weapon subclass in question without modifying other code. ```python # mygame/evadventure/objects.py @@ -234,18 +251,122 @@ class EvAdventureWeapon(EvAdventureObject): defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False) damage_roll = AttibuteProperty("1d6", autocreate=False) + + +def at_pre_use(self, user, target=None, *args, **kwargs): + if target and user.location != target.location: + # we assume weapons can only be used in the same location + user.msg("You are not close enough to the target!") + return False + + if self.quality <= 0: + user.msg(f"{self.get_display_name(user)} is broken and can't be used!") + return False + return super().at_pre_use(user, target=target, *args, **kwargs) + + def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs): + """When a weapon is used, it attacks an opponent""" + + location = attacker.location + + is_hit, quality, txt = rules.dice.opposed_saving_throw( + attacker, + target, + attack_type=self.attack_type, + defense_type=self.defense_type, + advantage=advantage, + disadvantage=disadvantage, + ) + location.msg_contents( + f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}", + from_obj=attacker, + mapping={target.key: target}, + ) + if is_hit: + # enemy hit, calculate damage + dmg = rules.dice.roll(self.damage_roll) + + if quality is Ability.CRITICAL_SUCCESS: + # doble damage roll for critical success + dmg += rules.dice.roll(self.damage_roll) + message = ( + f" $You() |ycritically|n $conj(hit) $You({target.key}) for |r{dmg}|n damage!" + ) + else: + message = f" $You() $conj(hit) $You({target.key}) for |r{dmg}|n damage!" + + location.msg_contents(message, from_obj=attacker, mapping={target.key: target}) + # call hook + target.at_damage(dmg, attacker=attacker) + + else: + # a miss + message = f" $You() $conj(miss) $You({target.key})." + if quality is Ability.CRITICAL_FAILURE: + message += ".. it's a |rcritical miss!|n, damaging the weapon." + if self.quality is not None: + self.quality -= 1 + location.msg_contents(message, from_obj=attacker, mapping={target.key: target}) + + def at_post_use(self, user, *args, **kwargs): + if self.quality <= 0: + user.msg(f"|r{self.get_display_name(user)} breaks and can no longer be used!") ``` -The `quality` is something we need to track in _Knave_. When getting critical failures on attacks, -a weapon's quality will go down. When it reaches 0, it will break. +In EvAdventure, we will assume all weapons (including bows etc) are used in the same location as the target. Weapons also have a `quality` attribute that gets worn down if the user rolls a critical failure. Once quality is down to 0, the weapon is broken and needs to be repaired. + +The `quality` is something we need to track in _Knave_. When getting critical failures on attacks, a weapon's quality will go down. When it reaches 0, it will break. The attack/defend type tracks how we resolve attacks with the weapon, like `roll + STR vs ARMOR + 10`. +In the `use` method we make use of the `rules` module we [created earlier](Beginner-Tutorial-Rules) to perform all the dice rolls needed to resolve the attack. + +This code requires some additional explanation: +```python +location.msg_contents( + f"$You() $conj(attack) $you({target.key}) with {self.key}: {txt}", + from_obj=attacker, + mapping={target.key: target}, +) +``` +`location.msg_contents` sends a message to everyone in `location`. Since people will usually notice if you swing a sword at somone, this makes sense to tell people about. This message should however look _different_ depending on who sees it. + +I should see: + + You attack Grendel with sword: