mirror of
https://github.com/evennia/evennia.git
synced 2026-04-05 07:27:17 +02:00
Updated HTML docs.
This commit is contained in:
parent
dbae67275a
commit
76d95c253e
87 changed files with 922 additions and 850 deletions
|
|
@ -1,47 +1,47 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some assumptions about the "Player Character" entity:
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) 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
|
||||
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.
|
||||
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:
|
||||
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
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(EvAdventureCharacter):
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
|
||||
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
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
# separate stuff
|
||||
|
||||
# separate stuff
|
||||
|
||||
class EvAdventureMob(EvadventureNPC):
|
||||
# more separate stuff
|
||||
```
|
||||
|
|
@ -57,18 +57,18 @@ Nevertheless, there are some things that _should_ be common for all 'living thin
|
|||
|
||||
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
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class LivingMixin:
|
||||
# stuff common for all living things
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
# stuff
|
||||
|
||||
class EvAdventureMob(LivingMixin, EvadventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
|
@ -77,7 +77,8 @@ class EvAdventureMob(LivingMixin, EvadventureNPC):
|
|||
In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md)
|
||||
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.
|
||||
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_. The order of inheritance matters here - the `LivingMixin` must come _before_ `DefaultCharacter` (or EvAdventureNPC etc) so that its methods are found first when called.Multiple inheritance is a powerful tool in object-oriented programming, and useful to know about. Be careful to over-use it, however. If you have too many mixins it can get hard to follow which method comes from where.
|
||||
|
||||
|
||||
## Living mixin class
|
||||
|
||||
|
|
@ -85,15 +86,15 @@ Above, the `LivingMixin` class cannot work on its own - it just 'patches' the ot
|
|||
|
||||
Let's get some useful common methods all living things should have in our game.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from .rules import dice
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
|
||||
# makes it easy for mobs to know to attack PCs
|
||||
is_pc = False
|
||||
is_pc = False
|
||||
|
||||
@property
|
||||
def hurt_level(self):
|
||||
|
|
@ -118,58 +119,58 @@ class LivingMixin:
|
|||
elif percent == 0:
|
||||
return "|RCollapsed!|n"
|
||||
|
||||
def heal(self, hp):
|
||||
"""
|
||||
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(f"You heal for {healed} HP.")
|
||||
|
||||
|
||||
"""
|
||||
damage = self.hp_max - self.hp
|
||||
healed = min(damage, hp)
|
||||
self.hp += healed
|
||||
|
||||
self.msg(f"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_attacked(self, attacker, **kwargs):
|
||||
|
||||
def at_attacked(self, attacker, **kwargs):
|
||||
"""Called when being attacked and combat starts."""
|
||||
pass
|
||||
|
||||
|
||||
def at_damage(self, damage, attacker=None):
|
||||
"""Called when attacked and taking damage."""
|
||||
self.hp -= damage
|
||||
|
||||
def at_defeat(self):
|
||||
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
|
||||
|
||||
pass
|
||||
|
||||
def at_do_loot(self, looted):
|
||||
"""Called when looting another entity"""
|
||||
"""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")
|
||||
"""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.
|
||||
|
||||
Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying.
|
||||
Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying.
|
||||
|
||||
## Character class
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
||||
|
|
@ -177,28 +178,28 @@ We will now start making the basic Character class, based on what we need from _
|
|||
# in mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
from .rules import dice
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
# ...
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
|
||||
strength = AttributeProperty(1)
|
||||
strength = AttributeProperty(1)
|
||||
dexterity = AttributeProperty(1)
|
||||
constitution = AttributeProperty(1)
|
||||
intelligence = AttributeProperty(1)
|
||||
wisdom = AttributeProperty(1)
|
||||
charisma = AttributeProperty(1)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
hp_max = AttributeProperty(8)
|
||||
|
||||
|
||||
level = AttributeProperty(1)
|
||||
xp = AttributeProperty(0)
|
||||
coins = AttributeProperty(0)
|
||||
|
|
@ -213,18 +214,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
"$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!
|
||||
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:
|
||||
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`
|
||||
|
|
@ -234,7 +235,7 @@ See [Attributes](../../../Components/Attributes.md) for seeing how Attributes wo
|
|||
|
||||
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.
|
||||
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()` from the `LivingMixin` class.
|
||||
|
||||
### Funcparser inlines
|
||||
|
||||
|
|
@ -259,68 +260,68 @@ Note how `$conj()` chose `collapse/collapses` to make the sentences grammaticall
|
|||
|
||||
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
|
||||
```python
|
||||
# mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
# ...
|
||||
|
||||
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:
|
||||
else:
|
||||
# ...
|
||||
|
||||
if current_ability < -10:
|
||||
# kill the character!
|
||||
character.at_death() # <------- TODO no more
|
||||
else:
|
||||
# ...
|
||||
# ...
|
||||
```
|
||||
|
||||
## Connecting the Character with Evennia
|
||||
## Connecting the Character with Evennia
|
||||
|
||||
You can easily make yourself an `EvAdventureCharacter` in-game by using the
|
||||
`type` command:
|
||||
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.
|
||||
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`).
|
||||
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:
|
||||
There are thus two ways to weave your new Character class into Evennia:
|
||||
|
||||
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_TYPECLASS = "evadventure.characters.EvAdventureCharacter"`.
|
||||
2. Or, change `typeclasses.characters.Character` to inherit from `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
|
||||
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
|
||||
## 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.
|
||||
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
|
||||
# mygame/evadventure/tests/test_characters.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..characters import EvAdventureCharacter
|
||||
from ..characters import EvAdventureCharacter
|
||||
|
||||
class TestCharacters(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
|
|
@ -328,73 +329,73 @@ class TestCharacters(BaseEvenniaTest):
|
|||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
||||
|
||||
def test_heal(self):
|
||||
self.character.hp = 0
|
||||
self.character.hp_max = 8
|
||||
|
||||
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
|
||||
|
||||
self.character.coins = 100
|
||||
|
||||
result = self.character.at_pay(60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(self.character.coins, 40)
|
||||
|
||||
# can't get more coins than we have
|
||||
|
||||
# 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 ...
|
||||
|
||||
# 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.
|
||||
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:
|
||||
For running the tests you do:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_characters
|
||||
|
||||
|
||||
## About races and classes
|
||||
## 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.
|
||||
_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:
|
||||
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.
|
||||
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming `race` as `charrace` thus matches in style.
|
||||
|
||||
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
|
||||
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
|
||||
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look like under _Knave_.
|
||||
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
|
||||
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
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. Check out your strength with
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`. Check out your strength with
|
||||
|
||||
py self.strength = 3
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
|
|||
def get_sides(self, combatant):
|
||||
"""
|
||||
Get a listing of the two 'sides' of this combat,
|
||||
m the perspective of the provided combatant.
|
||||
from the perspective of the provided combatant.
|
||||
"""
|
||||
if self.obj.allow_pvp:
|
||||
# in pvp, everyone else is an ememy
|
||||
|
|
@ -218,7 +218,7 @@ class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
|
|||
We use the `advantage/disadvantage_matrix` Attributes to track who has advantage against whom.
|
||||
|
||||
```{sidebar} .pop()
|
||||
The Python `.pop()` method exists on lists and dicts as well as some other iterables. It 'pops' and returns an element from the container. For a list, it's either popped by index or by popping the last element. For a dict (like here), a specific key must be given to pop. If you don't provide a default value as a second element, an error will be raised if the key you try to pop is not found.
|
||||
The Python `.pop()` method removes an element from a list or dict and returns it. For a list, it removes by index (or the last element by default). For a dict (like here), you specify which key to remove. Providing a default value as a second argument prevents an error if the key doesn't exist.
|
||||
```
|
||||
In the `has dis/advantage` methods we `pop` the target from the matrix which will result either in the value `True` or `False` (the default value we give to `pop` if the target is not in the matrix). This means that the advantage, once gained, can only be used once.
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
|
|||
surviving_combatant = None
|
||||
allies, enemies = (), ()
|
||||
else:
|
||||
# grab a random survivor and check of they have any living enemies.
|
||||
# grab a random survivor and check if they have any living enemies.
|
||||
surviving_combatant = random.choice(list(self.combatants.keys()))
|
||||
allies, enemies = self.get_sides(surviving_combatant)
|
||||
|
||||
|
|
@ -722,7 +722,7 @@ Using this in an option will rerun the current node, but will preserve the `kwar
|
|||
|
||||
### Stepping through the wizard
|
||||
|
||||
Our particualr menu is very symmetric - you select an option and then you will just select a series of option before you come back. So we will make another goto-function to help us easily do this. To understand, let's first show how we plan to use this:
|
||||
Our particular menu is very symmetric - you select an option and then you will just select a series of option before you come back. So we will make another goto-function to help us easily do this. To understand, let's first show how we plan to use this:
|
||||
|
||||
```python
|
||||
# in the base combat-node function (just shown as an example)
|
||||
|
|
@ -816,7 +816,7 @@ We will make one final helper function, to quickly add the `back` (and `abort`)
|
|||
|
||||
# ...
|
||||
|
||||
_get_default_wizard_options(caller, **kwargs):
|
||||
def _get_default_wizard_options(caller, **kwargs):
|
||||
return [
|
||||
{
|
||||
"key": "back",
|
||||
|
|
@ -932,7 +932,6 @@ def node_choose_ability(caller, raw_string, **kwargs):
|
|||
Ability.DEX,
|
||||
Ability.CON,
|
||||
Ability.INT,
|
||||
Ability.INT,
|
||||
Ability.WIS,
|
||||
Ability.CHA,
|
||||
)
|
||||
|
|
@ -979,7 +978,7 @@ def node_choose_wield_item(caller, raw_string, **kwargs):
|
|||
|
||||
Our [equipment handler](./Beginner-Tutorial-Equipment.md) has the very useful help method `.get_usable_objects_from_backpack`. We just call this to get a list of all the items we want to choose. Otherwise this node should look pretty familiar by now.
|
||||
|
||||
The `node_choose_wiqld_item` is very similar, except it uses `caller.equipment.get_wieldable_objects_from_backpack()` instead. We'll leave the implementation of this up to the reader.
|
||||
The `node_choose_wield_item` is very similar, except it uses `caller.equipment.get_wieldable_objects_from_backpack()` instead. We'll leave the implementation of this up to the reader.
|
||||
|
||||
### The main menu node
|
||||
|
||||
|
|
@ -1084,11 +1083,11 @@ def node_combat(caller, raw_string, **kwargs):
|
|||
|
||||
This starts off the `_step_wizard` for each action choice. It also lays out the `action_dict` for every action, leaving `None` values for the fields that will be set by the following nodes.
|
||||
|
||||
Note how we add the `"repeat"` key to some actions. Having them automatically repeat means the player don't have to insert the same action every time.
|
||||
Note how we add the `"repeat"` key to some actions. Having them automatically repeat means the player doesn't have to insert the same action every time.
|
||||
|
||||
## Attack Command
|
||||
|
||||
We will only need one single Command to run the Turnbased combat system. This is the `attack` command. Once you use it once, you will be in the menu.
|
||||
We will only need one single command to run the turnbased combat system. This is the `attack` command. Once you use it once, you will be in the menu.
|
||||
|
||||
|
||||
```python
|
||||
|
|
@ -1317,7 +1316,7 @@ But apart from the `# HEADER` and `# CODE` specials, this just a series of norma
|
|||
|
||||
Log into the game with a developer/superuser account and run
|
||||
|
||||
> batchcmd evadventure.batchscripts.turnbased_combat_demo
|
||||
> batchcode evadventure.batchscripts.turnbased_combat_demo
|
||||
|
||||
This should place you in the arena with the dummy (if not, check for errors in the output! Use `objects` and `delete` commands to list and delete objects if you need to start over.)
|
||||
|
||||
|
|
@ -1325,6 +1324,6 @@ You can now try `attack dummy` and should be able to pound away at the dummy (lo
|
|||
|
||||
## Conclusions
|
||||
|
||||
At this point we have coverered some ideas on how to implement both twitch- and turnbased combat systems. Along the way you have been exposed to many concepts such as classes, scripts and handlers, Commands, EvMenus and more.
|
||||
At this point we have covered some ideas on how to implement both twitch- and turnbased combat systems. Along the way you have been exposed to many concepts such as classes, scripts and handlers, Commands, EvMenus and more.
|
||||
|
||||
Before our combat system is actually usable, we need our enemies to actually fight back. We'll get to that next.
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
|
|||
|
||||
"""
|
||||
if action_dict["key"] not in self.action_classes:
|
||||
self.obj.msg("This is an unkown action!")
|
||||
self.obj.msg("This is an unknown action!")
|
||||
return
|
||||
|
||||
# store action dict and schedule it to run in dt time
|
||||
|
|
@ -285,7 +285,7 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
|
|||
- **Line 43**: We simply store the given action dict in the Attribute `action_dict` on the handler. Simple and effective!
|
||||
- **Line 44**: When you enter e.g. `attack`, you expect in this type of combat to see the `attack` command repeat automatically even if you don't enter anything more. To this end we are looking for a new key in action dicts, indicating that this action should _repeat_ with a certain rate (`dt`, given in seconds). We make this compatible with all action dicts by simply assuming it's zero if not specified.
|
||||
|
||||
[evennia.utils.utils.repeat](evennia.utils.utils.repeat) and [evennia.utils.utils.unrepeat](evennia.utils.utils.unrepeat) are convenient shortcuts to the [TickerHandler](../../../Components/TickerHandler.md). You tell `repeat` to call a given method/function at a certain rate. What you get back is a reference that you can then later use to 'un-repeat' (stop the repeating) later. We make sure to store this reference (we don't care exactly how it looks, just that we need to store it) in `the current_ticket_ref` Attribute (**Line 26**).
|
||||
[evennia.utils.utils.repeat](evennia.utils.utils.repeat) and [evennia.utils.utils.unrepeat](evennia.utils.utils.unrepeat) are convenient shortcuts to the [TickerHandler](../../../Components/TickerHandler.md). You tell `repeat` to call a given method/function at a certain rate. What you get back is a reference that you can then later use to 'un-repeat' (stop the repeating) later. We make sure to store this reference (we don't care exactly how it looks, just that we need to store it) in the `current_ticker_ref` Attribute (**Line 26**).
|
||||
|
||||
- **Line 48**: Whenever we queue a new action (it may replace an existing one) we must make sure to kill (un-repeat) any old repeats that are ongoing. Otherwise we would get old actions firing over and over and new ones starting alongside them.
|
||||
- **Line 49**: If `dt` is set, we call `repeat` to set up a new repeat action at the given rate. We store this new reference. After `dt` seconds, the `.execute_next_action` method will fire (we'll create that in the next section).
|
||||
|
|
@ -305,30 +305,30 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
|
|||
# ...
|
||||
|
||||
def execute_next_action(self):
|
||||
"""
|
||||
Triggered after a delay by the command
|
||||
"""
|
||||
combatant = self.obj
|
||||
action_dict = self.action_dict
|
||||
action_class = self.action_classes[action_dict["key"]]
|
||||
action = action_class(self, combatant, action_dict)
|
||||
|
||||
if action.can_use():
|
||||
action.execute()
|
||||
action.post_execute()
|
||||
|
||||
if not action_dict.get("repeat", True):
|
||||
# not a repeating action, use the fallback (normally the original attack)
|
||||
self.action_dict = self.fallback_action_dict
|
||||
self.queue_action(self.fallback_action_dict)
|
||||
|
||||
self.check_stop_combat()
|
||||
"""
|
||||
Triggered after a delay by the command
|
||||
"""
|
||||
combatant = self.obj
|
||||
action_dict = self.action_dict
|
||||
action_class = self.action_classes[action_dict["key"]]
|
||||
action = action_class(self, combatant, action_dict)
|
||||
|
||||
if action.can_use():
|
||||
action.execute()
|
||||
action.post_execute()
|
||||
|
||||
if not action_dict.get("repeat", True):
|
||||
# not a repeating action, use the fallback (normally the original attack)
|
||||
self.action_dict = self.fallback_action_dict
|
||||
self.queue_action(self.fallback_action_dict)
|
||||
|
||||
self.check_stop_combat()
|
||||
```
|
||||
|
||||
This is the method called after `dt` seconds in `queue_action`.
|
||||
|
||||
- **Line 5**: We defined a 'fallback action'. This is used after a one-time action (one that should not repeat) has completed.
|
||||
- **Line 15**: We take the `'key'` from the `action-dict` and use the `action_classes` mapping to get an action class (e.g. `ACtionAttack` we defined [here](./Beginner-Tutorial-Combat-Base.md#attack-action)).
|
||||
- **Line 15**: We take the `'key'` from the `action_dict` and use the `action_classes` mapping to get an action class (e.g. `ActionAttack` we defined [here](./Beginner-Tutorial-Combat-Base.md#attack-action)).
|
||||
- **Line 16**: Here we initialize the action class with the actual current data - the combatant and the `action_dict`. This calls the `__init__` method on the class and makes the action ready to use.
|
||||
```{sidebar} New action-dict keys
|
||||
To summarize, for twitch-combat use we have now introduced two new keys to action-dicts:
|
||||
|
|
@ -365,7 +365,7 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
|
|||
enemies = [comb for comb in enemies if comb.hp > 0 and comb.location == location]
|
||||
|
||||
if not allies and not enemies:
|
||||
self.msg("The combat is over. Noone stands.", broadcast=False)
|
||||
self.msg("The combat is over. No one stands.", broadcast=False)
|
||||
self.stop_combat()
|
||||
return
|
||||
if not allies:
|
||||
|
|
@ -384,7 +384,7 @@ We must make sure to check if combat is over.
|
|||
- **Line 12**: With our `.get_sides()` method we can easily get the two sides of the conflict.
|
||||
- **Lines 18, 19**: We get everyone still alive _and still in the same room_. The latter condition is important in case we move away from the battle - you can't hit your enemy from another room.
|
||||
|
||||
In the `stop_method` we'll need to do a bunch of cleanup. We'll hold off on implementing this until we have the Commands written out. Read on.
|
||||
In the `stop_combat` method we'll need to do a bunch of cleanup. We'll hold off on implementing this until we have the Commands written out. Read on.
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -416,7 +416,7 @@ from evennia import InterruptCommand
|
|||
|
||||
class _BaseTwitchCombatCommand(Command):
|
||||
"""
|
||||
Parent class for all twitch-combat commnads.
|
||||
Parent class for all twitch-combat commands.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -641,7 +641,7 @@ class CmdStunt(_BaseTwitchCombatCommand):
|
|||
# something like `boost str target`
|
||||
target = recipient if advantage else "me"
|
||||
recipient = "me" if advantage else recipient
|
||||
we still have None:s at this point, we can't continue
|
||||
# if any values are still None at this point, we can't continue
|
||||
if None in (stunt_type, recipient, target):
|
||||
self.msg("Both ability, recipient and target of stunt must be given.")
|
||||
raise InterruptCommand()
|
||||
|
|
@ -677,7 +677,7 @@ class CmdStunt(_BaseTwitchCombatCommand):
|
|||
|
||||
```
|
||||
|
||||
This looks much longer, but that is only because the stunt command should understand many different input structures depending on if you are trying to create a advantage or disadvantage, and if an ally or enemy should receive the effect of the stunt.
|
||||
This looks much longer, but that is only because the stunt command should understand many different input structures depending on if you are trying to create an advantage or disadvantage, and if an ally or enemy should receive the effect of the stunt.
|
||||
|
||||
Note the `enums.ABILITY_REVERSE_MAP` (created in the [Utilities lesson](./Beginner-Tutorial-Utilities.md)) being useful to convert your input of 'str' into `Ability.STR` needed by the action dict.
|
||||
|
||||
|
|
@ -754,8 +754,8 @@ To use an item, we need to make sure we are carrying it. Luckily our work in the
|
|||
|
||||
class CmdWield(_BaseTwitchCombatCommand):
|
||||
"""
|
||||
Wield a weapon or spell-rune. You will the wield the item,
|
||||
swapping with any other item(s) you were wielded before.
|
||||
Wield a weapon or spell-rune. You wield the item,
|
||||
swapping with any other item(s) you were wielding before.
|
||||
|
||||
Usage:
|
||||
wield <weapon or spell>
|
||||
|
|
@ -893,7 +893,7 @@ from .. import combat_twitch
|
|||
|
||||
# ...
|
||||
|
||||
class TestEvAdventureTwitchCombat(EvenniaCommandTestMixin)
|
||||
class TestEvAdventureTwitchCombat(EvenniaCommandTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
self.combathandler = (
|
||||
|
|
@ -904,12 +904,12 @@ class TestEvAdventureTwitchCombat(EvenniaCommandTestMixin)
|
|||
@patch("evadventure.combat_twitch.unrepeat", new=Mock())
|
||||
@patch("evadventure.combat_twitch.repeat", new=Mock())
|
||||
def test_hold_command(self):
|
||||
self.call(combat_twitch, CmdHold(), "", "You hold back, doing nothing")
|
||||
self.call(combat_twitch, CmdHold(), "", "You hold back, doing nothing.")
|
||||
self.assertEqual(self.combathandler.action_dict, {"key": "hold"})
|
||||
|
||||
```
|
||||
|
||||
The `EvenniaCommandTestMixin` as a few default objects, including `self.char1`, which we make use of here.
|
||||
The `EvenniaCommandTestMixin` has a few default objects, including `self.char1`, which we make use of here.
|
||||
|
||||
The two `@patch` lines are Python [decorators](https://realpython.com/primer-on-python-decorators/) that 'patch' the `test_hold_command` method. What they do is basically saying "in the following method, whenever any code tries to access `evadventure.combat_twitch.un/repeat`, just return a Mocked object instead".
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue