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: + +Others should see + + Beowulf attacks Grendel with sword: + +And Grendel should see + + Beowulf attacks you with sword: + +We provide the following string to `msg_contents`: +```python +f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}" +``` + +The `{...}` are normal f-string formatting markers like those we have used before. The `$func(...)` bits are [Evennnia FuncParser](FuncParser) function calls. FuncParser calls are executed as functions and the result replaces their position in the string. As this string is parsed by Evennia, this is what happens: + +First the f-string markers are replaced, so that we get this: + +```python +"$You() $cobj(attack) $you(Grendel) with sword: \n rolled 8 on d20 ..." +``` + +Next the funcparser functions are run: + + - `$You()` becomes the name or `You` depending on if the string is to be sent to that object or not. It uses the `from_obj=` kwarg to the `msg_contents` method to know this. Since `msg_contents=attacker` , this becomes `You` or `Beowulf` in this example. + - `$you(Grendel)` looks for the `mapping=` kwarg to `msg_contents` to determine who should be addressed here. If will replace this with the display name or the lowercase `you`. We have added `mapping={target.key: target}` - that is `{"Grendel": }`. So this will become `you` or `Grendel` depending on who sees the string. +- `$conj(attack)` _conjugates_ the verb depending on who sees it. The result will be `You attack ...` or `Beowulf attacks` (note the extra `s`). + +A few funcparser calls compacts all these points of view into one string! + ## Magic -In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both -hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon' -that is also a 'consumable' of sorts. +In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon' that is also a 'consumable' of sorts. ```python @@ -281,25 +402,16 @@ class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable): self.uses = 1 ``` -We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses` -again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `at_use` methods -are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted -when it runs out of uses. +We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses` again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `use` methods are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted when it runs out of uses. -We add a little convenience method `refresh` - we should call this when the character rests, to -make the runestone active again. +We add a little convenience method `refresh` - we should call this when the character rests, to make the runestone active again. -Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this -base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot -of custom code. +Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot of custom code. ## Armor -Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the -defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know, -defending is always `bonus + 10`, so the result will be the same - this means -we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case. +Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know, defending is always `bonus + 10`, so the result will be the same - this means we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case. `` ```python @@ -327,34 +439,42 @@ class EvAdventureHelmet(EvAdventureArmor): ## Your Bare hands -This is a 'dummy' object that is not stored in the database. We will use this in the upcoming -[Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing' -in your hands. This way we don't need to add any special case for this. +When we don't have any weapons, we'll be using our bare fists to fight. + +We will use this in the upcoming [Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing' in your hands. This way we don't need to add any special case for this. ```python -class WeaponEmptyHand: +# mygame/evadventure/objects.py + +from evennia import search_object, create_object + +# ... + +class WeaponBareHands(EvAdventureWeapon) obj_type = ObjType.WEAPON - key = "Empty Fists" inventory_use_slot = WieldLocation.WEAPON_HAND attack_type = Ability.STR defense_type = Ability.ARMOR damage_roll = "1d4" - quality = 100000 # let's assume fists are always available ... - - def __repr__(self): - return "" + quality = None # let's assume fists are indestructible ... + + +# common to put this at the bottom of module +BARE_HANDS = search_object("Bare hands", typeclass=WeaponBareHands).first() +if not BARE_HANDS: + BARE_HANDS = create_object(WeaponBareHands, key="Bare hands") + ``` +Since everyone's empty hands are the same (in our game), we create _one_ `Bare hands` weapon object that everyone shares. We do this by searching for the object with `search_object` (the `.first()` means we grab the first one even if we should by accident have created multiple hands, see [The Django querying tutorial](Beginner-Tutorial-Django-queries) for more info). If we find none, we create it. This way the `BARE_HANDS` object can be used by everyone (we just need to import `objects.BARE_HANDS`). + + ## Testing and Extra credits -Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier? -We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game. +Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier? We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game. -Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data -from the object it receives. +Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data from the object it receives. -When you change this function you must also update the related unit test - so your existing test becomes a -nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types -to `get_obj_stats`. +When you change this function you must also update the related unit test - so your existing test becomes a nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types to `get_obj_stats`. Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats). \ 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 76326866fa..52feb81932 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md @@ -1,5 +1,212 @@ # In-game Rooms -```{warning} -This part of the Beginner tutorial is still being developed. -``` \ No newline at end of file +A _room_ describes a specific location in the game world. Being an abstract concept, it can represent any area of game content that is convenient to group together. In this lesson we will also create a small in-game automap. + +In EvAdventure, we will have two main types of rooms: +- Normal, above-ground rooms. Based on a fixed map, these will be created once and then don't change. We'll cover them in this lesson. +- Dungeon rooms - these will be examples of _procedurally generated_ rooms, created on the fly as the players explore the underworld. Being subclasses of the normal room, we'll get to them in the [Dungeon generation lesson](Beginner-Tutorial-Dungeon). + +## The base room + +Our Evadventure-rooms need some extra functionality +- We need to know if we can do combat in them, if PvP is ok and if you can die in the room. +- We want to show a little _map_ as part of the room description. For this to work we need to remember to always use cardinal directions to connect rooms (north, east etc). + +> Create a new module `evadventure/rooms.py`. + +```python +# in evadventure/rooms.py + +from evennia import AttributeProperty, DefaultRoom + +class EvAdventureRoom(DefaultRoom): + """ + Simple room supporting some EvAdventure-specifics. + + """ + + allow_combat = AttributeProperty(False, autocreate=False) + allow_pvp = AttributeProperty(False, autocreate=False) + allow_death = AttributeProperty(False, autocreate=False) + +``` + +Our EvadventureRoom is very simple. We create three Attributes that defines if combat is allowed in the room, and if so if pvp is allowed and finally if death is allowed. Later on we must make sure our combat systems honors these values. This allows us to create 'safe' training rooms and the like. + +That's really all there we _really_ need for the basic room. It'd make for a very short lesson though, so let's add a map too. + + +## PvP room + +Here's a room that allows non-lethal PvP (sparring): + +```python + +# in evadventure/rooms.py + +class EvAdventurePvPRoom(EvAdventureRoom): + """ + Room where PvP can happen, but noone gets killed. + + """ + + allow_combat = AttributeProperty(True, autocreate=False) + allow_pvp = AttributeProperty(True, autocreate=False) + + def get_display_footer(self, looker, **kwargs): + """ + Customize footer of description. + """ + return "|yNon-lethal PvP combat is allowed here!|n" +``` + +The return of `get_display_footer` will show after the [main room description](Objects#changing-an-objects-appearance), showing that the room is a sparring room. This means that when a player drops to 0 HP, they will lose the combat, but don't stand any risk of dying (weapons wear out normally during sparring though). + +## Adding a room map + +We want a dynamic map that visualizes the exits you can use at any moment. Here's how our room will display: + +```{shell} + o o o + \|/ + o-@-o + | + o +The crossroads +A place where many roads meet. +Exits: north, norteast, south, west, and nortwest +``` + +> Documentation does not show ansi colors. + +Let's expand the base `EvAdventureRoom` with the map. + +```python +# in evadventyre/rooms.py + +from copy import deepcopy +from evennia import DefaultCharacter +from evennia.utils.utils import inherits_from + +CHAR_SYMBOL = "|w@|n" +CHAR_ALT_SYMBOL = "|w>|n" +ROOM_SYMBOL = "|bo|n" +LINK_COLOR = "|B" + +_MAP_GRID = [ + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", "@", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], +] +_EXIT_GRID_SHIFT = { + "north": (0, 1, "||"), + "east": (1, 0, "-"), + "south": (0, -1, "||"), + "west": (-1, 0, "-"), + "northeast": (1, 1, "/"), + "southeast": (1, -1, "\\"), + "southwest": (-1, -1, "/"), + "northwest": (-1, 1, "\\"), +} + +class EvAdventureRoom(DefaultRoom): + + # ... + + def format_appearance(self, appearance, looker, **kwargs): + """Don't left-strip the appearance string""" + return appearance.rstrip() + + def get_display_header(self, looker, **kwargs): + """ + Display the current location as a mini-map. + + """ + # make sure to not show make a map for users of screenreaders. + # for optimization we also don't show it to npcs/mobs + if not inherits_from(looker, DefaultCharacter) or ( + looker.account and looker.account.uses_screenreader() + ): + return "" + + # build a map + map_grid = deepcopy(_MAP_GRID) + dx0, dy0 = 2, 2 + map_grid[dy0][dx0] = CHAR_SYMBOL + for exi in self.exits: + dx, dy, symbol = _EXIT_GRID_SHIFT.get(exi.key, (None, None, None)) + if symbol is None: + # we have a non-cardinal direction to go to - indicate this + map_grid[dy0][dx0] = CHAR_ALT_SYMBOL + continue + map_grid[dy0 + dy][dx0 + dx] = f"{LINK_COLOR}{symbol}|n" + if exi.destination != self: + map_grid[dy0 + dy + dy][dx0 + dx + dx] = ROOM_SYMBOL + + # Note that on the grid, dy is really going *downwards* (origo is + # in the top left), so we need to reverse the order at the end to mirror it + # vertically and have it come out right. + return " " + "\n ".join("".join(line) for line in reversed(map_grid)) +``` + +The string returned from `get_display_header` will end up at the top of the [room description](Objects#changing-an-objects-description), a good place to have the map appear! + +The map itself consists of the 2D matrix `_MAP_GRID`. This is a 2D area described by a list of Python lists. To find a given place in the list, you first first need to find which of the nested lists to use, and then which element to use in that list. Indices start from 0 in Python. So to draw the `o` symbol for the southermost room, you'd need to do so at `_MAP_GRID[4][2]`. + +The `_EXIT_GRID_SHIFT` indicates the direction to go for each cardinal exit, along with the map symbol to draw at that point. So `"east": (1, 0, "-")` means the east exit will be drawn one step in the positive x direction (to the right), using the "-" symbol. For symbols like `|` and "\\" we need to escape with a double-symbol since these would otherwise be interpreted as part of other formatting. + +We start by making a `deepcopy` of the `_MAP_GRID`. This is so that we don't modify the original but always have an empty template to work from. + +We use `@` to indicate the location of the player (at coordinate `(2, 2)`). We then take the actual exits from the room use their names to figure out what symbols to draw out from the center. Once we have placed all the exit- and room-symbols in the grid, we merge it all together into a single string on the last line: + +```python +return " " + "\n ".join("".join(line) for line in reversed(map_grid)) +``` + +At the end we use Python's standard [join](https://www.w3schools.com/python/ref_string_join.asp) to convert the grid into a single string. In doing so we must flip the grid upside down (reverse the outermost list). Why is this? If you think about how a MUD game displays its data - by printing at the bottom and then scrolling upwards - you'll realize that Evennia has to send out the top of your map _first_ and the bottom of it _last_ for it to show correctly to the user. + +We want to be able to get on/off the grid if so needed. So if a room has a non-cardinal exit in it (like 'back' or up/down), we'll indicate this by showing the `>` symbol instead of the `@` in your current room. + +## Testing + +> Create a new module `evadventure/tests/test_rooms.py`. + +```{sidebar} +You can find a ready testing module [here in the tutorial folder](evennia.contrib.tutorials.evadventure.tests.test_rooms). +``` +The main thing to test with our new rooms is the map. Here's the basic principle for how to do this testing: + +```python +# in evadventure/tests/test_rooms.py + +from evennia import DefaultExit, create_object +from evennia.utils.test_resources import EvenniaTestCase +from ..characters import EvAdventureCharacter +from ..rooms import EvAdventureRoom + +class EvAdventureRoomTest(EvenniaTestCase): + + def test_map(self): + center_room = create_object(EvAdventureRoom, key="room_center") + + n_room = create_object(EvAdventureRoom, key="room_n) + create_object(DefaultExit, + key="north", location=center_room, destination=n_room) + ne_room = create_object(EvAdventureRoom, key="room=ne") + create_object(DefaultExit, + key="northeast", location=center_room, destination=ne_room) + # ... etc for all cardinal directions + + char = create_object(EvAdventureCharacter, + key="TestChar", location=center_room) + desc = center_room.return_appearance(char) + + # compare the desc we got with the expected description here + +``` + +## Conclusion + +In this lesson we manipulated strings and made a map. Changing the description of an object is a big part of changing the 'graphics' of a text-based game, so checking out the [documentation on this](Objects#changing-an-objects-description) is good extra reading. diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.combat.md b/docs/source/api/evennia.contrib.tutorials.evadventure.combat.md deleted file mode 100644 index eee067c118..0000000000 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.combat.md +++ /dev/null @@ -1,10 +0,0 @@ -```{eval-rst} -evennia.contrib.tutorials.evadventure.combat -=================================================== - -.. automodule:: evennia.contrib.tutorials.evadventure.combat - :members: - :undoc-members: - :show-inheritance: - -``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_base.md b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_base.md new file mode 100644 index 0000000000..4da5d14571 --- /dev/null +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_base.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.tutorials.evadventure.combat\_base +========================================================= + +.. automodule:: evennia.contrib.tutorials.evadventure.combat_base + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md index d46e70a590..ffccb7f735 100644 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_turnbased.md @@ -1,5 +1,5 @@ ```{eval-rst} -evennia.contrib.tutorials.evadventure.combat\_turnbased +evennia.contrib.tutorials.evadventure.combat\_turnbased ============================================================== .. automodule:: evennia.contrib.tutorials.evadventure.combat_turnbased @@ -7,4 +7,4 @@ evennia.contrib.tutorials.evadventure.combat\_turnbased :undoc-members: :show-inheritance: -``` +``` \ No newline at end of file diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.combat_twitch.md b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_twitch.md new file mode 100644 index 0000000000..1236f5ac11 --- /dev/null +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.combat_twitch.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.tutorials.evadventure.combat\_twitch +=========================================================== + +.. automodule:: evennia.contrib.tutorials.evadventure.combat_twitch + :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 38729c6790..d3a1157628 100644 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.md +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.md @@ -16,7 +16,9 @@ evennia.contrib.tutorials.evadventure evennia.contrib.tutorials.evadventure.build_world evennia.contrib.tutorials.evadventure.characters evennia.contrib.tutorials.evadventure.chargen - evennia.contrib.tutorials.evadventure.combat + evennia.contrib.tutorials.evadventure.combat_base + evennia.contrib.tutorials.evadventure.combat_turnbased + evennia.contrib.tutorials.evadventure.combat_twitch evennia.contrib.tutorials.evadventure.commands evennia.contrib.tutorials.evadventure.dungeon evennia.contrib.tutorials.evadventure.enums diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.tests.md b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.md index 3507cac214..cd6179c55e 100644 --- a/docs/source/api/evennia.contrib.tutorials.evadventure.tests.md +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.md @@ -20,6 +20,7 @@ evennia.contrib.tutorials.evadventure.tests evennia.contrib.tutorials.evadventure.tests.test_dungeon evennia.contrib.tutorials.evadventure.tests.test_equipment evennia.contrib.tutorials.evadventure.tests.test_quests + evennia.contrib.tutorials.evadventure.tests.test_rooms evennia.contrib.tutorials.evadventure.tests.test_rules evennia.contrib.tutorials.evadventure.tests.test_utils diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_rooms.md b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_rooms.md new file mode 100644 index 0000000000..47d7d74fa8 --- /dev/null +++ b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_rooms.md @@ -0,0 +1,10 @@ +```{eval-rst} +evennia.contrib.tutorials.evadventure.tests.test\_rooms +============================================================== + +.. automodule:: evennia.contrib.tutorials.evadventure.tests.test_rooms + :members: + :undoc-members: + :show-inheritance: + +``` \ No newline at end of file diff --git a/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev b/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev index 6f80ec9b7e..64918384fe 100644 --- a/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev +++ b/evennia/contrib/tutorials/evadventure/batchscripts/combat_demo.ev @@ -2,6 +2,8 @@ # # Set up a combat area for testing combat. Requires developer or superuser perm. # +# Run from in-game as batchcmd contrib.tutorials.evadventure.batchscripts.combat_demo +# # start from limbo @@ -13,13 +15,13 @@ type self = evennia.contrib.tutorials.evadventure.characters.EvAdventureCharacte # assign us the twitch combat cmdset (requires superuser/developer perms) -py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat.TwitchAttackCmdSet") +py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat_twitch.TwitchAttackCmdSet", persistent=True) # Create and give us a weapons (this will use defaults on the class) create sword:evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon -# create a consumable to use +# create a consumable to use create potion:evennia.contrib.tutorials.evadventure.objects.EvAdventureConsumable @@ -45,8 +47,8 @@ desc dummy = This is is an ugly training dummy made out of hay and wood. # make the dummy crazy tough -dummy.hp_max = 1000 +set dummy/hp_max = 1000 # -dummy.hp = 1000 +set dummy/hp = 1000 diff --git a/evennia/contrib/tutorials/evadventure/combat_twitch.py b/evennia/contrib/tutorials/evadventure/combat_twitch.py index 6a6a945836..a5be284c06 100644 --- a/evennia/contrib/tutorials/evadventure/combat_twitch.py +++ b/evennia/contrib/tutorials/evadventure/combat_twitch.py @@ -89,8 +89,9 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase): for comb in self.obj.location.contents if hasattr(comb, "scripts") and comb.scripts.has(self.key) ] + location = self.obj.location - if self.obj.location.allow_pvp: + if hasattr(location, "allow_pvp") and location.allow_pvp: # in pvp, everyone else is an enemy allies = [combatant] enemies = [comb for comb in combatants if comb != combatant] @@ -202,17 +203,21 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase): self.action_dict = self.fallback_action_dict self.queue_action(self.fallback_action_dict) + self.check_stop_combat() + def check_stop_combat(self): """ Check if the combat is over. """ - allies, enemies = self.get_sides() + allies, enemies = self.get_sides(self.obj) allies.append(self.obj) - # remove all dead combatants - allies = [comb for comb in allies if comb.hp > 0] - enemies = [comb for comb in enemies if comb.hp > 0] + location = self.obj.location + + # only keep combatants that are alive and still in the same room + allies = [comb for comb in allies if comb.hp > 0 and comb.location == location] + enemies = [comb for comb in enemies if comb.hp > 0 and comb.location == location] if not allies and not enemies: self.msg("Noone stands after the dust settles.", broadcast=False) @@ -220,11 +225,14 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase): return if not allies or not enemies: - still_standing = list_to_string(f"$You({comb.key})" for comb in allies + enemies) - self.msg( - f"The combat is over. Still standing: {still_standing}.", - broadcast=False, - ) + if allies + enemies == [self.obj]: + self.msg("The combat is over.") + else: + still_standing = list_to_string(f"$You({comb.key})" for comb in allies + enemies) + self.msg( + f"The combat is over. Still standing: {still_standing}.", + broadcast=False, + ) self.stop_combat() def stop_combat(self): @@ -274,11 +282,18 @@ class _BaseTwitchCombatCommand(Command): rhs = " ".join(rhs) self.lhs, self.rhs = lhs.strip(), rhs.strip() - def get_or_create_combathandler(self, combathandler_name="combathandler"): + def get_or_create_combathandler(self, target=None, combathandler_name="combathandler"): """ Get or create the combathandler assigned to this combatant. """ + if target: + # add/check combathandler to the target + if target.hp_max is None: + self.msg("You can't attack that!") + raise InterruptCommand() + + EvAdventureCombatTwitchHandler.get_or_create_combathandler(target) return EvAdventureCombatTwitchHandler.get_or_create_combathandler(self.caller) @@ -301,19 +316,19 @@ class CmdAttack(_BaseTwitchCombatCommand): if not target: return - combathandler = self.get_or_create_combathandler() + combathandler = self.get_or_create_combathandler(target) # we use a fixed dt of 3 here, to mimic Diku style; one could also picture # attacking at a different rate, depending on skills/weapon etc. combathandler.queue_action({"key": "attack", "target": target, "dt": 3}) combathandler.msg(f"$You() $conj(attack) $You({target.key})!", self.caller) -class CmdLook(default_cmds.CmdLook): +class CmdLook(default_cmds.CmdLook, _BaseTwitchCombatCommand): def func(self): # get regular look, followed by a combat summary super().func() if not self.args: - combathandler = self.get_or_create_combathandler(self.caller.location) + combathandler = self.get_or_create_combathandler() txt = str(combathandler.get_combat_summary(self.caller)) maxwidth = max(display_len(line) for line in txt.strip().split("\n")) self.msg(f"|r{pad(' Combat Status ', width=maxwidth, fillchar='-')}|n\n{txt}") @@ -416,7 +431,7 @@ class CmdStunt(_BaseTwitchCombatCommand): self.target = target.strip() def func(self): - combathandler = self.get_or_create_combathandler() + combathandler = self.get_or_create_combathandler(self.target) target = self.caller.search(self.target) if not target: @@ -478,7 +493,7 @@ class CmdUseItem(_BaseTwitchCombatCommand): if not target: return - combathandler = self.get_or_create_combathandler() + combathandler = self.get_or_create_combathandler(self.target) combathandler.queue_action({"key": "use", "item": item, "target": target}) combathandler.msg( f"$You() prepare to use {item.get_display_name(self.caller)}!", self.caller diff --git a/evennia/contrib/tutorials/evadventure/equipment.py b/evennia/contrib/tutorials/evadventure/equipment.py index 0fe1076625..b392858dcb 100644 --- a/evennia/contrib/tutorials/evadventure/equipment.py +++ b/evennia/contrib/tutorials/evadventure/equipment.py @@ -6,7 +6,7 @@ Knave has a system of Slots for its inventory. from evennia.utils.utils import inherits_from from .enums import Ability, WieldLocation -from .objects import EvAdventureObject, WeaponEmptyHand +from .objects import BARE_HANDS, EvAdventureObject class EquipmentError(TypeError): @@ -167,7 +167,7 @@ class EquipmentHandler: if not weapon: weapon = slots[WieldLocation.WEAPON_HAND] if not weapon: - weapon = WeaponEmptyHand() + weapon = BARE_HANDS return weapon def display_loadout(self): diff --git a/evennia/contrib/tutorials/evadventure/objects.py b/evennia/contrib/tutorials/evadventure/objects.py index 0134138619..aef8aa1596 100644 --- a/evennia/contrib/tutorials/evadventure/objects.py +++ b/evennia/contrib/tutorials/evadventure/objects.py @@ -18,7 +18,7 @@ rune sword (weapon+quest). """ -from evennia import AttributeProperty +from evennia import AttributeProperty, create_object, search_object from evennia.objects.objects import DefaultObject from evennia.utils.utils import make_iter @@ -125,7 +125,7 @@ class EvAdventureTreasure(EvAdventureObject): """ obj_type = ObjType.TREASURE - value = AttributeProperty(100) + value = AttributeProperty(100, autocreate=False) class EvAdventureConsumable(EvAdventureObject): @@ -136,14 +136,26 @@ class EvAdventureConsumable(EvAdventureObject): """ obj_type = ObjType.CONSUMABLE - size = AttributeProperty(0.25) - uses = AttributeProperty(1) + size = AttributeProperty(0.25, autocreate=False) + uses = AttributeProperty(1, autocreate=False) - def use(self, user, target, *args, **kwargs): + def at_pre_use(self, user, target=None, *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 + + return super().at_pre_use(user, target=target, *args, **kwargs) + + def use(self, user, target=None, *args, **kwargs): """ Use the consumable. """ + if user.location: user.location.msg_contents( f"$You() $conj(use) {self.get_display_name(user)}.", from_obj=user @@ -193,11 +205,16 @@ class EvAdventureWeapon(EvAdventureObject): return super().get_display_name(looker=looker, **kwargs) + quality_txt - def at_pre_use(self, user, *args, **kwargs): + 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, *args, **kwargs) + 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""" @@ -239,7 +256,8 @@ class EvAdventureWeapon(EvAdventureObject): message = f" $You() $conj(miss) $You({target.key})." if quality is Ability.CRITICAL_FAILURE: message += ".. it's a |rcritical miss!|n, damaging the weapon." - self.quality -= 1 + 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): @@ -262,21 +280,6 @@ class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable): damage_roll = AttributeProperty("1d6") -class WeaponEmptyHand(EvAdventureWeapon): - """ - This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it. - - """ - - obj_type = ObjType.WEAPON - key = "Empty Fists" - inventory_use_slot = WieldLocation.WEAPON_HAND - attack_type = Ability.STR - defense_type = Ability.ARMOR - damage_roll = "1d4" - quality = 100000 # let's assume fists are always available ... - - class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable): """ Base class for magic runestones. In _Knave_, every spell is represented by a rune stone @@ -335,3 +338,23 @@ class EvAdventureHelmet(EvAdventureArmor): obj_type = ObjType.HELMET inventory_use_slot = WieldLocation.HEAD + + +class WeaponBareHands(EvAdventureWeapon): + """ + This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it. + + """ + + obj_type = ObjType.WEAPON + key = "Bare hands" + inventory_use_slot = WieldLocation.WEAPON_HAND + attack_type = Ability.STR + defense_type = Ability.ARMOR + damage_roll = "1d4" + quality = 100000 # let's assume fists are always available ... + + +BARE_HANDS = search_object("Bare hands", typeclass=WeaponBareHands) +if not BARE_HANDS: + BARE_HANDS = create_object(WeaponBareHands, key="Bare hands") diff --git a/evennia/contrib/tutorials/evadventure/tests/test_rooms.py b/evennia/contrib/tutorials/evadventure/tests/test_rooms.py new file mode 100644 index 0000000000..e697f42591 --- /dev/null +++ b/evennia/contrib/tutorials/evadventure/tests/test_rooms.py @@ -0,0 +1,54 @@ +""" +Test of EvAdventure Rooms + +""" + +from evennia import DefaultExit, create_object +from evennia.utils.ansi import strip_ansi +from evennia.utils.test_resources import EvenniaTestCase + +from ..characters import EvAdventureCharacter +from ..rooms import EvAdventureRoom + + +class EvAdventureRoomTest(EvenniaTestCase): + def setUp(self): + self.char = create_object(EvAdventureCharacter, key="TestChar") + + def test_map(self): + center_room = create_object(EvAdventureRoom, key="room_center") + n_room = create_object(EvAdventureRoom, key="room_n") + create_object(DefaultExit, key="north", location=center_room, destination=n_room) + ne_room = create_object(EvAdventureRoom, key="room_ne") + create_object(DefaultExit, key="northeast", location=center_room, destination=ne_room) + e_room = create_object(EvAdventureRoom, key="room_e") + create_object(DefaultExit, key="east", location=center_room, destination=e_room) + se_room = create_object(EvAdventureRoom, key="room_se") + create_object(DefaultExit, key="southeast", location=center_room, destination=se_room) + s_room = create_object(EvAdventureRoom, key="room_") + create_object(DefaultExit, key="south", location=center_room, destination=s_room) + sw_room = create_object(EvAdventureRoom, key="room_sw") + create_object(DefaultExit, key="southwest", location=center_room, destination=sw_room) + w_room = create_object(EvAdventureRoom, key="room_w") + create_object(DefaultExit, key="west", location=center_room, destination=w_room) + nw_room = create_object(EvAdventureRoom, key="room_nw") + create_object(DefaultExit, key="northwest", location=center_room, destination=nw_room) + + desc = center_room.return_appearance(self.char) + + expected = r""" + o o o + \|/ + o-@-o + /|\ + o o o +room_center +You see nothing special. +Exits: north, northeast, east, southeast, south, southwest, west, and northwest""" + + result = "\n".join(part.rstrip() for part in strip_ansi(desc).split("\n")) + expected = "\n".join(part.rstrip() for part in expected.split("\n")) + print(result) + print(expected) + + self.assertEqual(result, expected)