Expand tutorial on equipmenthandler

This commit is contained in:
Griatch 2022-08-30 23:03:39 +02:00
parent af2837c8c1
commit ba13e3e44f
423 changed files with 689 additions and 3613 deletions

View file

@ -181,7 +181,7 @@ class CmdEcho(Command):
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
makes use of some of those variables we found the Command offers to us. If you did the
[basic Python tutorial](./Python-basic-introduction.md), you will recognize `.msg` - this will send a message
[basic Python tutorial](./Beginner-Tutorial-Python-basic-introduction.md), you will recognize `.msg` - this will send a message
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
that in the message.

View file

@ -149,7 +149,7 @@ the raw description of your current room (including color codes), so that you ca
set its description to something else.
You create new Commands (or modify existing ones) in Python outside the game. We will get to that
later, in the [Commands tutorial](./Adding-Commands.md).
later, in the [Commands tutorial](./Beginner-Tutorial-Adding-Commands.md).
## Get a Personality

View file

@ -200,7 +200,7 @@ people change and re-structure this in various ways to better fit their ideas.
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
[Tutorial World](./Tutorial-World.md) was built with such a batch-file.
[Tutorial World](./Beginner-Tutorial-Tutorial-World.md) was built with such a batch-file.
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different

View file

@ -251,7 +251,7 @@ You are specifying exactly which typeclass you want to use to build the Giantess
desc = You see nothing special.
-------------------------------------------------------------------------------
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
We used the `examine` command briefly in the [lesson about building in-game](./Beginner-Tutorial-Building-Quickstart.md). Now these lines
may be more useful to us:
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
unique 'primary key' or _dbref_ for this entity in the database.
@ -357,7 +357,7 @@ You got a lot longer output this time. You have a lot more going on than a simpl
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
- **Account** shows, well the `Account` object associated with this Character and Session.
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
get to them in the [next lesson](./Beginner-Tutorial-Adding-Commands.md). For now it's enough to know these consitute all the
commands available to you at a given moment.
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.

View file

@ -143,8 +143,8 @@ change (no code changed, only stuff in the database).
## Adding a Command to an object
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
but _also to those in the same location as that object_. If you did the [Building introduction](./Building-Quickstart.md)
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World.md)
but _also to those in the same location as that object_. If you did the [Building introduction](./Beginner-Tutorial-Building-Quickstart.md)
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-Tutorial-World.md)
also has many examples of objects with commands on them.
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.

View file

@ -30,18 +30,18 @@ these concepts in the context of Evennia before.
:maxdepth: 1
:numbered:
Building-Quickstart
Tutorial-World
Python-basic-introduction
Gamedir-Overview
Python-classes-and-objects
Evennia-Library-Overview
Learning-Typeclasses
Adding-Commands
More-on-Commands
Creating-Things
Searching-Things
Django-queries
Beginner-Tutorial-Building-Quickstart
Beginner-Tutorial-Tutorial-World
Beginner-Tutorial-Python-basic-introduction
Beginner-Tutorial-Gamedir-Overview
Beginner-Tutorial-Python-classes-and-objects
Beginner-Tutorial-Evennia-Library-Overview
Beginner-Tutorial-Learning-Typeclasses
Beginner-Tutorial-Adding-Commands
Beginner-Tutorial-More-on-Commands
Beginner-Tutorial-Creating-Things
Beginner-Tutorial-Searching-Things
Beginner-Tutorial-Django-queries
```
@ -50,17 +50,17 @@ Django-queries
```{toctree}
:maxdepth: 2
Building-Quickstart
Tutorial-World
Python-basic-introduction
Gamedir-Overview
Python-classes-and-objects
Evennia-Library-Overview
Learning-Typeclasses
Adding-Commands
More-on-Commands
Creating-Things
Searching-Things
Django-queries
Beginner-Tutorial-Building-Quickstart
Beginner-Tutorial-Tutorial-World
Beginner-Tutorial-Python-basic-introduction
Beginner-Tutorial-Gamedir-Overview
Beginner-Tutorial-Python-classes-and-objects
Beginner-Tutorial-Evennia-Library-Overview
Beginner-Tutorial-Learning-Typeclasses
Beginner-Tutorial-Adding-Commands
Beginner-Tutorial-More-on-Commands
Beginner-Tutorial-Creating-Things
Beginner-Tutorial-Searching-Things
Beginner-Tutorial-Django-queries
```

View file

@ -93,7 +93,7 @@ The form `from ... import ... as ...` renames the import.
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
> easy to read as possible, and renaming adds another layer of potential confusion.
In [the basic intro to Python](./Python-basic-introduction.md) we learned how to open the in-game
In [the basic intro to Python](./Beginner-Tutorial-Python-basic-introduction.md) we learned how to open the in-game
multi-line interpreter.
> py
@ -153,7 +153,7 @@ Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. Th
actually do anything on its own, its only code (except the docstring) is `pass` which means,
well, to pass and don't do anything.
We will get back to this module in the [next lesson](./Learning-Typeclasses.md). First we need to do a
We will get back to this module in the [next lesson](./Beginner-Tutorial-Learning-Typeclasses.md). First we need to do a
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
things to understand before you can use Evennia efficiently.
```{sidebar} OOP

View file

@ -29,16 +29,16 @@ and "what to think about" when creating a multiplayer online text game.
```{toctree}
:maxdepth: 1
Planning-Where-Do-I-Begin.md
Game-Planning.md
Planning-The-Tutorial-Game.md
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
Beginner-Tutorial-Game-Planning.md
Beginner-Tutorial-Planning-The-Tutorial-Game.md
```
## Table of Contents
```{toctree}
Planning-Where-Do-I-Begin.md
Game-Planning.md
Planning-The-Tutorial-Game.md
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
Beginner-Tutorial-Game-Planning.md
Beginner-Tutorial-Planning-The-Tutorial-Game.md
```

View file

@ -32,7 +32,7 @@ Here's an overview of the topside camp for inspiration (quickly thrown together
![Last Step Camp](../../../_static/images/starting_tutorial/Dungeon_Merchant_Camp.jpg)
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Game-Planning.md) lesson.
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Beginner-Tutorial-Game-Planning.md) lesson.
## Administration

View file

@ -276,6 +276,34 @@ class EvAdventureRollEngine:
# ...
```
## Connecting the Character with Evennia
You can easily make yourself an `EvAdventureCharacter` in-game by using the
`type` command:
type self = evadventure.characters.EvAdventureCharacter
You can now do `examine self` to check your type updated.
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
the `Character` class in `mygame/typeclasses/characters.py`).
There are thus two ways to weave your new Character class into Evennia:
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_CLASS = "evadventure.characters.EvAdventureCharacter"`.
2. Or, change `typeclasses.characters.Character` to inherit from `EvAdventureCharacter`.
You must always reload the server for changes like this to take effect.
```{important}
In this tutorial we are making all changes in a folder `mygame/evadventure/`. This means we can isolate
our code but means we need to do some extra steps to tie the character (and other objects) into Evennia.
For your own game it would be just fine to start editing `mygame/typeclasses/characters.py` directly
instead.
```
## Unit Testing
@ -329,6 +357,34 @@ For running the tests you do:
evennia test --settings settings.py .evadventure.tests.test_character
## About races and classes
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
add these functions.
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
an Attribute on your Character:
```python
# mygame/evadventure/characters.py
from evennia import DefaultCharacter, AttributeProperty
# ...
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# ...
charclass = AttributeProperty("Fighter")
charrace = AttributeProperty("Human")
```
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
`race` as `charrace` thus matches in style.
## Summary

View file

@ -32,14 +32,13 @@ The BACKPACK is special - it contains any number of items (up to the maximum slo
> Create a new module `mygame/evadventure/equipment.py`.
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_.
```{sidebar}
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_.
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).
@ -101,13 +100,14 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
return EquipmentHandler(self)
```
After reloading the server, the equipment-handler will now be accessible on the character as
After reloading the server, the equipment-handler will now be accessible on character-instances as
character.equipment
The `@lazy_property` works such that it will not load the handler until it is first accessed. When that
happens, we start up the handler and feed it `self` (the Character 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.
@ -125,9 +125,36 @@ have one item except `WieldLocation.BACKPACK`, which is a list.
## Connecting the EquipmentHandler
We already made `EquipmentHandler` available on the Character as `.equipment`. Now we want it to come into
play automatically whenever we pick up or drop something. To do this we need to override two hooks
on the Character class:
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`:
1. `self.at_pre_move(destination)` (abort if return False)
2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
4. `source_location.at_object_leave(self, destination)`
5. `self.announce_move_from(destination)`
6. (move happens here)
7. `self.announce_move_to(source_location)`
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.
- `.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
- `.at_object_leave` - used to remove the item from the equipmenthandler
You could also picture using `.at_pre_object_leave` to restrict dropping (cursed?) items, but
we will skip that for this tutorial.
```python
# mygame/evadventure/character.py
@ -143,7 +170,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
if they pick up something). If it returns False, move is aborted.
"""
# we haven't written this yet!
return self.equipment.validate_slot_usage(moved_object)
def at_object_receive(self, moved_object, source_location, **kwargs):
@ -151,7 +177,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
Called by Evennia when an object arrives 'in' the character.
"""
self.equipment.add(moved_object)
self.equipment.add(moved_object)
def at_object_leave(self, moved_object, destination, **kwargs):
"""
@ -161,6 +187,413 @@ 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.
## 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
from .enums import WieldLocation, Ability
class EquipmentError(TypeError):
"""All types of equipment-errors"""
pass
class EquipmentHandler:
# ...
@property
def max_slots(self):
"""Max amount of slots, based on CON defense (CON + 10)"""
return getattr(self.obj, Ability.CON.value, 1) + 10
def count_slots(self):
"""Count current slot usage"""
slots = self.slots
wield_usage = sum(
getattr(slotobj, "size", 0) or 0
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
backpack_usage = sum(
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
)
return wield_usage + backpack_usage
def validate_slot_usage(self, obj):
"""
Check if obj can fit in equipment, based on its size.
"""
if not inherits_from(obj, EvAdventureObject):
# in case we mix with non-evadventure objects
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
size = obj.size
max_slots = self.max_slots
current_slot_usage = self.count_slots()
return current_slot_usage + size <= max_slots:
```
```{sidebar}
The `@property` decorator turns a method into a property so you don't need to 'call' it.
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.
### `.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).
So to be clear,
```python
getattr(self.obj, Ability.CON.value) + 10
```
is the same as writing
```python
getattr(your_character, "Constitution") + 10
```
which is the same as doing something like this:
```python
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.
### `.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:
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.
In `count_slots` we have this code:
```python
wield_usage = sum(
getattr(slotobj, "size", 0)
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
```
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:
```python
backpack_item_sizes = []
for slot, slotobj in slots.items():
if slot is not WieldLocation.BACKPACK:
size = getattr(slotobj, "size", 0)
backpack_item_sizes.append(size)
wield_usage = sum(backpack_item_sizes)
```
The same is done for the items actually in the BACKPACK slot. The total sizes are added
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.
## `.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).
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def add(self, obj):
"""
Put something in the backpack.
"""
self.validate_slot_usage(obj)
self.slots[WieldLocation.BACKPACK].append(obj)
self._save()
def remove(self, slot):
"""
Remove contents of a particular slot, for
example `equipment.remove(WieldLocation.SHIELD_HAND)`
"""
slots = self.slots
ret = []
if slot is WieldLocation.BACKPACK:
# empty entire backpack!
ret.extend(slots[slot])
slots[slot] = []
else:
ret.append(slots[slot])
slots[slot] = None
if ret:
self._save()
return ret
```
Both of these should be straight forward to follow. In `.add`, we make use of `validate_slot_usage` to
double-check we can actually fit the thing, then we add the item to the backpack.
In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it is and return
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.
## 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:
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def move(self, obj):
"""Move object from backpack to its intended `inventory_use_slot`."""
# make sure to remove from equipment/backpack first, to avoid double-adding
self.remove(obj)
slots = self.slots
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
to_backpack = []
if use_slot is WieldLocation.TWO_HANDS:
# two-handed weapons can't co-exist with weapon/shield-hand used items
to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
slots[use_slot] = obj
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
# can't keep a two-handed weapon if adding a one-handed weapon or shield
to_backpack = [slots[WieldLocation.TWO_HANDS]]
slots[WieldLocation.TWO_HANDS] = None
slots[use_slot] = obj
elif use_slot is WieldLocation.BACKPACK:
# it belongs in backpack, so goes back to it
to_backpack = [obj]
else:
# for others (body, head), just replace whatever's there
replaced = [obj]
slots[use_slot] = obj
for to_backpack_obj in to_backpack:
# put stuff in backpack
slots[use_slot].append(to_backpack_obj)
# store new state
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.
## Get everything
In order to visualize our inventory, we need some method to get everything we are carrying.
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def all(self):
"""
Get all objects in inventory, regardless of location.
"""
slots = self.slots
lst = [
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
(slots[WieldLocation.BODY], WieldLocation.BODY),
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
return lst
```
Here we get all the equipment locations and add their contents together into a list of tuples
`[(item, WieldLocation), ...]`. This is convenient for display.
## 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.
```python
# mygame/evadventure/equipment.py
from .objects import WeaponEmptyHand
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
@property
def armor(self):
slots = self.slots
return sum(
(
# armor is listed using its defense, so we remove 10 from it
# (11 is base no-armor value in Knave)
getattr(slots[WieldLocation.BODY], "armor", 1),
# shields and helmets are listed by their bonus to armor
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
getattr(slots[WieldLocation.HEAD], "armor", 0),
)
)
@property
def weapon(self):
# first checks two-handed wield, then one-handed; the two
# should never appear simultaneously anyhow (checked in `move` method).
slots = self.slots
weapon = slots[WieldLocation.TWO_HANDS]
if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
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 `.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'
object that represents your bare hands with damage and all.
(created in [The Object tutorial](./Beginner-Tutorial-Objects.md#your-bare-hands) earlier).
## Extra credits
This covers the basic functionality of the equipment handler. There are other useful methods that
can be added:
- Given an item, figure out which equipment slot it is currently in
- Make a string representing the current loadout
- Get everything in the backpack (only)
- Get all wieldable items (weapons, shields) from backpack
- Get all usable items (items with a use-location of `BACKPACK`) from the backpack
Experiment with adding those. A full example is found in
[evennia/contrib/tutorials/evadventure/equipment.py](evennia.contrib.tutorials.evadventure.equipment).
## Unit Testing
> Create a new module `mygame/evadventure/tests/test_equipment.py`.
```{sidebar}
See [evennia/contrib/tutorials/evadventure/tests/test_equipment.py](evennia.contrib.tutorials.evadventure.tests.test_equipment)
for a finished testing example.
```
To test the `EquipmentHandler`, easiest is create an `EvAdventureCharacter` (this should by now
have `EquipmentHandler` available on itself as `.equipment`) and a few test objects; then test
passing these into the handler's methods.
```python
# mygame/evadventure/tests/test_equipment.py
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest
from ..objects import EvAdventureRoom
from ..enums import WieldLocation
class TestEquipment(BaseEvenniaTest):
def setUp(self):
self.character = create.create_object(EvAdventureCharacter, key='testchar')
self.helmet = create.create_object(EvAdventureHelmet, key="helmet")
self.weapon = create.create_object(EvAdventureWeapon, key="weapon")
def test_add_remove):
self.character.equipment.add(self.helmet)
self.assertEqual(
self.character.equipment.slots[WieldLocation.BACKPACK],
[self.helmet]
)
self.character.equipment.remove(self.helmet)
self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])
# ...
```
## 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.
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!

View file

@ -325,10 +325,36 @@ class EvAdventureHelmet(EvAdventureArmor):
inventory_use_slot = WieldLocation.HEAD
```
## 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.
```python
class WeaponEmptyHand:
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 "<WeaponEmptyHand>"
```
## 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.
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`.
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).

View file

@ -216,6 +216,12 @@ So we just set them to dummy values. We'll need to get back to this when we have
## Testing
```{important}
It's useful for any game dev to know how to effectively test their code. So we'll try to include a
*Testing* section at the end of each of the implementation lessons to follow. Writing tests for your code
is optional but highly recommended; it can feel a little cumbersome at first, but you'll thank yourself later.
```
> create a new module `mygame/evadventure/tests/test_utils.py`
How do you know if you made a typo in the code above? You could _manually_ test it by reloading your
@ -228,7 +234,7 @@ doing that test when you change this code later.
```{sidebar}
In [evennia/contrib/evadventure/tests/test_utils.py](evennia.contrib.evadventure.tests.test_utils)
is the final test module. To dive deeper into unit testing in Evennia, see the
is an example of the testing module. To dive deeper into unit testing in Evennia, see the
[Unit testing](../../../Coding/Unit-Testing.md) documentation.
```