mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Made several tutorial doc pages
This commit is contained in:
parent
fe9157da16
commit
39735626e1
393 changed files with 4534 additions and 170 deletions
|
|
@ -0,0 +1,354 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](Beginner-Gutorial-Rules) we made some assumptions
|
||||
about the "Player Character" entity:
|
||||
|
||||
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
|
||||
- It should have a `.heal(amount)` method.
|
||||
|
||||
So we have some guidelines of how it should look! A Character is a database entity with values that
|
||||
should be able to be changed over time. It makes sense to base it off Evennia's
|
||||
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
|
||||
RPG, it will hold everything relevant to that PC.
|
||||
|
||||
## Inheritance structure
|
||||
|
||||
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
|
||||
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
|
||||
|
||||
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
|
||||
we could use a class inheritance like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(EvAdventureCharacter):
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
|
||||
|
||||
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
|
||||
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
|
||||
PCs like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
# separate stuff
|
||||
|
||||
class EvAdventureMob(EvadventureNPC):
|
||||
# more separate stuff
|
||||
```
|
||||
|
||||
Nevertheless, there are some things that _should_ be common for all 'living things':
|
||||
|
||||
- All can take damage.
|
||||
- All can die.
|
||||
- All can heal
|
||||
- All can hold and lose coins
|
||||
- All can loot their fallen foes.
|
||||
- All can get looted when defeated.
|
||||
|
||||
We don't want to code this separately for every class but we no longer have a common parent
|
||||
class to put it on. So instead we'll use the concept of a _mixin_ class:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class LivingMixin:
|
||||
# stuff common for all living things
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureMob(LivingMixin, EvadventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/characters.py](evennia.contrib.tutorials.evadventure.characters)
|
||||
is an example of a character class structure.
|
||||
```
|
||||
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
|
||||
extra functionality all living things should be able to do. This is an example of
|
||||
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
|
||||
since it can also get confusing to follow the code.
|
||||
|
||||
## Living mixin class
|
||||
|
||||
> Create a new module `mygame/evadventure/characters.py`
|
||||
|
||||
Let's get some useful common methods all living things should have in our game.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
|
||||
# makes it easy for mobs to know to attack PCs
|
||||
is_pc = False
|
||||
|
||||
def heal(self, hp):
|
||||
"""
|
||||
Heal hp amount of health, not allowing to exceed our max hp
|
||||
|
||||
"""
|
||||
damage = self.hp_max - self.hp
|
||||
healed = min(damage, hp)
|
||||
self.hp += healed
|
||||
|
||||
self.msg("You heal for {healed} HP.")
|
||||
|
||||
def at_pay(self, amount):
|
||||
"""When paying coins, make sure to never detract more than we have"""
|
||||
amount = min(amount, self.coins)
|
||||
self.coins -= amount
|
||||
return amount
|
||||
|
||||
def at_damage(self, damage, attacker=None):
|
||||
"""Called when attacked and taking damage."""
|
||||
self.hp -= damage
|
||||
|
||||
def at_defeat(self):
|
||||
"""Called when defeated. By default this means death."""
|
||||
self.at_death()
|
||||
|
||||
def at_death(self):
|
||||
"""Called when this thing dies."""
|
||||
# this will mean different things for different living things
|
||||
pass
|
||||
|
||||
def at_do_loot(self, looted):
|
||||
"""Called when looting another entity"""
|
||||
looted.at_looted(self)
|
||||
|
||||
def at_looted(self, looter):
|
||||
"""Called when looted by another entity"""
|
||||
|
||||
# default to stealing some coins
|
||||
max_steal = dice.roll("1d10")
|
||||
stolen = self.at_pay(max_steal)
|
||||
looter.coins += stolen
|
||||
|
||||
```
|
||||
Most of these are empty since they will behave differently for characters and npcs. But having them
|
||||
in the mixin means we can expect these methods to be available for all living things.
|
||||
|
||||
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
|
||||
strength = AttributeProperty(1)
|
||||
dexterity = AttributeProperty(1)
|
||||
constitution = AttributeProperty(1)
|
||||
intelligence = AttributeProperty(1)
|
||||
wisdom = AttributeProperty(1)
|
||||
charisma = AttributeProperty(1)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
hp_max = AttributeProperty(8)
|
||||
|
||||
level = AttributeProperty(1)
|
||||
xp = AttributeProperty(0)
|
||||
coins = AttributeProperty(0)
|
||||
|
||||
def at_defeat(self):
|
||||
"""Characters roll on the death table"""
|
||||
if self.location.allow_death:
|
||||
# this allow rooms to have non-lethal battles
|
||||
dice.roll_death(self)
|
||||
else:
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
self.heal(self.hp_max)
|
||||
|
||||
def at_death(self):
|
||||
"""We rolled 'dead' on the death table."""
|
||||
self.location.msg_contents(
|
||||
"$You() collapse in a heap, embraced by death.",
|
||||
from_obj=self)
|
||||
# TODO - go back into chargen to make a new character!
|
||||
```
|
||||
|
||||
We make an assumption about our rooms here - that they have a property `.allow_death`. We need
|
||||
to make a note to actually add such a property to rooms later!
|
||||
|
||||
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
|
||||
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible
|
||||
on every character in several ways:
|
||||
|
||||
- As `character.strength`
|
||||
- As `character.db.strength`
|
||||
- As `character.attributes.get("strength")`
|
||||
|
||||
See [Attributes](../../../Components/Attributes.md) for seeing how Attributes work.
|
||||
|
||||
Unlike in base _Knave_, we store `coins` as a separate Attribute rather than as items in the inventory,
|
||||
this makes it easier to handle barter and trading later.
|
||||
|
||||
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()`
|
||||
from the `LivingMixin` class.
|
||||
|
||||
### Funcparser inlines
|
||||
|
||||
This piece of code is worth some more explanation:
|
||||
|
||||
```python
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
```
|
||||
|
||||
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a
|
||||
message to everything inside my current location". In other words, send a message to everyone
|
||||
in the same place as the character.
|
||||
|
||||
The `$You() $conj(collapse)` are [Funcparser inlines](Funcparser). These are functions that execute
|
||||
in the string. The resulting string may look different for different audiences. The `$You()` inline
|
||||
function will use `from_obj` to figure out who 'you' are and either show your name or 'You'.
|
||||
The `$conj()` (verb conjugator) will tweak the (English) verb to match.
|
||||
|
||||
- You will see: `"You collapse in a heap, alive but beaten."`
|
||||
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
|
||||
|
||||
Note how `$conj()` chose `collapse/collapses` to make the sentences grammatically correct.
|
||||
|
||||
### Backtracking
|
||||
|
||||
We make our first use of the `rules.dice` roller to roll on the death table! As you may recall, in the
|
||||
previous lesson, we didn't know just what to do when rolling 'dead' on this table. Now we know - we
|
||||
should be calling `at_death` on the character. So let's add that where we had TODOs before:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# kill the character!
|
||||
character.at_death() # <------ TODO no more
|
||||
else:
|
||||
# ...
|
||||
|
||||
if current_ability < -10:
|
||||
# kill the character!
|
||||
character.at_death() # <------- TODO no more
|
||||
else:
|
||||
# ...
|
||||
```
|
||||
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_characters.py`
|
||||
|
||||
For testing, we just need to create a new EvAdventure character and check
|
||||
that calling the methods on it doesn't error out.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_characters.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..characters import EvAdventureCharacter
|
||||
|
||||
class TestCharacters(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
||||
|
||||
def test_heal(self):
|
||||
self.character.hp = 0
|
||||
self.character.hp_max = 8
|
||||
|
||||
self.character.heal(1)
|
||||
self.assertEqual(self.character.hp, 1)
|
||||
# make sure we can't heal more than max
|
||||
self.character.heal(100)
|
||||
self.assertEqual(self.character.hp, 8)
|
||||
|
||||
def test_at_pay(self):
|
||||
self.character.coins = 100
|
||||
|
||||
result = self.character.at_pay(60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(self.character.coins, 40)
|
||||
|
||||
# can't get more coins than we have
|
||||
result = self.character.at_pay(100)
|
||||
self.assertEqual(result, 40)
|
||||
self.assertEqual(self.character.coins, 0)
|
||||
|
||||
# tests for other methods ...
|
||||
|
||||
```
|
||||
If you followed the previous lessons, these tests should look familiar. Consider adding
|
||||
tests for other methods as practice. Refer to previous lessons for details.
|
||||
|
||||
For running the tests you do:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_character
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
|
||||
like under _Knave_.
|
||||
|
||||
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
|
||||
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
|
||||
the command
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
|
||||
Check out your strength with
|
||||
|
||||
py self.strength = 3
|
||||
|
||||
```{important}
|
||||
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
|
||||
Attributes added with `AttributeProperty` are not available until they have been accessed at
|
||||
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
|
||||
then on.
|
||||
```
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# 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.
|
||||
|
||||
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). This is what we have in `enums.py`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
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).
|
||||
|
||||
## EquipmentHandler
|
||||
|
||||
> 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) talking about the principle.
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation
|
||||
|
||||
class EquipmentHandler:
|
||||
save_attribute = "inventory_slots"
|
||||
|
||||
def __init__(self, obj):
|
||||
# here obj is the character we store the handler on
|
||||
self.obj = obj
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load our data from an Attribute on `self.obj`"""
|
||||
self.slots = self.obj.attributes.get(
|
||||
self.save_attribute,
|
||||
category="inventory",
|
||||
default={
|
||||
WieldLocation.WEAPON_HAND: None,
|
||||
WieldLocation.SHIELD_HAND: None,
|
||||
WieldLocation.TWO_HANDS: None,
|
||||
WieldLocation.BODY: None,
|
||||
WieldLocation.HEAD: None,
|
||||
WieldLocation.BACKPACK: []
|
||||
}
|
||||
)
|
||||
|
||||
def _save(self):
|
||||
"""Save our data back to the same Attribute"""
|
||||
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
||||
```
|
||||
|
||||
This is a compact and functioning little handler. Before analyzing how it works, this is how
|
||||
we will add it to the Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
# ...
|
||||
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .equipment import EquipmentHandler
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
@lazy_property
|
||||
def equipment(self):
|
||||
return EquipmentHandler(self)
|
||||
```
|
||||
|
||||
After reloading the server, the equipment-handler will now be accessible on the character 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.
|
||||
|
||||
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.
|
||||
|
||||
Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named
|
||||
[Attribute](Attributes). Since we use a `category`, we are unlikely to collide with other Attributes.
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
# In-game Objects and items
|
||||
|
||||
In the previous lesson we established what a 'Character' is in our game. Before we continue
|
||||
we also need to have a notion what an 'item' or 'object' is.
|
||||
|
||||
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.
|
||||
- `obj_type` - Which 'type' of item this is.
|
||||
|
||||
|
||||
## New Enums
|
||||
|
||||
We added a few enumberations for Abilities back in the [Utilities tutorial](./Beginner-Tutorial-Utilities.md).
|
||||
Before we continue, let's expand with enums for use-slots and object types.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
class ObjType(Enum):
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Once we have these enums, we will use them for referencing things.
|
||||
|
||||
## The base object
|
||||
|
||||
> Create a new module `mygame/evadventure/objects.py`
|
||||
|
||||
```{sidebar}
|
||||
[evennia/contrib/tutorials/evadventure/objects.py](evennia.contrib.tutorials.evadventure.objects) has
|
||||
a full set of objects implemented.
|
||||
```
|
||||
<div style="clear: right;"></div>
|
||||
|
||||
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
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from evennia.utils.utils import make_iter
|
||||
from .utils import get_obj_stats
|
||||
from .enums import WieldLocation, ObjType
|
||||
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
"""
|
||||
Base for all evadventure objects.
|
||||
|
||||
"""
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = AttributeProperty(1, autocreate=False)
|
||||
value = AttributeProperty(0, autocreate=False)
|
||||
|
||||
# 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
|
||||
|
||||
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_help(self):
|
||||
"""Get any help text for this item"""
|
||||
return "No help for this item"
|
||||
```
|
||||
|
||||
### Using Attributes or not
|
||||
|
||||
In theory, `size` and `value` does not change and _could_ also be just set as a regular Python
|
||||
property on the class:
|
||||
|
||||
```python
|
||||
class EvAdventureObject(DefaultObject):
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = 1
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
from .enums import ObjType
|
||||
from evennia.utils import search
|
||||
|
||||
# get all shields in the game
|
||||
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.
|
||||
|
||||
## Other object types
|
||||
|
||||
Some of the other object types are very simple so far.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from .enums import ObjType
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureQuestObject(EvAdventureObject):
|
||||
"""Quest objects should usually not be possible to sell or trade."""
|
||||
obj_type = ObjType.QUEST
|
||||
|
||||
class EvAdventureTreasure(EvAdventureObject):
|
||||
"""Treasure is usually just for selling for coin"""
|
||||
obj_type = ObjType.TREASURE
|
||||
value = AttributeProperty(100, autocreate=False)
|
||||
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
"""An item that can be used up"""
|
||||
|
||||
obj_type = ObjType.CONSUMABLE
|
||||
value = AttributeProperty(0.25, autocreate=False)
|
||||
uses = AttributeProperty(1, autocreate=False)
|
||||
|
||||
def at_pre_use(self, user, *args, **kwargs):
|
||||
"""Called before using. If returning False, abort use."""
|
||||
return uses > 0
|
||||
|
||||
def at_use(self, user, *args, **kwargs):
|
||||
"""Called when using the item"""
|
||||
pass
|
||||
|
||||
def at_post_use(self. user, *args, **kwargs):
|
||||
"""Called after using the item"""
|
||||
# detract a usage, deleting the item if used up.
|
||||
self.uses -= 1
|
||||
if self.uses <= 0:
|
||||
user.msg(f"{self.key} was used up.")
|
||||
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.
|
||||
|
||||
## Weapons
|
||||
|
||||
All weapons need properties that describe how efficient they are in battle.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from .enums import WieldLocation, ObjType, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
"""Base class for all weapons"""
|
||||
|
||||
obj_type = ObjType.WEAPON
|
||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.STR, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d6", autocreate=False)
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
|
||||
"""Base for all magical rune stones"""
|
||||
|
||||
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
|
||||
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.INT, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.DEX, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d8", autocreate=False)
|
||||
|
||||
def at_post_use(self, user, *args, **kwargs):
|
||||
"""Called after usage/spell was cast"""
|
||||
self.uses -= 1
|
||||
# we don't delete the rune stone here, but
|
||||
# it must be reset on next rest.
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the rune stone (normally after rest)"""
|
||||
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 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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
``
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureAmor(EvAdventureObject):
|
||||
obj_type = ObjType.ARMOR
|
||||
inventory_use_slot = WieldLocation.BODY
|
||||
|
||||
armor = AttributeProperty(1, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
|
||||
class EvAdventureShield(EvAdventureArmor):
|
||||
obj_type = ObjType.SHIELD
|
||||
inventory_use_slot = WieldLocation.SHIELD_HAND
|
||||
|
||||
|
||||
class EvAdventureHelmet(EvAdventureArmor):
|
||||
obj_type = ObjType.HELMET
|
||||
inventory_use_slot = WieldLocation.HEAD
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -25,9 +25,11 @@ This is a big part. You'll be seeing a lot of code and there are plenty of lesso
|
|||
Take your time!
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to
|
||||
find and make use of things in Evennia. We also have a good idea of the type of game we want.
|
||||
find and make use of things in Evennia. We also have a good idea of the type of game we will
|
||||
create.
|
||||
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot
|
||||
of experience with using Evennia. This be _really_ helpful for doing your own thing later.
|
||||
of experience using Evennia and be really helpful for doing your own thing later!
|
||||
|
||||
Fully coded examples of all code we make in this part can be found in the
|
||||
[evennia/contrib/tutorials/evadventure](evennia.contrib.tutorials.evadventure) package.
|
||||
|
|
@ -39,27 +41,29 @@ Fully coded examples of all code we make in this part can be found in the
|
|||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tuturial-Objects
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tuturial-Objects
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
|
|
|
|||
|
|
@ -361,30 +361,21 @@ class EvAdventureRollEngine:
|
|||
|
||||
# ...
|
||||
|
||||
def heal(character, amount):
|
||||
"""
|
||||
Heal a certain amount of health, but not more
|
||||
than character's `hp_max`.
|
||||
|
||||
"""
|
||||
|
||||
hp = character.hp
|
||||
hp_max = character.hp_max
|
||||
|
||||
damage = hp_max - hp
|
||||
character.hp += min(damage, amount)
|
||||
|
||||
def heal_from_rest(self, character):
|
||||
"""
|
||||
A night's rest retains 1d8 + CON HP
|
||||
|
||||
"""
|
||||
con_bonus = getattr(character, Ability.CON.value, 1)
|
||||
self.heal(character, self.roll("1d8") + con_bonus)
|
||||
character.heal(self.roll("1d8") + con_bonus)
|
||||
```
|
||||
|
||||
By splitting this into two methods, we get a free convenient `heal` method we can use for healing
|
||||
also outside of sleeping.
|
||||
We make another assumption here - that `character.heal()` is a thing. We tell this function how
|
||||
much the character should heal, and it will do so, making sure to not heal more than its max
|
||||
number of HPs
|
||||
|
||||
> Knowing what is available on the character and what rule rolls we need is a bit of a chicken-and-egg
|
||||
> problem. We will make sure to implement the matching _Character_ class next lesson.
|
||||
|
||||
|
||||
### Rolling on a table
|
||||
|
|
@ -568,11 +559,71 @@ We don't yet know what 'killing the character' technically means, so we mark thi
|
|||
return to it in a later lesson. We just know that we need to do _something_ here to kill off the
|
||||
character!
|
||||
|
||||
## Testing
|
||||
|
||||
> Make a new module `mygame/evadventure/tests/test_rules.py`
|
||||
|
||||
Testing the `rules` module will also showcase some very useful tools when testing.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_rules.py
|
||||
|
||||
from unittest.mock import patch
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from .. import rules
|
||||
|
||||
class TestEvAdventureRuleEngine(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
"""Called before every test method"""
|
||||
super().setUp()
|
||||
self.roll_engine = rules.EvAdventureRollEngine()
|
||||
|
||||
@patch("evadventure.rules.randint")
|
||||
def test_roll(self, mock_randint):
|
||||
mock_randint.return_value = 4
|
||||
self.assertEqual(self.roll_engine.roll("1d6", 4)
|
||||
self.assertEqual(self.roll_engine.roll("2d6", 2 * 4)
|
||||
|
||||
# test of the other rule methods below ...
|
||||
```
|
||||
|
||||
As before, run the specific test with
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_rules
|
||||
|
||||
### Mocking and patching
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_rules.py](evennia.contrib.tutorials.evadventure.tests.test_rules)
|
||||
has a complete example of rule testing.
|
||||
```
|
||||
The `setUp` method is a special method of the testing class. It will be run before every
|
||||
test method. We use `super().setUp()` to make sure the parent class' version of this method
|
||||
always fire. Then we create a fresh `EvAdventureRollEngine` we can test with.
|
||||
|
||||
In our test, we import `patch` from the `unittest.mock` library. This is a very useful tool for testing.
|
||||
Normally the `randint` function we imported in `rules` will return a random value. That's very hard to
|
||||
test for, since the value will be different every test.
|
||||
|
||||
With `@patch` (this is called a _decorator_), we temporarily replace `rules.randint` with a 'mock' - a
|
||||
dummy entity. This mock is passed into the testing method. We then take this `mock_randint` and set
|
||||
`.return_value = 4` on it.
|
||||
|
||||
Adding `return_value` to the mock means that every time this mock is called, it will return 4. For the
|
||||
duration of the test we can now check with `self.assertEqual` that our `roll` method always returns a
|
||||
result as-if the random result was 4.
|
||||
|
||||
There are [many resources for understanding mock](https://realpython.com/python-mock-library/), refer to
|
||||
them for further help.
|
||||
|
||||
> The `EvAdventureRollEngine` have many methods to test. We leave this as an extra exercise!
|
||||
|
||||
## Summary
|
||||
|
||||
This concludes all the core rule mechanics of _Knave_ - the rules used during play. We noticed here
|
||||
that we are going to soon need to establish how our _Character_ actually stores data. So we will
|
||||
address that next, before we get to the character generation itself.
|
||||
address that next.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -128,42 +128,9 @@ class Ability(Enum):
|
|||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
class WieldLocation(Enum):
|
||||
"""
|
||||
Wield (or wear) locations.
|
||||
|
||||
"""
|
||||
|
||||
# wield/wear location
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
|
||||
class ObjType(Enum):
|
||||
"""
|
||||
Object types.
|
||||
|
||||
"""
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Here the `Ability` class holds basic properties of a character sheet, while `WieldLocation` tracks
|
||||
equipment and where a character would wield and wear things - since _Knave_ has these, it makes sense
|
||||
to track it. Finally we have a set of different `ObjType`s, for differentiate game items. These are
|
||||
extracted by reading the _Knave_ object lists and figuring out how they should be categorized.
|
||||
Here the `Ability` class holds basic properties of a character sheet.
|
||||
|
||||
|
||||
## Utility module
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue