diff --git a/docs/1.0/Coding/Coding-Overview.html b/docs/1.0/Coding/Coding-Overview.html
index 181809cbe3..9ea043afb0 100644
--- a/docs/1.0/Coding/Coding-Overview.html
+++ b/docs/1.0/Coding/Coding-Overview.html
@@ -162,6 +162,7 @@ make your game, also if you never coded before.
Softcode is a very simple programming language that was created for in-game development on TinyMUD derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea is that by providing a stripped down, minimalistic language for in-game use, you can allow quick and easy building and game development to happen without having to learn C/C++. There is an added benefit of not having to have to hand out shell access to all developers, and permissions can be used to alleviate many security problems.
-
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language.
-Each softcode function is a single line of varying size. Some functions can be a half of a page long
-or more which is obviously not very readable nor (easily) maintainable over time.
+
Softcode is a simple programming language that was created for in-game development on TinyMUD derivatives such as MUX, PennMUSH, TinyMUSH, and RhostMUSH. The idea was that by providing a stripped down, minimalistic language for in-game use, you could allow quick and easy building and game development to happen without builders having to learn the ‘hardcode’ language for those servers (C/C++). There is an added benefit of not having to have to hand out shell access to all developers. Permissions in softcode can be used to alleviate many security problems.
+
Writing and installing softcode is done through a MUD client. Thus it is not a formatted language. Each softcode function is a single line of varying size. Some functions can be a half of a page long or more which is obviously not very readable nor (easily) maintainable over time.
Pasting this into a MUX/MUSH and typing ‘hello’ will theoretically yield ‘Hello World!’, assuming
-certain flags are not set on your account object.
-
Setting attributes is done via @set. Softcode also allows the use of the ampersand (&) symbol.
-This shorter version looks like this:
+
Pasting this into a MUD client, sending it to a MUX/MUSH server and typing ‘hello’ will theoretically yield ‘Hello World!’, assuming certain flags are not set on your account object.
+
Setting attributes in Softcode is done via @set. Softcode also allows the use of the ampersand (&) symbol. This shorter version looks like this:
&HELLO_WORLD.Cme=$hello:@pemit%#=HelloWorld!
-
Perhaps I want to break the Hello World into an attribute which is retrieved when emitting:
+
We could also read the text from an attribute which is retrieved when emitting:
The v() function returns the HELLO_VALUE.D attribute on the object that the command resides
-(me, which is yourself in this case). This should yield the same output as the first example.
-
If you are still curious about how Softcode works, take a look at some external resources:
+
The v() function returns the HELLO_VALUE.D attribute on the object that the command resides (me, which is yourself in this case). This should yield the same output as the first example.
+
If you are curious about how MUSH/MUX Softcode works, take a look at some external resources:
Softcode is excellent at what it was intended for: simple things. It is a great tool for making an interactive object, a room with ambiance, simple global commands, simple economies and coded systems. However, once you start to try to write something like a complex combat system or a higher end economy, you’re likely to find yourself buried under a mountain of functions that span multiple objects across your entire code.
-
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they once were they can still stutter under the weight of more complex systems if not designed properly.
+
Not to mention, softcode is not an inherently fast language. It is not compiled, it is parsed with each calling of a function. While MUX and MUSH parsers have jumped light years ahead of where they once were, they can still stutter under the weight of more complex systems if those are not designed properly.
+
Also, Softcode is not a standardized language. Different servers each have their own slight variations. Code tools and resources are also limited to the documentation from those servers.
Now that starting text-based games is easy and an option for even the most technically inarticulate, new projects are a dime a dozen. People are starting new MUDs every day with varying levels of commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of small, one or two developer games, some of the benefit of softcode fades.
-
Softcode is great in that it allows a mid to large sized staff all work on the same game without stepping on one another’s toes. As mentioned before, shell access is not necessary to develop a MUX or a MUSH. However, now that we are seeing a lot more small, one or two-man shops, the issue of shell access and stepping on each other’s toes is a lot less.
+
Now that starting text-based games is easy and an option for even the most technically inarticulate, new projects are a dime a dozen. People are starting new MUDs every day with varying levels of commitment and ability. Because of this shift from fewer, larger, well-staffed games to a bunch of small, one or two developer games, the benefit of softcode fades.
+
Softcode is great in that it allows a mid to large sized staff all work on the same game without stepping on one another’s toes without shell access. However, the rise of modern code collaboration tools (such as private github/gitlab repos) has made it trivial to collaborate on code.
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and
-professional programming language. You code it using the conveniences of modern text editors.
-Evennia developers have access to the entire library of Python modules out there in the wild - not
-to mention the vast online help resources available. Python code is not bound to one-line functions
-on objects but complex systems may be organized neatly into real source code modules, sub-modules, or even broken out into entire Python packages as desired.
-
So what is not included in Evennia is a MUX/MOO-like online player-coding system. Advanced coding in Evennia is primarily intended to be done outside the game, in full-fledged Python modules. Advanced building is best handled by extending Evennia’s command system with your own sophisticated building commands. We feel that with a small development team you are better off using a professional source-control system (svn, git, bazaar, mercurial etc) anyway.
+
Evennia shuns in-game softcode for on-disk Python modules. Python is a popular, mature and professional programming language. Evennia developers have access to the entire library of Python modules out there in the wild - not to mention the vast online help resources available. Python code is not bound to one-line functions on objects; complex systems may be organized neatly into real source code modules, sub-modules, or even broken out into entire Python packages as desired.
+
So what is not included in Evennia is a MUX/MOO-like online player-coding system (aka Softcode). Advanced coding in Evennia is primarily intended to be done outside the game, in full-fledged Python modules (what MUSH would call ‘hardcode’). Advanced building is best handled by extending Evennia’s command system with your own sophisticated building commands.
+
In Evennia you develop your MU like you would any piece of modern software - using your favorite code editor/IDE and online code sharing tools.
Adding advanced and flexible building commands to your game is easy and will probably be enough to satisfy most creative builders. However, if you really, really want to offer online coding, there is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You could even re-implement MUX’ softcode in Python should you be very ambitious. The in-game-python is an optional pseudo-softcode plugin aimed at developers wanting to script their game from inside it.
+
Adding advanced and flexible building commands to your game is easy and will probably be enough to satisfy most creative builders. However, if you really, really want to offer online coding, there is of course nothing stopping you from adding that to Evennia, no matter our recommendations. You could even re-implement MUX’ softcode in Python should you be very ambitious.
+
In default Evennia, the Funcparser system allows for simple remapping of text on-demand without becomeing a full softcode language. The contribs has several tools and utililities to start from when adding more complex in-game building.
Evennia offers many convenient ways to store object data, such as via Attributes or Scripts. This is
-sufficient for most use cases. But if you aim to build a large stand-alone system, trying to squeeze
-your storage requirements into those may be more complex than you bargain for. Examples may be to
-store guild data for guild members to be able to change, tracking the flow of money across a game-
-wide economic system or implement other custom game systems that requires the storage of custom data
-in a quickly accessible way. Whereas Tags or Scripts can handle many situations,
-sometimes things may be easier to handle by adding your own database model.
+
Evennia offers many convenient ways to store object data, such as via Attributes or Scripts. This is sufficient for most use cases. But if you aim to build a large stand-alone system, trying to squeeze your storage requirements into those may be more complex than you bargain for. Examples may be to store guild data for guild members to be able to change, tracking the flow of money across a game-wide economic system or implement other custom game systems that requires the storage of custom data in a quickly accessible way.
+
Whereas Tags or Scripts can handle many situations, sometimes things may be easier to handle by adding your own database model.
SQL-type databases (which is what Evennia supports) are basically highly optimized systems for
@@ -129,9 +125,7 @@ retrieving text stored in tables. A table may look like this
2|Rock|evennia.DefaultObject|None...
-
Each line is considerably longer in your database. Each column is referred to as a “field” and every
-row is a separate object. You can check this out for yourself. If you use the default sqlite3
-database, go to your game folder and run
+
Each line is considerably longer in your database. Each column is referred to as a “field” and every row is a separate object. You can check this out for yourself. If you use the default sqlite3 database, go to your game folder and run
evennia dbshell
@@ -149,59 +143,38 @@ database, go to your game folder and run
sqlite> .exit
-
Evennia uses Django, which abstracts away the database SQL
-manipulation and allows you to search and manipulate your database entirely in Python. Each database
-table is in Django represented by a class commonly called a model since it describes the look of
-the table. In Evennia, Objects, Scripts, Channels etc are examples of Django models that we then
-extend and build on.
+
Evennia uses Django, which abstracts away the database SQL manipulation and allows you to search and manipulate your database entirely in Python. Each database table is in Django represented by a class commonly called a model since it describes the look of the table. In Evennia, Objects, Scripts, Channels etc are examples of Django models that we then extend and build on.
Here is how you add your own database table/models:
-
In Django lingo, we will create a new “application” - a subsystem under the main Evennia program.
-For this example we’ll call it “myapp”. Run the following (you need to have a working Evennia
-running before you do this, so make sure you have run the steps in [Setup Quickstart](Getting-
-Started) first):
+
In Django lingo, we will create a new “application” - a subsystem under the main Evennia program. For this example we’ll call it “myapp”. Run the following (you need to have a working Evennia running before you do this, so make sure you have run the steps in [Setup Quickstart](Getting- Started) first):
cd mygame/world
evennia startapp myapp
-
A new folder myapp is created. “myapp” will also be the name (the “app label”) from now on. We
-chose to put it in the world/ subfolder here, but you could put it in the root of your mygame if
-that makes more sense.
-
The myapp folder contains a few empty default files. What we are
-interested in for now is models.py. In models.py you define your model(s). Each model will be a
-table in the database. See the next section and don’t continue until you have added the models you
-want.
-
You now need to tell Evennia that the models of your app should be a part of your database
-scheme. Add this line to your mygame/server/conf/settings.pyfile (make sure to use the path where
-you put myapp and don’t forget the comma at the end of the tuple):
+
A new folder myapp is created. “myapp” will also be the name (the “app label”) from now on. We chose to put it in the world/ subfolder here, but you could put it in the root of your mygame if that makes more sense. 1. The myapp folder contains a few empty default files. What we are interested in for now is models.py. In models.py you define your model(s). Each model will be a table in the database. See the next section and don’t continue until you have added the models you want.
+
You now need to tell Evennia that the models of your app should be a part of your database scheme. Add this line to your mygame/server/conf/settings.pyfile (make sure to use the path where you put myapp and don’t forget the comma at the end of the tuple):
This will add your new database table to the database. If you have put your game under version
-control (if not, you should), don’t forget to gitaddmyapp/* to add all items
+
This will add your new database table to the database. If you have put your game under version control (if not, you should), don’t forget to gitaddmyapp/* to add all items
to version control.
A Django model is the Python representation of a database table. It can be handled like any other
-Python class. It defines fields on itself, objects of a special type. These become the “columns”
-of the database table. Finally, you create new instances of the model to add new rows to the
-database.
-
We won’t describe all aspects of Django models here, for that we refer to the vast Django
-documentation on the subject. Here is a
-(very) brief example:
+
A Django model is the Python representation of a database table. It can be handled like any other Python class. It defines fields on itself, objects of a special type. These become the “columns” of the database table. Finally, you create new instances of the model to add new rows to the database.
+
We won’t describe all aspects of Django models here, for that we refer to the vast Django documentation on the subject. Here is a (very) brief example:
fromdjango.dbimportmodelsclassMyDataStore(models.Model):
@@ -218,26 +191,44 @@ documentation on the subject. Here is a
We create four fields: two character fields of limited length and one text field which has no
maximum length. Finally we create a field containing the current time of us creating this object.
-
The db_date_created field, with exactly this name, is required if you want to be able to store
-instances of your custom model in an Evennia Attribute. It will automatically be set
-upon creation and can after that not be changed. Having this field will allow you to do e.g.
-obj.db.myinstance=mydatastore. If you know you’ll never store your model instances in Attributes
-the db_date_created field is optional.
+
The db_date_created field, with exactly this name, is required if you want to be able to store instances of your custom model in an Evennia Attribute. It will automatically be set upon creation and can after that not be changed. Having this field will allow you to do e.g. obj.db.myinstance=mydatastore. If you know you’ll never store your model instances in Attributes the db_date_created field is optional.
-
You don’t have to start field names with db_, this is an Evennia convention. It’s nevertheless
-recommended that you do use db_, partly for clarity and consistency with Evennia (if you ever want
-to share your code) and partly for the case of you later deciding to use Evennia’s
+
You don’t have to start field names with db_, this is an Evennia convention. It’s nevertheless recommended that you do use db_, partly for clarity and consistency with Evennia (if you ever want to share your code) and partly for the case of you later deciding to use Evennia’s
SharedMemoryModel parent down the line.
-
The field keyword db_index creates a database index for this field, which allows quicker
-lookups, so it’s recommended to put it on fields you know you’ll often use in queries. The
-null=True and blank=True keywords means that these fields may be left empty or set to the empty
-string without the database complaining. There are many other field types and keywords to define
-them, see django docs for more info.
-
Similar to using django-admin you
-are able to do evenniainspectdb to get an automated listing of model information for an existing
-database. As is the case with any model generating tool you should only use this as a starting
+
The field keyword db_index creates a database index for this field, which allows quicker lookups, so it’s recommended to put it on fields you know you’ll often use in queries. The null=True and blank=True keywords means that these fields may be left empty or set to the empty string without the database complaining. There are many other field types and keywords to define them, see django docs for more info.
+
Similar to using django-admin you are able to do evenniainspectdb to get an automated listing of model information for an existing database. As is the case with any model generating tool you should only use this as a starting
point for your models.
You may want to use ForeignKey or ManyToManyField to relate your new model to existing ones.
+
To do this we need to specify the app-path for the root object type we want to store as a string (we must use a string rather than the class directly or you’ll run into problems with models not having been initialized yet).
+
+
"objects.ObjectDB" for all Objects (like exits, rooms, characters etc)
It may seem counter-intuitive, but this will work correctly:
+
myspecial.db_character = my_character # a Character instance
+my_character = myspecial.db_character # still a Character
+
+
+
This works because when the .db_character field is loaded into Python, the entity itself knows that it’s supposed to be a Character and loads itself to that form.
+
The drawback of this is that the database won’t enforce the type of object you store in the relation. This is the price we pay for many of the other advantages of the Typeclass system.
+
While the db_character field fail if you try to store an Account, it will gladly accept any instance of a typeclass that inherits from ObjectDB, such as rooms, exits or other non-character Objects. It’s up to you to validate that what you store is what you expect it to be.
To create a new row in your table, you instantiate the model and then call its save() method:
@@ -251,70 +242,41 @@ point for your models.
-
Note that the db_date_created field of the model is not specified. Its flag at_now_add=True
-makes sure to set it to the current date when the object is created (it can also not be changed
-further after creation).
-
When you update an existing object with some new field value, remember that you have to save the
-object afterwards, otherwise the database will not update:
+
Note that the db_date_created field of the model is not specified. Its flag at_now_add=True makes sure to set it to the current date when the object is created (it can also not be changed further after creation).
+
When you update an existing object with some new field value, remember that you have to save the object afterwards, otherwise the database will not update:
Evennia’s normal models don’t need to explicitly save, since they are based on SharedMemoryModel
-rather than the raw django model. This is covered in the next section.
+
Evennia’s normal models don’t need to explicitly save, since they are based on SharedMemoryModel rather than the raw django model. This is covered in the next section.
Evennia doesn’t base most of its models on the raw django.db.models but on the Evennia base model
-evennia.utils.idmapper.models.SharedMemoryModel. There are two main reasons for this:
+
Evennia doesn’t base most of its models on the raw django.db.models.Model but on the Evennia base model evennia.utils.idmapper.models.SharedMemoryModel. There are two main reasons for this:
Ease of updating fields without having to explicitly call save()
On-object memory persistence and database caching
-
The first (and least important) point means that as long as you named your fields db_*, Evennia
-will automatically create field wrappers for them. This happens in the model’s
-Metaclass so there is no speed
-penalty for this. The name of the wrapper will be the same name as the field, minus the db_
-prefix. So the db_key field will have a wrapper property named key. You can then do:
+
The first (and least important) point means that as long as you named your fields db_*, Evennia will automatically create field wrappers for them. This happens in the model’s Metaclass so there is no speed penalty for this. The name of the wrapper will be the same name as the field, minus the db_ prefix. So the db_key field will have a wrapper property named key. You can then do:
my_datastore.key="Larger Sword"
-
and don’t have to explicitly call save() afterwards. The saving also happens in a more efficient
-way under the hood, updating only the field rather than the entire model using django optimizations.
-Note that if you were to manually add the property or method key to your model, this will be used
-instead of the automatic wrapper and allows you to fully customize access as needed.
-
To explain the second and more important point, consider the following example using the default
-Django model parent:
+
and don’t have to explicitly call save() afterwards. The saving also happens in a more efficient way under the hood, updating only the field rather than the entire model using django optimizations. Note that if you were to manually add the property or method key to your model, this will be used instead of the automatic wrapper and allows you to fully customize access as needed.
+
To explain the second and more important point, consider the following example using the default Django model parent:
shield=MyDataStore.objects.get(db_key="SmallShield")shield.cracked=True# where cracked is not a database field
The outcome of that last print statement is undefined! It could maybe randomly work but most
-likely you will get an AttributeError for not finding the cracked property. The reason is that
-cracked doesn’t represent an actual field in the database. It was just added at run-time and thus
-Django don’t care about it. When you retrieve your shield-match later there is no guarantee you
-will get back the same Python instance of the model where you defined cracked, even if you
-search for the same database object.
-
Evennia relies heavily on on-model handlers and other dynamically created properties. So rather than
-using the vanilla Django models, Evennia uses SharedMemoryModel, which levies something called
-idmapper. The idmapper caches model instances so that we will always get the same instance back
-after the first lookup of a given object. Using idmapper, the above example would work fine and you
-could retrieve your cracked property at any time - until you rebooted when all non-persistent data
-goes.
+
The outcome of that last print statement is undefined! It could maybe randomly work but most likely you will get an AttributeError for not finding the cracked property. The reason is that cracked doesn’t represent an actual field in the database. It was just added at run-time and thus Django don’t care about it. When you retrieve your shield-match later there is no guarantee you will get back the same Python instance of the model where you defined cracked, even if you search for the same database object.
+
Evennia relies heavily on on-model handlers and other dynamically created properties. So rather than using the vanilla Django models, Evennia uses SharedMemoryModel, which levies something called idmapper. The idmapper caches model instances so that we will always get the same instance back after the first lookup of a given object. Using idmapper, the above example would work fine and you could retrieve your cracked property at any time - until you rebooted when all non-persistent data goes.
Using the idmapper is both more intuitive and more efficient per object; it leads to a lot less
-reading from disk. The drawback is that this system tends to be more memory hungry overall. So if
-you know that you’ll never need to add new properties to running instances or know that you will
-create new objects all the time yet rarely access them again (like for a log system), you are
-probably better off making “plain” Django models rather than using SharedMemoryModel and its
-idmapper.
-
To use the idmapper and the field-wrapper functionality you just have to have your model classes
-inherit from evennia.utils.idmapper.models.SharedMemoryModel instead of from the default
-django.db.models.Model:
+reading from disk. The drawback is that this system tends to be more memory hungry overall. So if you know that you’ll never need to add new properties to running instances or know that you will create new objects all the time yet rarely access them again (like for a log system), you are probably better off making “plain” Django models rather than using SharedMemoryModel and its idmapper.
+
To use the idmapper and the field-wrapper functionality you just have to have your model classes inherit from evennia.utils.idmapper.models.SharedMemoryModel instead of from the default django.db.models.Model:
fromevennia.utils.idmapper.modelsimportSharedMemoryModelclassMyDataStore(SharedMemoryModel):
@@ -330,9 +292,7 @@ inherit from evenni
To search your new custom database table you need to use its database manager to build a query.
-Note that even if you use SharedMemoryModel as described in the previous section, you have to use
-the actual field names in the query, not the wrapper name (so db_key and not just key).
+
To search your new custom database table you need to use its database manager to build a query. Note that even if you use SharedMemoryModel as described in the previous section, you have to use the actual field names in the query, not the wrapper name (so db_key and not just key).
fromworld.myappimportMyDataStore# get all datastore objects exactly matching a given key
@@ -346,8 +306,7 @@ the actual field names in the query, not the wrapper name (so self.caller.msg(match.db_text)
NOTE - this tutorial is WIP and NOT complete! It was put on hold to focus on
-releasing Evennia 1.0. You will still learn things from it, but don’t expect
-perfection.
+
NOTE - this tutorial is WIP and NOT complete yet! You will still learn
+things from it, but don’t expect perfection.
A complete example MUD using Evennia. This is the final result of what is
-implemented if you follow the Getting-Started tutorial. It’s recommended
-that you follow the tutorial step by step and write your own code. But if
-you prefer you can also pick apart or use this as a starting point for your
-own game.
+implemented if you follow Part 3 of the Getting-Started tutorial.
+It’s recommended that you follow the tutorial step by step and write your own
+code. But if you prefer you can also pick apart or use this as a starting point
+for your own game.
diff --git a/docs/1.0/Contribs/Contribs-Overview.html b/docs/1.0/Contribs/Contribs-Overview.html
index 8a039e147e..08eda7318c 100644
--- a/docs/1.0/Contribs/Contribs-Overview.html
+++ b/docs/1.0/Contribs/Contribs-Overview.html
@@ -743,12 +743,11 @@ character make small verbal observations at irregular intervals.
NOTE - this tutorial is WIP and NOT complete! It was put on hold to focus on
-releasing Evennia 1.0. You will still learn things from it, but don’t expect
-perfection.
+
NOTE - this tutorial is WIP and NOT complete yet! You will still learn
+things from it, but don’t expect perfection.
diff --git a/docs/1.0/Contributing-Docs.html b/docs/1.0/Contributing-Docs.html
index a3cae85852..60d085e577 100644
--- a/docs/1.0/Contributing-Docs.html
+++ b/docs/1.0/Contributing-Docs.html
@@ -157,32 +157,20 @@
at the root of evennia/docs/source/.
source/Components/ are docs describing separate Evennia building blocks, that is, things
-that you can import and use. This extends and elaborates on what can be found out by reading
-the api docs themselves. Example are documentation for Accounts, Objects and Commands.
-
source/Concepts/ describes how larger-scale features of Evennia hang together - things that
-can’t easily be broken down into one isolated component. This can be general descriptions of
-how Models and Typeclasses interact to the path a message takes from the client to the server
-and back.
-
source/Setup/ holds detailed docs on installing, running and maintaining the Evennia server and
-the infrastructure around it.
-
source/Coding/ has help on how to interact with, use and navigate the Evennia codebase itself.
-This also has non-Evennia-specific help on general development concepts and how to set up a sane development environment.
+that you can import and use. This extends and elaborates on what can be found out by reading the api docs themselves. Example are documentation for Accounts, Objects and Commands.
+
source/Concepts/ describes how larger-scale features of Evennia hang together - things that can’t easily be broken down into one isolated component. This can be general descriptions of how Models and Typeclasses interact to the path a message takes from the client to the server and back.
+
source/Setup/ holds detailed docs on installing, running and maintaining the Evennia server and the infrastructure around it.
+
source/Coding/ has help on how to interact with, use and navigate the Evennia codebase itself. This also has non-Evennia-specific help on general development concepts and how to set up a sane development environment.
source/Contribs/ holds documentation specifically for packages in the evennia/contribs/ folder. Any contrib-specific tutorials will be found here instead of in Howtos
source/Howtos/ holds docs that describe how to achieve a specific goal, effect or
-result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the
-documentation for further reading.
-
+result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the documentation for further reading.
source/Howtos/Beginner-Tutorial/ holds all documents part of the initial tutorial sequence.
-
-
Other files and folders:
-
source/api/ contains the auto-generated API documentation as .html files. Don’t edit these
-files manually, they are auto-generated from sources.
+
source/api/ contains the auto-generated API documentation as .html files. Don’t edit these files manually, they are auto-generated from sources.
source/_templates and source/_static hold files for the doc itself. They should only be modified if wanting to change the look and structure of the documentation generation itself.
-
conf.py holds the Sphinx configuration. It should usually not be modified except to update
-the Evennia version on a new branch.
+
conf.py holds the Sphinx configuration. It should usually not be modified except to update the Evennia version on a new branch.
diff --git a/docs/1.0/Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.html b/docs/1.0/Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.html
index aaef11f666..0e2cc3339f 100644
--- a/docs/1.0/Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.html
+++ b/docs/1.0/Howtos/Beginner-Tutorial/Beginner-Tutorial-Overview.html
@@ -553,12 +553,108 @@ Click here to see the full index of all parts and lessons of the Beginner-Tutori
diff --git a/docs/1.0/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html b/docs/1.0/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
index 4d9e8997f3..ca603c4328 100644
--- a/docs/1.0/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
+++ b/docs/1.0/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
@@ -132,12 +132,10 @@ if we cannot find and use it afterwards.
accts=evennia.search_account(key="MyAccountName",email="foo@bar.com")
-
-```{sidebar} Querysets
-
-What is returned from the main search functions is actually a `queryset`. They can be treated like lists except that they can't modified in-place. We'll discuss querysets in the `next lesson` <Django-queries>`_.
-
-
+
Strings are always case-insensitive, so searching for "rose", "Rose" or "rOsE" give the same results. It’s important to remember that what is returned from these search methods is a listing of zero, one or more elements - all the matches to your search. To get the first match:
rose = roses[0]
@@ -187,7 +185,7 @@ What is returned from the main search functions is actually a `queryset`. They c
result=self.caller.search(query)ifnotresultreturn
- self.caller.msg(f"Found match for {query}: {foo}")
+ self.caller.msg(f"Found match for {query}: {foo}")
Remember, self.caller is the one calling the command. This is usually a Character, which
diff --git a/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
new file mode 100644
index 0000000000..65d4afe784
--- /dev/null
+++ b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.html b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.html
index d4ad8c94f9..5e8a2c45b7 100644
--- a/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.html
+++ b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.html
@@ -212,6 +212,29 @@ since it can also get confusing to follow the code.
# makes it easy for mobs to know to attack PCsis_pc=False
+ @property
+ defhurt_level(self):
+"""
+ String describing how hurt this character is.
+ """
+ percent=max(0,min(100,100*(self.hp/self.hp_max)))
+ if95<percent<=100:
+ return"|gPerfect|n"
+ elif80<percent<=95:
+ return"|gScraped|n"
+ elif60<percent<=80:
+ return"|GBruised|n"
+ elif45<percent<=60:
+ return"|yHurt|n"
+ elif30<percent<=45:
+ return"|yWounded|n"
+ elif15<percent<=30:
+ return"|rBadly wounded|n"
+ elif1<percent<=15:
+ return"|rBarely hanging on|n"
+ elifpercent==0:
+ return"|RCollapsed!|n"
+
defheal(self,hp):""" Heal hp amount of health, not allowing to exceed our max hp
@@ -229,6 +252,10 @@ since it can also get confusing to follow the code.
self.coins-=amountreturnamount
+ defat_attacked(self,attacker,**kwargs):
+"""Called when being attacked and combat starts."""
+ pass
+
defat_damage(self,damage,attacker=None):"""Called when attacked and taking damage."""self.hp-=damage
@@ -256,8 +283,8 @@ since it can also get confusing to follow the code.
-
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.
+
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.
@@ -310,21 +337,17 @@ in the mixin means we can expect these methods to be available for all living th
# 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!
+
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:
+The AttributeProperty is one way to add an Attribute in a field-like way; these will be accessible on every character in several ways:
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.
+
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.
This piece of code is worth some more explanation:
@@ -333,14 +356,9 @@ from the LivingMixi
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.
+
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. 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.
+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: "Youcollapseinaheap,alivebutbeaten."
Others in the room will see: "Thomascollapsesinaheap,alivebutbeaten."
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:
+
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:
# mygame/evadventure/rules.py classEvAdventureRollEngine:
diff --git a/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.html b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.html
index 0ca68079da..a3e804f707 100644
--- a/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.html
+++ b/docs/1.0/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Chargen.html
@@ -134,12 +134,7 @@ using a menu when they log in.
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
-
When doing this, connecting with the game with a new account will land you in “OOC” mode. The
-ooc-version of look (sitting in the Account cmdset) will show a list of available characters
-if you have any. You can also enter charcreate to make a new character. The charcreate is a
-simple command coming with Evennia that just lets you make a new character with a given name and
-description. We will later modify that to kick off our chargen. For now we’ll just keep in mind
-that’s how we’ll start off the menu.
+
When doing this, connecting with the game with a new account will land you in “OOC” mode. The ooc-version of look (sitting in the Account cmdset) will show a list of available characters if you have any. You can also enter charcreate to make a new character. The charcreate is a simple command coming with Evennia that just lets you make a new character with a given name and description. We will later modify that to kick off our chargen. For now we’ll just keep in mind that’s how we’ll start off the menu.
In Knave, most of the character-generation is random. This means this tutorial can be pretty
compact while still showing the basic idea. What we will create is a menu looking like this:
Silas
@@ -309,11 +304,8 @@ keep in here.
]
-
Here we have followed the Knave rulebook to randomize abilities, description and equipment.
-The dice.roll() and dice.roll_random_table methods now become very useful! Everything here
-should be easy to follow.
-
The main difference from baseline Knave is that we make a table of “starting weapon” (in Knave
-you can pick whatever you like).
+
Here we have followed the Knave rulebook to randomize abilities, description and equipment. The dice.roll() and dice.roll_random_table methods now become very useful! Everything here should be easy to follow.
+
The main difference from baseline Knave is that we make a table of “starting weapon” (in Knave you can pick whatever you like).
We also initialize .ability_changes=0. Knave only allows us to swap the values of two
Abilities once. We will use this to know if it has been done or not.
@@ -362,9 +354,7 @@ Abilities once. We will use this to know if it has been done or not.
-
The new show_sheet method collect the data from the temporary sheet and return it in a pretty
-form. Making a ‘template’ string like _TEMP_SHEET makes it easier to change things later if you want
-to change how things look.
+
The new show_sheet method collect the data from the temporary sheet and return it in a pretty form. Making a ‘template’ string like _TEMP_SHEET makes it easier to change things later if you want to change how things look.
@@ -421,8 +411,7 @@ This is a bit more involved.
returnnew_character
-
We use create_object to create a new EvAdventureCharacter. We feed it with all relevant data
-from the temporary character sheet. This is when these become an actual character.
+
We use create_object to create a new EvAdventureCharacter. We feed it with all relevant data from the temporary character sheet. This is when these become an actual character.
@@ -595,16 +573,13 @@ know this, we check the _update_name) to handle the user’s input.
For the (single) option, we use a special key named _default. This makes this option
a catch-all: If the user enters something that does not match any other option, this is
-the option that will be used.
-Since we have no other options here, we will always use this option no matter what the user enters.
+the option that will be used. Since we have no other options here, we will always use this option no matter what the user enters.
Also note that the goto part of the option points to the _update_name callable rather than to
the name of a node. It’s important we keep passing kwargs along to it!
When a user writes anything at this node, the _update_name callable will be called. This has
the same arguments as a node, but it is not a node - we will only use it to figure out which
node to go to next.
-
In _update_name we now have a use for the raw_string argument - this is what was written by
-the user on the previous node, remember? This is now either an empty string (meaning to ignore
-it) or the new name of the character.
+
In _update_name we now have a use for the raw_string argument - this is what was written by the user on the previous node, remember? This is now either an empty string (meaning to ignore it) or the new name of the character.
A goto-function like _update_name must return the name of the next node to use. It can also
optionally return the kwargs to pass into that node - we want to always do this, so we don’t
loose our temporary character sheet. Here we will always go back to the node_chargen.
@@ -698,13 +673,8 @@ node (such as WIS
In _swap_abilities, we need to analyze the raw_string from the user to see what they
want to do.
Most code in the helper is validating the user didn’t enter nonsense. If they did,
-we use caller.msg() to tell them and then return None,kwargs, which re-runs the same node (the
-name-selection) all over again.
-
Since we want users to be able to write “CON” instead of the longer “constitution”, we need a
-mapping _ABILITIES to easily convert between the two (it’s stored as consitution on the
-temporary character sheet). Once we know which abilities they want to swap, we do so and tick up
-the .ability_changes counter. This means this option will no longer be available from the main
-node.
+we use caller.msg() to tell them and then return None,kwargs, which re-runs the same node (the name-selection) all over again.
+
Since we want users to be able to write “CON” instead of the longer “constitution”, we need a mapping _ABILITIES to easily convert between the two (it’s stored as consitution on the temporary character sheet). Once we know which abilities they want to swap, we do so and tick up the .ability_changes counter. This means this option will no longer be available from the main node.
Finally, we return to node_chargen again.
@@ -725,12 +695,8 @@ node.
returntext,None
-
When entering the node, we will take the Temporary character sheet and use its .appy method to
-create a new Character with all equipment.
-
This is what is called an end node, because it returns None instead of options. After this,
-the menu will exit. We will be back to the default character selection screen. The characters
-found on that screen are the ones listed in the _playable_characters Attribute, so we need to
-also the new character to it.
+
When entering the node, we will take the Temporary character sheet and use its .appy method to create a new Character with all equipment.
+
This is what is called an end node, because it returns None instead of options. After this, the menu will exit. We will be back to the default character selection screen. The characters found on that screen are the ones listed in the _playable_characters Attribute, so we need to also the new character to it.
@@ -756,17 +722,12 @@ also the new character to it.
-
Now that we have all the nodes, we add them to the menutree we left empty before. We only add
-the nodes, not the goto-helpers! The keys we set in the menutree dictionary are the names we
-should use to point to nodes from inside the menu (and we did).
-
We also add a keyword argument startnode pointing to the node_chargen node. This tells EvMenu
-to first jump into that node when the menu is starting up.
+
Now that we have all the nodes, we add them to the menutree we left empty before. We only add the nodes, not the goto-helpers! The keys we set in the menutree dictionary are the names we should use to point to nodes from inside the menu (and we did).
+
We also add a keyword argument startnode pointing to the node_chargen node. This tells EvMenu to first jump into that node when the menu is starting up.
This lesson taught us how to use EvMenu to make an interactive character generator. In an RPG
-more complex than Knave, the menu would be bigger and more intricate, but the same principles
-apply.
+
This lesson taught us how to use EvMenu to make an interactive character generator. In an RPG more complex than Knave, the menu would be bigger and more intricate, but the same principles apply.
Together with the previous lessons we have now fished most of the basics around player
characters - how they store their stats, handle their equipment and how to create them.
In the next lesson we’ll address how EvAdventure Rooms work.
Combat is core to many games. Exactly how it works is very game-dependent. In this lesson we will build a framework to implement two common flavors:
+
+
“Twitch-based” combat (specific lesson here) means that you perform a combat action by entering a command, and after some delay (which may depend on your skills etc), the action happens. It’s called ‘twitch’ because actions often happen fast enough that changing your strategy may involve some element of quick thinking and a ‘twitchy trigger finger’.
+
“Turn-based” combat (specific lesson here) means that players input actions in clear turns. Timeout for entering/queuing your actions is often much longer than twitch-based style. Once everyone made their choice (or the timeout is reached), everyone’s action happens all at once, after which the next turn starts. This style of combat requires less player reflexes.
+
+
We will design a base combat system that supports both styles.
+
+
We need a CombatHandler to track the progress of combat. This will be a Script. Exactly how this works (and where it is stored) will be a bit different between Twitch- and Turnbased combat. We will create its common framework in this lesson.
+
Combat are divided into actions. We want to be able to easily extend our combat with more possible actions. An action needs Python code to show what actually happens when the action is performed. We will define such code in Action classes.
+
We also need a way to describe a specific instance of a given action. That is, when we do an “attack” action, we need at the minimum to know who is being attacked. For this will we use Python dicts that we will refer to as action_dicts.
Our “Combat Handler” will handle the administration around combat. It needs to be persistent (even is we reload the server your combat should keep going).
+
Creating the CombatHandler is a little of a catch-22 - how it works depends on how Actions and Action-dicts look. But without having the CombatHandler, it’s hard to know how to design Actions and Action-dicts. So we’ll start with its general structure and fill out the details later in this lesson.
+
Below, methods with pass will be filled out this lesson while those raising NotImplementedError will be different for Twitch/Turnbased combat and will be implemented in their respective lessons following this one.
+
# in evadventure/combat_base.py
+
+fromevenniaimportDefaultScript
+
+
+classCombatFailure(RuntimeError):
+"""If some error happens in combat"""
+ pass
+
+
+classEvAdventureCombatBaseHandler(DefaultSCript):
+"""
+ This should be created when combat starts. It 'ticks' the combat
+ and tracks all sides of it.
+
+ """
+ # common for all types of combat
+
+ action_classes={}# to fill in later
+ fallback_action_dict={}
+
+ @classmethod
+ defget_or_create_combathandler(cls,obj,**kwargs):
+""" Get or create combathandler on `obj`."""
+ pass
+
+ defmsg(self,message,combatant=None,broadcast=True,location=True):
+"""
+ Send a message to all combatants.
+
+ """
+ pass# TODO
+
+ defget_combat_summary(self,combatant):
+"""
+ Get a nicely formatted 'battle report' of combat, from the
+ perspective of the combatant.
+
+ """
+ pass# TODO
+
+ # implemented differently by Twitch- and Turnbased combat
+
+ defget_sides(self,combatant):
+"""
+ Get who's still alive on the two sides of combat, as a
+ tuple `([allies], [enemies])` from the perspective of `combatant`
+ (who is _not_ included in the `allies` list.
+
+ """
+ raiseNotImplementedError
+
+ defgive_advantage(self,recipient,target):
+"""
+ Give advantage to recipient against target.
+
+ """
+ raiseNotImplementedError
+
+ defgive_disadvantage(self,recipient,target):
+"""
+ Give disadvantage to recipient against target.
+
+ """
+ raiseNotImplementedError
+
+ defhas_advantage(self,combatant,target):
+"""
+ Does combatant have advantage against target?
+
+ """
+ raiseNotImplementedError
+
+ defhas_disadvantage(self,combatant,target):
+"""
+ Does combatant have disadvantage against target?
+
+ """
+ raiseNotImplementedError
+
+ defqueue_action(self,combatant,action_dict):
+"""
+ Queue an action for the combatant by providing
+ action dict.
+
+ """
+ raiseNotImplementedError
+
+ defexecute_next_action(self,combatant):
+"""
+ Perform a combatant's next action.
+
+ """
+ raiseNotImplementedError
+
+ defstart_combat(self):
+"""
+ Start combat.
+
+ """
+ raiseNotImplementedError
+
+ defcheck_stop_combat(self):
+"""
+ Check if the combat is over and if it should be stopped.
+
+ """
+ raiseNotImplementedError
+
+ defstop_combat(self):
+"""
+ Stop combat and do cleanup.
+
+ """
+ raiseNotImplementedError
+
+
+
+
+
The Combat Handler is a Script. Scripts are typeclassed entities, which means that they are persistently stored in the database. Scripts can optionally be stored “on” other objects (such as on Characters or Rooms) or be ‘global’ without any such connection. While Scripts has an optional timer component, it is not active by default and Scripts are commonly used just as plain storage. Since Scripts don’t have an in-game existence, they are great for storing data on ‘systems’ of all kinds, including our combat.
A helper method for quickly getting the combathandler for an ongoing combat and combatant.
+
We expect to create the script “on” an object (which one we don’t know yet, but we expect it to be a typeclassed entity).
+
# in evadventure/combat_base.py
+
+fromevenniaimportcreate_script
+
+# ...
+
+classEvAdventureCombatBaseHandler(DefaultScript):
+
+ # ...
+
+ @classmethod
+ defget_or_create_combathandler(cls,obj,**kwargs):
+"""
+ Get or create a combathandler on `obj`.
+
+ Args:
+ obj (any): The Typeclassed entity to store this Script on.
+ Keyword Args:
+ combathandler_key (str): Identifier for script. 'combathandler' by
+ default.
+ **kwargs: Extra arguments to the Script, if it is created.
+
+ """
+ ifnotobj:
+ raiseCombatFailure("Cannot start combat without a place to do it!")
+
+ combathandler_key=kwargs.pop("key","combathandler")
+ combathandler=obj.ndb.combathandler
+ ifnotcombathandlerornotcombathandler.id:
+ combathandler=obj.scripts.get(combathandler_key).first()
+ ifnotcombathandler:
+ # have to create from scratch
+ persistent=kwargs.pop("persistent",True)
+ combathandler=create_script(
+ cls,
+ key=combathandler_key,
+ obj=obj,
+ persistent=persistent,
+ **kwargs,
+ )
+ obj.ndb.combathandler=combathandler
+ returncombathandler
+
+ # ...
+
+
+
+
This helper method uses obj.scripts.get() to find if the combat script already exists ‘on’ the provided obj. If not, it will create it using Evennia’s create_script function. For some extra speed we cache the handler as obj.ndb.combathandler The .ndb. (non-db) means that handler is cached only in memory.
+
+
get_or_create_combathandler is decorated to be a classmethod, meaning it should be used on the handler class directly (rather than on an instance of said class). This makes sense because this method actually should return the new instance.
+
As a class method we’ll need to call this directly on the class, like this:
# in evadventure/combat_base.py
+
+# ...
+
+classEvAdventureCombatBaseHandler(DefaultScript):
+ # ...
+
+ defmsg(self,message,combatant=None,broadcast=True,location=None):
+"""
+ Central place for sending messages to combatants. This allows
+ for adding any combat-specific text-decoration in one place.
+
+ Args:
+ message (str): The message to send.
+ combatant (Object): The 'You' in the message, if any.
+ broadcast (bool): If `False`, `combatant` must be included and
+ will be the only one to see the message. If `True`, send to
+ everyone in the location.
+ location (Object, optional): If given, use this as the location to
+ send broadcast messages to. If not, use `self.obj` as that
+ location.
+
+ Notes:
+ If `combatant` is given, use `$You/you()` markup to create
+ a message that looks different depending on who sees it. Use
+ `$You(combatant_key)` to refer to other combatants.
+
+ """
+ ifnotlocation:
+ location=self.obj
+
+ location_objs=location.contents
+
+ exclude=[]
+ ifnotbroadcastandcombatant:
+ exclude=[objforobjinlocation_objsifobjisnotcombatant]
+
+ location.msg_contents(
+ message,
+ exclude=exclude,
+ from_obj=combatant,
+ mapping={locobj.key:locobjforlocobjinlocation_objs},
+ )
+
+ # ...
+
+
+
+
We saw the location.msg_contents() method before in the Weapon class of the Objects lesson. Its purpose is to take a string on the form "$You()dostuffagainst$you(key)" and make sure all sides see a string suitable just to them. Our msg() method will by default broadcast the message to everyone in the room.
# in evadventure/combat_base.py
+
+# ...
+
+fromevenniaimportEvTable
+
+# ...
+
+classEvAdventureCombatBaseHandler(DefaultScript):
+
+ # ...
+
+ defget_combat_summary(self,combatant):
+
+allies,enemies=self.get_sides(combatant)
+# we must include outselves at the top of the list (we are not returned from get_sides)
+allies.insert(0,combatant)
+nallies,nenemies=len(allies),len(enemies)
+
+ # prepare colors and hurt-levels
+allies=[f"{ally} ({ally.hurt_level})"forallyinallies]
+enemies=[f"{enemy} ({enemy.hurt_level})"forenemyinenemies]
+
+ # the center column with the 'vs'
+ vs_column=[""for_inrange(max(nallies,nenemies))]
+ vs_column[len(vs_column)//2]="|wvs|n"
+
+# the two allies / enemies columns should be centered vertically
+diff=abs(nallies-nenemies)
+ top_empty=diff//2
+ bot_empty=diff-top_empty
+ topfill=[""for_inrange(top_empty)]
+ botfill=[""for_inrange(bot_empty)]
+
+ ifnallies>=nenemies:
+ enemies=topfill+enemies+botfill
+ else:
+ allies=topfill+allies+botfill
+
+ # make a table with three columns
+returnevtable.EvTable(
+table=[
+ evtable.EvColumn(*allies,align="l"),
+ evtable.EvColumn(*vs_column,align="c"),
+ evtable.EvColumn(*enemies,align="r"),
+ ],
+ border=None,
+ maxwidth=78,
+ )
+
+ # ...
+
+
+
This may look complex, but the complexity is only in figuring out how to organize three columns, especially how to to adjust to the two sides on each side of the vs are roughly vertically aligned.
+
+
Line 15 : We make use of the self.get_sides(combatant) method which we haven’t actually implemented yet. This is because turn-based and twitch-based combat will need different ways to find out what the sides are. The allies and enemies are lists.
+
Line 17: The combatant is not a part of the allies list (this is how we defined get_sides to work), so we insert it at the top of the list (so they show first on the left-hand side).
Lines 28-39: We determine how to vertically center the two sides by adding empty lines above and below the content.
+
Line 41: The Evtable is an Evennia utility for making, well, text tables. Once we are happy with the columns, we feed them to the table and let Evennia do the rest. It’s worth to explore EvTable since it can help you create all sorts of nice layouts.
In EvAdventure we will only support a few common combat actions, mapping to the equivalent rolls and checks used in Knave. We will design our combat framework so that it’s easy to expand with other actions later.
+
+
hold - The simplest action. You just lean back and do nothing.
+
attack - You attack a given target using your currently equipped weapon. This will become a roll of STR or WIS against the targets’ ARMOR.
+
stunt - You make a ‘stunt’, which in roleplaying terms would mean you tripping your opponent, taunting or otherwise trying to gain the upper hand without hurting them. You can do this to give yourself (or an ally) advantage against a target on the next action. You can also give a targetdisadvantage against you or an ally for their next action.
+
useitem - You make use of a Consumable in your inventory. When used on yourself, it’d normally be something like a healing potion. If used on an enemy it could be a firebomb or a bottle of acid.
+
wield - You wield an item. Depending on what is being wielded, it will be wielded in different ways: A helmet will be placed on the head, a piece of armor on the chest. A sword will be wielded in one hand, a shield in another. A two-handed axe will use up two hands. Doing so will move whatever was there previously to the backpack.
+
flee - You run away/disengage. This action is only applicable in turn-based combat (in twitch-based combat you just move to another room to flee). We will thus wait to define this action until the Turnbased combat lesson.
To pass around the details of an attack (the second point above), we will use a dict. A dict is simple and also easy to save in an Attribute. We’ll call this the action_dict and here’s what we need for each action.
+
+
You don’t need to type these out anywhere, it’s listed here for reference. We will use these dicts when calling combathandler.queue_action(combatant,action_dict).
+
+
hold_action_dict={
+ "key":"hold"
+}
+attack_action_dict={
+ "key":"attack",
+ "target":<Character/NPC>
+}
+stunt_action_dict={
+ "key":"stunt",
+ "recipient":<Character/NPC>,# who gains advantage/disadvantage
+ "target":<Character/NPC>,# who the recipient gainst adv/dis against
+ "advantage":bool,# grant advantage or disadvantage?
+ "stunt_type":Ability,# Ability to use for the challenge
+ "defense_type":Ability,# what Ability for recipient to defend with if we
+ # are trying to give disadvantage
+}
+use_item_action_dict={
+ "key":"use",
+ "item":<Object>
+ "target":<Character/NPC/None># if using item against someone else
+}
+wield_action_dict={
+ "key":"wield",
+ "item":<Object>
+}
+
+# used only for the turnbased combat, so its Action will be defined there
+flee_action_dict={
+ "key":"flee"
+}
+
+
+
Apart from the stunt action, these dicts are all pretty simple. The key identifes the action to perform and the other fields identifies the minimum things you need to know in order to resolve each action.
+
We have not yet written the code to set these dicts, but we will assume that we know who is performing each of these actions. So if Beowulf attacks Grendel, Beowulf is not himself included in the attack dict:
Let’s explain the longest action dict, the Stunt action dict in more detail as well. In this example, The Trickster is performing a Stunt in order to help his friend Paladin to gain an INT- advantage against the Goblin (maybe the paladin is preparing to cast a spell of something). Since Trickster is doing the action, he’s not showing up in the dict:
This should result in an INT vs INT based check between the Trickster and the Goblin (maybe the trickster is trying to confuse the goblin with some clever word play). If the Trickster wins, the Paladin gains advantage against the Goblin on the Paladin’s next action .
We will create a new instance of this class every time an action is happening. So we store some key things every action will need - we will need a reference to the common combathandler (which we will design in the next section), and to the combatant (the one performing this action). The action_dict is a dict matching the action we want to perform.
+
The setattr Python standard function assigns the keys/values of the action_dict to be properties “on” this action. This is very convenient to use in other methods. So for the stunt action, other methods could just access self.key, self.recipient, self.target and so on directly.
+
# in evadventure/combat_base.py
+
+classCombatAction:
+
+ # ...
+
+ defmsg(self,message,broadcast=True):
+ "Send message to others in combat"
+ self.combathandler.msg(message,combatant=self.combatant,broadcast=broadcast)
+
+ defcan_use(self):
+"""Return False if combatant can's use this action right now"""
+ returnTrue
+
+ defexecute(self):
+"""Does the actional action"""
+ pass
+
+ defpost_execute(self):
+"""Called after `execute`"""
+ pass
+
+
+
It’s very common to want to send messages to everyone in combat - you need to tell people they are getting attacked, if they get hurt and so on. So having a msg helper method on the action is convenient. We offload all the complexity to the combathandler.msg() method.
+
The can_use, execute and post_execute should all be called in a chain and we should make sure the combathandler calls them like this:
Refer to how we designed Evadventure weapons to understand what happens here - most of the work is performed by the weapon class - we just plug in the relevant arguments.
# in evadventure/combat_base.py
+
+# ...
+
+classCombatActionStunt(CombatAction):
+"""
+ Perform a stunt the grants a beneficiary (can be self) advantage on their next action against a
+ target. Whenever performing a stunt that would affect another negatively (giving them
+ disadvantage against an ally, or granting an advantage against them, we need to make a check
+ first. We don't do a check if giving an advantage to an ally or ourselves.
+
+ action_dict = {
+ "key": "stunt",
+ "recipient": Character/NPC,
+ "target": Character/NPC,
+ "advantage": bool, # if False, it's a disadvantage
+ "stunt_type": Ability, # what ability (like STR, DEX etc) to use to perform this stunt.
+ "defense_type": Ability, # what ability to use to defend against (negative) effects of
+ this stunt.
+ }
+
+ """
+
+ defexecute(self):
+ combathandler=self.combathandler
+ attacker=self.combatant
+ recipient=self.recipient# the one to receive the effect of the stunt
+ target=self.target# the affected by the stunt (can be the same as recipient/combatant)
+ txt=""
+
+ ifrecipient==target:
+ # grant another entity dis/advantage against themselves
+ defender=recipient
+ else:
+ # recipient not same as target; who will defend depends on disadvantage or advantage
+ # to give.
+ defender=targetifself.advantageelserecipient
+
+ # trying to give advantage to recipient against target. Target defends against caller
+ is_success,_,txt=rules.dice.opposed_saving_throw(
+ attacker,
+ defender,
+ attack_type=self.stunt_type,
+ defense_type=self.defense_type,
+ advantage=combathandler.has_advantage(attacker,defender),
+ disadvantage=combathandler.has_disadvantage(attacker,defender),
+ )
+
+ self.msg(f"$You() $conj(attempt) stunt on $You({defender.key}). {txt}")
+
+ # deal with results
+ ifis_success:
+ ifself.advantage:
+ combathandler.give_advantage(recipient,target)
+ else:
+ combathandler.give_disadvantage(recipient,target)
+ ifrecipient==self.combatant:
+ self.msg(
+ f"$You() $conj(gain) {'advantage'ifself.advantageelse'disadvantage'} "
+ f"against $You({target.key})!"
+ )
+ else:
+ self.msg(
+ f"$You() $conj(cause) $You({recipient.key}) "
+ f"to gain {'advantage'ifself.advantageelse'disadvantage'} "
+ f"against $You({target.key})!"
+ )
+ self.msg(
+ "|yHaving succeeded, you hold back to plan your next move.|n [hold]",
+ broadcast=False,
+ )
+ else:
+ self.msg(f"$You({defender.key}) $conj(resist)! $You() $conj(fail) the stunt.")
+
+
+
+
The main action here is the call to the rules.dice.opposed_saving_throw to determine if the stunt succeeds. After that, most lines is about figuring out who should be given advantage/disadvantage and to communicate the result to the affected parties.
+
Note that we make heavy use of the helper methods on the combathandler here, even those that are not yet implemented. As long as we pass the action_dict into the combathandler, the action doesn’t actually care what happens next.
+
After we have performed a successful stunt, we queue the combathandler.fallback_action_dict. This is because stunts are meant to be one-off things are if we are repeating actions, it would not make sense to repeat the stunt over and over.
# in evadventure/combat_base.py
+
+# ...
+
+classCombatActionUseItem(CombatAction):
+"""
+ Use an item in combat. This is meant for one-off or limited-use items (so things like scrolls and potions, not swords and shields). If this is some sort of weapon or spell rune, we refer to the item to determine what to use for attack/defense rolls.
+
+ action_dict = {
+ "key": "use",
+ "item": Object
+ "target": Character/NPC/Object/None
+ }
+
+ """
+
+ defexecute(self):
+ item=self.item
+ user=self.combatant
+ target=self.target
+
+ ifitem.at_pre_use(user,target):
+ item.use(
+ user,
+ target,
+ advantage=self.combathandler.has_advantage(user,target),
+ disadvantage=self.combathandler.has_disadvantage(user,target),
+ )
+ item.at_post_use(user,target)
+
# in evadventure/combat_base.py
+
+# ...
+
+classCombatActionWield(CombatAction):
+"""
+ Wield a new weapon (or spell) from your inventory. This will
+ swap out the one you are currently wielding, if any.
+
+ action_dict = {
+ "key": "wield",
+ "item": Object
+ }
+
+ """
+
+ defexecute(self):
+ self.combatant.equipment.move(self.item)
+
+
+
+
We rely on the Equipment handler we created to handle the swapping of items for us. Since it doesn’t make sense to keep swapping over and over, we queue the fallback action after this one.
Unit testing the combat base classes can seem impossible because we have not yet implemented most of it. We can however get very far by the use of Mocks. The idea of a Mock is that you replace a piece of code with a dummy object (a ‘mock’) that can be called to return some specific value.
+
For example, consider this following test of the CombatHandler.get_combat_summary. We can’t just call this because it internally calls .get_sides which would raise a NotImplementedError.
# in evadventure/tests/test_combat.py
+
+fromunittest.mockimportMock
+
+fromevennia.utils.test_resourcesimportEvenniaTestCase
+fromevenniaimportcreate_object
+from..importcombat_base
+from..roomsimportEvAdventureRoom
+from..charactersimportEvAdventureCharacter
+
+
+classTestEvAdventureCombatBaseHandler(EvenniaTestCase):
+
+ defsetUp(self):
+
+ self.location=create_object(EvAdventureRoom,key="testroom")
+ self.combatant=create_object(EvAdventureCharacter,key="testchar")
+ self.target=create_object(EvAdventureMob,key="testmonster")
+
+ self.combathandler=combat_base.get_combat_summary(self.location)
+
+ deftest_get_combat_summary(self):
+
+ # do the test from perspective of combatant
+self.combathandler.get_sides=Mock(return_value=([],[self.target]))
+result=str(self.combathandler.get_combat_summary(self.combatant))
+ self.assertEqual(
+ result,
+ " testchar (Perfect) vs testmonster (Perfect)"
+ )
+ # test from the perspective of the monster
+self.combathandler.get_sides=Mock(return_value=([],[self.combatant]))
+result=str(self.combathandler.get_combat_summary(self.target))
+ self.assertEqual(
+ result,
+ " testmonster (Perfect) vs testchar (Perfect)"
+ )
+
+
+
The interesting places are where we apply the mocks:
+
+
Line 25 and Line 32: While get_sides is not implemented yet, we know what it is supposed to return - a tuple of lists. So for the sake of the test, we replace the get_sides method with a mock that when called will return something useful.
+
+
With this kind of approach it’s possible to fully test a system also when it’s not ‘complete’ yet.
We have the core functionality we need for our combat system! In the following two lessons we will make use of these building blocks to create two styles of combat.
In this lesson we will be building on the combat base to implement a combat system that works in turns and where you select your actions in a menu, like this:
Note that this documentation doesn’t show in-game colors. Also, if you interested in an alternative, see the previous lesson where we implemented a ‘twitch’-like combat system based on entering direct commands for every action.
+
+
With ‘turnbased’ combat, we mean combat that ‘ticks’ along at a slower pace, slow enough to allow the participants to select their options in a menu (the menu is not strictly necessary, but it’s a good way to learn how to make menus as well). Their actions are queued and will be executed when the turn timer runs out. To avoid waiting unnecessarily, we will also move on to the next round whenever everyone has made their choices.
+
The advantage of a turnbased system is that it removes player speed from the equation; your prowess in combat does not depend on how quickly you can enter a command. For RPG-heavy games you could also allow players time to make RP emotes during the rounds of combat to flesh out the action.
+
The advantage of using a menu is that you have all possible actions directly available to you, making it beginner friendly and easy to know what you can do. It also means a lot less writing which can be an advantage to some players.
Here is the general principle of the Turnbased combat handler:
+
+
The turnbased version of the CombatHandler will be stored on the current location. That means that there will only be one combat per location. Anyone else starting combat will join the same handler and be assigned a side to fight on.
+
The handler will run a central timer of 30s (in this example). When it fires, all queued actions will be executed. If everyone has submitted their actions, this will happen immediately when the last one submits.
+
While in combat you will not be able to move around - you are stuck in the room. Fleeing combat is a separate action that takes a few turns to complete (we will need to create this).
+
Starting the combat is done via the attack<target> command. After that you are in the combat menu and will use the menu for all subsequent actions.
Create a new module evadventure/combat_turnbased.py.
+
+
# in evadventure/combat_turnbased.py
+
+from.combat_baseimport(
+ CombatActionAttack,
+ CombatActionHold,
+ CombatActionStunt,
+ CombatActionUseItem,
+ CombatActionWield,
+ EvAdventureCombatBaseHandler,
+)
+
+from.combat_baseimportEvAdventureCombatBaseHandler
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ action_classes={
+ "hold":CombatActionHold,
+ "attack":CombatActionAttack,
+ "stunt":CombatActionStunt,
+ "use":CombatActionUseItem,
+ "wield":CombatActionWield,
+ "flee":None# we will add this soon!
+ }
+
+ # fallback action if not selecting anything
+ fallback_action_dict=AttributeProperty({"key":"hold"},autocreate=False)
+
+ # track which turn we are on
+ turn=AttributeProperty(0)
+ # who is involved in combat, and their queued action
+ # as {combatant: actiondict, ...}
+ combatants=AttributeProperty(dict)
+
+ # who has advantage against whom. This is a structure
+ # like {"combatant": {enemy1: True, enemy2: True}}
+ advantage_matrix=AttributeProperty(defaultdict(dict))
+ # same for disadvantages
+ disadvantage_matrix=AttributeProperty(defaultdict(dict))
+
+ # how many turns you must be fleeing before escaping
+ flee_timeout=AttributeProperty(1,autocreate=False)
+
+ # track who is fleeing as {combatant: turn_they_started_fleeing}
+ fleeing_combatants=AttributeProperty(dict)
+
+ # list of who has been defeated so far
+ defeated_combatants=AttributeProperty(list)
+
+
+
+
We leave a placeholder for the "flee" action since we haven’t created it yet.
+
Since the turnbased combat handler is shared between all combatants, we need to store references to those combatants on the handler, in the combatantsAttribute. In the same way we must store a matrix of who has advantage/disadvantage against whom. We must also track who is fleeing, in particular how long they have been fleeing, since they will be leaving combat after that time.
The two sides are different depending on if we are in an PvP room or not: In a PvP room everyone else is your enemy. Otherwise only NPCs in combat is your enemy (you are assumed to be teaming up with your fellow players).
+
# in evadventure/combat_turnbased.py
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defget_sides(self,combatant):
+"""
+ Get a listing of the two 'sides' of this combat,
+ m the perspective of the provided combatant.
+ """
+ ifself.obj.allow_pvp:
+ # in pvp, everyone else is an ememy
+ allies=[combatant]
+ enemies=[combforcombinself.combatantsifcomb!=combatant]
+ else:
+ # otherwise, enemies/allies depend on who combatant is
+ pcs=[combforcombinself.combatantsifinherits_from(comb,EvAdventureCharacter)]
+ npcs=[combforcombinself.combatantsifcombnotinpcs]
+ ifcombatantinpcs:
+ # combatant is a PC, so NPCs are all enemies
+ allies=[combforcombinpcsifcomb!=combatant]
+ enemies=npcs
+ else:
+ # combatant is an NPC, so PCs are all enemies
+ allies=[combforcombinnpcsifcomb!=combatant]
+ enemies=pcs
+ returnallies,enemies
+
+
+
Note that since the EvadventureCombatBaseHandler (which our turnbased handler is based on) is a Script, it provides many useful features. For example self.obj is the entity on which this Script ‘sits’. Since we are planning to put this handler on the current location, then self.obj will be that Room.
+
All we do here is check if it’s a PvP room or not and use this to figure out who would be an ally or an enemy. Note that the combatant is not included in the allies return - we’ll need to remember this.
We use the advantage/disadvantage_matrix Attributes to track who has advantage against whom.
+
+
In the hasdis/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.
+
We also consider everyone to have advantage against fleeing combatants.
Since the combat handler is shared we must be able to add- and remove combatants easily.
+This is new compared to the base handler.
+
# in evadventure/combat_turnbased.py
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defadd_combatant(self,combatant):
+"""
+ Add a new combatant to the battle. Can be called multiple times safely.
+ """
+ ifcombatantnotinself.combatants:
+ self.combatants[combatant]=self.fallback_action_dict
+ returnTrue
+ returnFalse
+
+ defremove_combatant(self,combatant):
+"""
+ Remove a combatant from the battle.
+ """
+ self.combatants.pop(combatant,None)
+ # clean up menu if it exists
+ # TODO!
+
+
+
We simply add the the combatant with the fallback action-dict to start with. We return a bool from add_combatant so that the calling function will know if they were actually added anew or not (we may want to do some extra setup if they are new).
+
For now we just pop the combatant, but in the future we’ll need to do some extra cleanup of the menu when combat ends (we’ll get to that).
Since you can’t just move away from the room to flee turnbased combat, we need to add a new CombatAction subclass like the ones we created in the base combat lesson.
+
# in evadventure/combat_turnbased.py
+
+from.combat_baseimportCombatAction
+
+# ...
+
+classCombatActionFlee(CombatAction):
+"""
+ Start (or continue) fleeing/disengaging from combat.
+
+ action_dict = {
+ "key": "flee",
+ }
+ """
+
+ defexecute(self):
+ combathandler=self.combathandler
+
+ ifself.combatantnotincombathandler.fleeing_combatants:
+ # we record the turn on which we started fleeing
+ combathandler.fleeing_combatants[self.combatant]=self.combathandler.turn
+
+ # show how many turns until successful flight
+ current_turn=combathandler.turn
+ started_fleeing=combathandler.fleeing_combatants[self.combatant]
+ flee_timeout=combathandler.flee_timeout
+ time_left=flee_timeout-(current_turn-started_fleeing)-1
+
+ iftime_left>0:
+ self.msg(
+ "$You() $conj(retreat), being exposed to attack while doing so (will escape in "
+ f"{time_left} $pluralize(turn, {time_left}))."
+ )
+
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ action_classes={
+ "hold":CombatActionHold,
+ "attack":CombatActionAttack,
+ "stunt":CombatActionStunt,
+ "use":CombatActionUseItem,
+ "wield":CombatActionWield,
+ "flee":CombatActionFlee# < ---- added!
+ }
+
+ # ...
+
+
+
We create the action to make use of the fleeing_combatants dict we set up in the combat handler. This dict stores the fleeing combatant along with the turn its fleeing started. If performing the flee action multiple times, we will just display how many turns are remaining.
+
Finally, we make sure to add our new CombatActionFlee to the action_classes registry on the combat handler.
# in evadventure/combat_turnbased.py
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defqueue_action(self,combatant,action_dict):
+ self.combatants[combatant]=action_dict
+
+ # track who inserted actions this turn (non-persistent)
+ did_action=set(self.ndb.did_actionorset())
+ did_action.add(combatant)
+ iflen(did_action)>=len(self.combatants):
+ # everyone has inserted an action. Start next turn without waiting!
+ self.force_repeat()
+
+
+
+
To queue an action, we simply store its action_dict with the combatant in the combatants Attribute.
+
We use a Python set() to track who has queued an action this turn. If all combatants have entered a new (or renewed) action this turn, we use the .force_repeat() method, which is available on all Scripts. When this is called, the next round will fire immediately instead of waiting until it times out.
# in evadventure/combat_turnbased.py
+
+importrandom
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defexecute_next_action(self,combatant):
+ # this gets the next dict and rotates the queue
+action_dict=self.combatants.get(combatant,self.fallback_action_dict)
+
+ # use the action-dict to select and create an action from an action class
+action_class=self.action_classes[action_dict["key"]]
+action=action_class(self,combatant,action_dict)
+
+ action.execute()
+ action.post_execute()
+
+ifaction_dict.get("repeat",False):
+# queue the action again *without updating the
+ # *.ndb.did_action list* (otherwise
+ # we'd always auto-end the turn if everyone used
+ # repeating actions and there'd be
+ # no time to change it before the next round)
+ self.combatants[combatant]=action_dict
+ else:
+ # if not a repeat, set the fallback action
+ self.combatants[combatant]=self.fallback_action_dict
+
+
+ defat_repeat(self):
+"""
+ This method is called every time Script repeats
+ (every `interval` seconds). Performs a full turn of
+ combat, performing everyone's actions in random order.
+ """
+ self.turn+=1
+ # random turn order
+ combatants=list(self.combatants.keys())
+random.shuffle(combatants)# shuffles in place
+
+ # do everyone's next queued combat action
+ forcombatantincombatants:
+ self.execute_next_action(combatant)
+
+self.ndb.did_action=set()
+
+ # check if one side won the battle
+ self.check_stop_combat()
+
+
+
Our action-execution consists of two parts - the execute_next_action (which was defined in the parent class for us to implement) and the at_repeat method which is a part of the Script
+
For execute_next_action :
+
+
Line 13: We get the action_dict from the combatants Attribute. We return the fallback_action_dict if nothing was queued (this defaults to hold).
+
Line 16: We use the key of the action_dict (which would be something like “attack”, “use”, “wield” etc) to get the class of the matching Action from the action_classes dictionary.
+
Line 17: Here the action class is instantiated with the combatant and action dict, making it ready to execute. This is then executed on the following lines.
+
Line 22: We introduce a new optional action-dict here, the boolean repeat key. This allows us to re-queue the action. If not the fallback action will be used.
+
+
The at_repeat is called repeatedly every interval seconds that the Script fires. This is what we use to track when each round ends.
+
+
Lines 43: In this example, we have no internal order between actions. So we simply randomize in which order they fire.
+
Line 49: This set was assigned to in the queue_action method to know when everyone submitted a new action. We must make sure to unset it here before the next round.
# in evadventure/combat_turnbased.py
+
+importrandom
+fromevennia.utils.utilsimportlist_to_string
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defstop_combat(self):
+"""
+ Stop the combat immediately.
+
+ """
+ forcombatantinself.combatants:
+ self.remove_combatant(combatant)
+ self.stop()
+ self.delete()
+
+ defcheck_stop_combat(self):
+"""Check if it's time to stop combat"""
+
+ # check if anyone is defeated
+ forcombatantinlist(self.combatants.keys()):
+ ifcombatant.hp<=0:
+# PCs roll on the death table here, NPCs die.
+# Even if PCs survive, they
+ # are still out of the fight.
+ combatant.at_defeat()
+ self.combatants.pop(combatant)
+ self.defeated_combatants.append(combatant)
+ self.msg("|r$You() $conj(fall) to the ground, defeated.|n",combatant=combatant)
+ else:
+ self.combatants[combatant]=self.fallback_action_dict
+
+ # check if anyone managed to flee
+ flee_timeout=self.flee_timeout
+ forcombatant,started_fleeinginself.fleeing_combatants.items():
+ifself.turn-started_fleeing>=flee_timeout-1:
+# if they are still alive/fleeing and have been fleeing long enough, escape
+ self.msg("|y$You() successfully $conj(flee) from combat.|n",combatant=combatant)
+ self.remove_combatant(combatant)
+
+ # check if one side won the battle
+ ifnotself.combatants:
+ # noone left in combat - maybe they killed each other or all fled
+surviving_combatant=None
+allies,enemies=(),()
+ else:
+ # grab a random survivor and check of they have any living enemies.
+ surviving_combatant=random.choice(list(self.combatants.keys()))
+ allies,enemies=self.get_sides(surviving_combatant)
+
+ ifnotenemies:
+ # if one way or another, there are no more enemies to fight
+ still_standing=list_to_string(f"$You({comb.key})"forcombinallies)
+ knocked_out=list_to_string(combforcombinself.defeated_combatantsifcomb.hp>0)
+killed=list_to_string(combforcombinself.defeated_combatantsifcomb.hp<=0)
+
+ ifstill_standing:
+ txt=[f"The combat is over. {still_standing} are still standing."]
+ else:
+ txt=["The combat is over. No-one stands as the victor."]
+ ifknocked_out:
+ txt.append(f"{knocked_out} were taken down, but will live.")
+ ifkilled:
+ txt.append(f"{killed} were killed.")
+ self.msg(txt)
+ self.stop_combat()
+
+
+
The check_stop_combat is called at the end of the round. We want to figure out who is dead and if one of the ‘sides’ won.
+
+
Lines 28-38: We go over all combatants and determine if they are out of HP. If so we fire the relevant hooks and add them to the defeated_combatants Attribute.
+
Line 38: For all surviving combatants, we make sure give them the fallback_action_dict.
+
Lines 41-46: The fleeing_combatant Attribute is a dict on the form {fleeing_combatant:turn_number}, tracking when they first started fleeing. We compare this with the current turn number and the flee_timeout to see if they now flee and should be allowed to be removed from combat.
+
Lines 49-56: Here on we are determining if one ‘side’ of the conflict has defeated the other side.
+
Line 60: The list_to_string Evennia utility converts a list of entries, like ["a","b","c" to a nice string "a,bandc". We use this to be able to present some nice ending messages to the combatants.
Since we are using the timer-component of the Script to tick our combat, we also need a helper method to ‘start’ that.
+
fromevennia.utils.utilsimportlist_to_string
+
+# in evadventure/combat_turnbased.py
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defstart_combat(self,**kwargs):
+"""
+ This actually starts the combat. It's safe to run this multiple times
+ since it will only start combat if it isn't already running.
+
+ """
+ ifnotself.is_active:
+ self.start(**kwargs)
+
+
+
+
The start(**kwargs) method is a method on the Script, and will make it start to call at_repeat every interval seconds. We will pass that interval inside kwargs (so for example, we’ll do combathandler.start_combat(interval=30) later).
The EvMenu used to create in-game menues in Evennia. We used a simple EvMenu already in the Character Generation Lesson. This time we’ll need to be a bit more advanced. While The EvMenu documentation describe its functionality in more detail, we will give a quick overview of how it works here.
+
An EvMenu is made up of nodes, which are regular functions on this form (somewhat simplified here, there are more options):
+
defnode_somenodename(caller,raw_string,**kwargs):
+
+ text="some text to show in the node"
+ options=[
+ {
+ "key":"Option 1",# skip this to get a number
+ "desc":"Describing what happens when choosing this option."
+ "goto":"name of the node to go to"# OR (callable, {kwargs}}) returning said name
+ },
+ # other options here
+ ]
+ returntext,options
+
+
+
So basically each node takes the arguments of caller (the one using the menu), raw_string (the empty string or what the user input on the previous node) and **kwargs which can be used to pass data from node to node. It returns text and options.
+
The text is what the user will see when entering this part of the menu, such as “Choose who you want to attack!”. The options is a list of dicts describing each option. They will appear as a multi-choice list below the node text (see the example at the top of this lesson page).
+
When we create the EvMenu later, we will create a node index - a mapping between a unique name and these “node functions”. So something like this:
+
# example of a EvMenu node index
+ {
+ "start":node_combat_main,
+ "node1":node_func1,
+ "node2":node_func2,
+ "some name":node_somenodename,
+ "end":node_abort_menu,
+ }
+
+
+
Each option dict has a key "goto" that determines which node the player should jump to if they choose that option. Inside the menu, each node needs to be referenced with these names (like "start", "node1" etc).
+
The "goto" value of each option can either specify the name directly (like "node1") or it can be given as a tuple (callable,{keywords}). This callable is called and is expected to in turn return the next node-name to use (like "node1").
+
The callable (often called a “goto callable”) looks very similar to a node function:
+
def_goto_when_choosing_option1(caller,raw_string,**kwargs):
+ # do whatever is needed to determine the next node
+ returnnodename# also nodename, dict works
+
+
+
+
Here, caller is still the one using the menu and raw_string is the actual string you entered to choose this option. **kwargs is the keywords you added to the (callable,{keywords}) tuple.
+
The goto-callable must return the name of the next node. Optionally, you can return both nodename,{kwargs}. If you do the next node will get those kwargs as ingoing **kwargs. This way you can pass information from one node to the next. A special feature is that if nodename is returned as None, then the current node will be rerun again.
+
Here’s a (somewhat contrived) example of how the goto-callable and node-function hang together:
Our combat menu will be pretty simple. We will have one central menu node with options indicating all the different actions of combat. When choosing an action in the menu, the player should be asked a series of question, each specifying one piece of information needed for that action. The last step will be the build this information into an action-dict we can queue with the combathandler.
+
To understand the process, here’s how the action selection will work (read left to right):
+
+
+
In base node
+
step 1
+
step 2
+
step 3
+
step 4
+
+
+
+
select attack
+
select target
+
queue action-dict
+
-
+
-
+
+
select stunt-giveadvantage
+
select Ability
+
select alliedrecipient
+
select enemytarget
+
queue action-dict
+
+
select stunt-givedisadvantage
+
select Ability
+
select enemyrecipient
+
select alliedtarget
+
queue action-dict
+
+
select useitemonyourselforally
+
select item from inventory
+
select alliedtarget
+
queue action-dict
+
-
+
+
select useitemonenemy
+
select item from inventory
+
select enemytarget
+
queue action-dict
+
-
+
+
select wield/swapitemfrominventory
+
select item from inventory`
+
queue action-dict
+
-
+
-
+
+
select flee
+
queue action-dict
+
-
+
-
+
-
+
+
select hold,doingnothing
+
queue action-dict
+
-
+
-
+
-
+
+
+
+
Looking at the above table we can see that we have a lot of re-use. The selection of allied/enemy/target/recipient/item represent nodes that can be shared by different actions.
+
Each of these actions also follow a linear sequence, like the step-by step ‘wizard’ you see in some software. We want to be able to step back and forth in each sequence, and also abort the action if you change your mind along the way.
+
After queueing the action, we should always go back to the base node where we will wait until the round ends and all actions are executed.
+
We will create a few helpers to make our particular menu easy to work with.
All callables are left as None since we haven’t created them yet. But it’s good to note down the expected names because we need them in order to jump from node to node. The important one to note is that node_combat will be the base node we should get back to over and over.
We only add this to not have to write as much when calling this later. We pass caller.location, which is what retrieves/creates the combathandler on the current location. The interval is how often the combathandler (which is a Script) will call its at_repeat method. We set the flee_time Attribute at the same time.
This is our first “goto function”. This will be called to actually queue our finished action-dict with the combat handler. After doing that, it should return us to the base node_combat.
We make one assumption here - that kwargs contains the action_dict key with the action-dict ready to go.
+
Since this is a goto-callable, we must return the next node to go to. Since this is the last step, we will always go back to the node_combat base node, so that’s what we return.
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:
+
# in the base combat-node function (just shown as an example)
+
+options=[
+ # ...
+ "desc":"use an item on an enemy",
+ "goto":(
+ _step_wizard,
+ {
+ "steps":["node_choose_use_item","node_choose_enemy_target"],
+ "action_dict":{"key":"use","item":None,"target":None},
+ }
+ )
+]
+
+
+
When the user chooses to use an item on an enemy, we will call _step_wizard with two keywords steps and action_dict. The first is the sequence of menu nodes we need to guide the player through in order to build up our action-dict.
+
The latter is the action_dict itself. Each node will gradually fill in the None places in this dict until we have a complete dict and can send it to the _queue_action goto function we defined earlier.
+
Furthermore, we want the ability to go “back” to the previous node like this:
+
# in some other node (shown only as an example)
+
+defsome_node(caller,raw_string,**kwargs):
+
+ # ...
+
+ options=[
+ # ...
+ {
+ "key":"back",
+ "goto":(_step_wizard,{**kwargs,**{"step":"back"}})
+ },
+ ]
+
+ # ...
+
+
+
Note the use of ** here. {**dict1,**dict2} is a powerful one-liner syntax to combine two dicts into one. This preserves (and passes on) the incoming kwargs and just adds a new key “step” to it. The end effect is similar to if we had done kwargs["step"]="back" on a separate line (except we end up with a newdict when using the **-approach).
+
So let’s implement a _step_wizard goto-function to handle this!
+
# in evadventure/combat_turnbased.py
+
+# ...
+
+def_step_wizard(caller,raw_string,**kwargs):
+
+ # get the steps and count them
+ steps=kwargs.get("steps",[])
+ nsteps=len(steps)
+
+ # track which step we are on
+ istep=kwargs.get("istep",-1)
+
+ # check if we are going back (forward is default)
+ step_direction=kwargs.get("step","forward")
+
+ ifstep_direction=="back":
+ # step back in wizard
+ ifistep<=0:
+ # back to the start
+ return"node_combat"
+ istep=kwargs["istep"]=istep-1
+ returnsteps[istep],kwargs
+ else:
+ # step to the next step in wizard
+ ifistep>=nsteps-1:
+ # we are already at end of wizard - queue action!
+ return_queue_action(caller,raw_string,**kwargs)
+ else:
+ # step forward
+ istep=kwargs["istep"]=istep+1
+ returnsteps[istep],kwargs
+
+
+
+
This depends on passing around steps, step and istep with the **kwargs. If step is “back” then we will go back in the sequence of steps otherwise forward. We increase/decrease the istep key value to track just where we are.
+
If we reach the end we call our _queue_action helper function directly. If we back up to the beginning we return to the base node.
+
We will make one final helper function, to quickly add the back (and abort) options to the nodes that need it:
This is not a goto-function, it’s just a helper that we will call to quickly add these extra options a node’s option list and not have to type it out over and over.
+
As we’ve seen before, the back option will use the _step_wizard to step back in the wizard. The abort option will simply jump back to the main node, aborting the wizard.
+
The _default option is special. This option key tells EvMenu: “use this option if none of the other match”. That is, if they enter an empty input or garbage, we will just re-display the node. We make sure pass along the kwargs though, so we don’t lose any information of where we were in the wizard.
These nodes all work the same: They should present a list of suitable targets/recipients to choose from and then put that result in the action-dict as either target or recipient key.
# in evadventure/combat_turnbased.py
+
+# ...
+
+defnode_choose_enemy_target(caller,raw_string,**kwargs):
+
+ text="Choose an enemy to target"
+
+ action_dict=kwargs["action_dict"]
+ combathandler=_get_combathandler(caller)
+_,enemies=combathandler.get_sides(caller)
+
+options=[
+{
+"desc":target.get_display_name(caller),
+"goto":(
+ _step_wizard,
+{**kwargs,**{"action_dict":{**action_dict,**{"target":target}}}},
+)
+ }
+ fortargetinenemies
+ ]
+options.extend(_get_default_wizard_options(caller,**kwargs))
+returntext,options
+
+
+defnode_choose_enemy_recipient(caller,raw_string,**kwargs):
+ # almost the same, except storing "recipient"
+
+
+defnode_choose_allied_target(caller,raw_string,**kwargs):
+ # almost the same, except using allies + yourself
+
+
+defnode_choose_allied_recipient(caller,raw_string,**kwargs):
+ # almost the same, except using allies + yourself and storing "recipient"
+
+
+
+
Line 11: Here we use combathandler.get_sides(caller) to get the ‘enemies’ from the perspective of caller (the one using the menu).
+
Line 13-31: This is a loop over all enemies we found.
+
+
Line 15: We use target.get_display_name(caller). This method (a default method on all Evennia Objects) allows the target to return a name while being aware of who’s asking. It’s what makes an admin see Name(#5) while a regular user just sees Name. If you didn’t care about that, you could just do target.key here.
+
Line 18: This line looks complex, but remember that {**dict1,**dict2} is a one-line way to merge two dicts together. What this does is to do this in three steps:
+
+
First we add action_dict together with a dict {"target":target}. This has the same effect as doing action_dict["target"]=target, except we create a new dict out of the merger.
+
Next we take this new merger and creates a new dict {"action_dict":new_action_dict}.
+
Finally we merge this with the existing kwargs dict. The result is a new dict that now has the updated "action_dict" key pointing to an action-dict where target is set.
+
+
+
+
+
Line 23: We extend the options list with the default wizard options (back, abort). Since we made a helper function for this, this is only one line.
+
+
Creating the three other needed nodes node_choose_enemy_recipient, node_choose_allied_target and node_choose_allied_recipient are following the same pattern; they just use either the allies or enemies return from combathandler.get_sides() (for the allies, don’t forget to add caller so you can target yourself!). It then sets either the target or recipient field in the action_dict. We leave these up to the reader to implement.
The principle is the same as for the target/recipient-setter nodes, except that we just provide a list of the abilities to choose from. We update the stunt_type and defense_type keys in the action_dict, as needed by the Stunt action.
# in evadventure/combat_turnbased.py
+
+# ...
+
+defnode_choose_use_item(caller,raw_string,**kwargs):
+ text="Select the item"
+ action_dict=kwargs["action_dict"]
+
+ options=[
+ {
+ "desc":item.get_display_name(caller),
+ "goto":(
+ _step_wizard,
+ {**kwargs,**{"action_dict":{**action_dict,**{"item":item}}}},
+ ),
+ }
+ foritemincaller.equipment.get_usable_objects_from_backpack()
+ ]
+ ifnotoptions:
+ text="There are no usable items in your inventory!"
+
+ options.extend(_get_default_wizard_options(caller,**kwargs))
+ returntext,options
+
+
+defnode_choose_wield_item(caller,raw_string,**kwargs):
+ # same except using caller.equipment.get_wieldable_objects_from_backpack()
+
+
+
+
Our equipment handler 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.
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.
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.
+
# in evadventure/combat_turnbased.py
+
+fromevenniaimportCommand,CmdSet,EvMenu
+
+# ...
+
+classCmdTurnAttack(Command):
+"""
+ Start or join combat.
+
+ Usage:
+ attack [<target>]
+
+ """
+
+ key="attack"
+ aliases=["hit","turnbased combat"]
+
+ turn_timeout=30# seconds
+ flee_time=3# rounds
+
+ defparse(self):
+ super().parse()
+ self.args=self.args.strip()
+
+ deffunc(self):
+ ifnotself.args:
+ self.msg("What are you attacking?")
+ return
+
+ target=self.caller.search(self.args)
+ ifnottarget:
+ return
+
+ ifnothasattr(target,"hp"):
+ self.msg("You can't attack that.")
+ return
+
+ eliftarget.hp<=0:
+ self.msg(f"{target.get_display_name(self.caller)} is already down.")
+ return
+
+ iftarget.is_pcandnottarget.location.allow_pvp:
+ self.msg("PvP combat is not allowed here!")
+ return
+
+ combathandler=_get_combathandler(
+ self.caller,self.turn_timeout,self.flee_time)
+
+ # add combatants to combathandler. this can be done safely over and over
+ combathandler.add_combatant(self.caller)
+ combathandler.queue_action(self.caller,{"key":"attack","target":target})
+ combathandler.add_combatant(target)
+ target.msg("|rYou are attacked by {self.caller.get_display_name(self.caller)}!|n")
+ combathandler.start_combat()
+
+ # build and start the menu
+ EvMenu(
+ self.caller,
+ {
+ "node_choose_enemy_target":node_choose_enemy_target,
+ "node_choose_allied_target":node_choose_allied_target,
+ "node_choose_enemy_recipient":node_choose_enemy_recipient,
+ "node_choose_allied_recipient":node_choose_allied_recipient,
+ "node_choose_ability":node_choose_ability,
+ "node_choose_use_item":node_choose_use_item,
+ "node_choose_wield_item":node_choose_wield_item,
+ "node_combat":node_combat,
+ },
+ startnode="node_combat",
+ combathandler=combathandler,
+ auto_look=False,
+ # cmdset_mergetype="Union",
+ persistent=True,
+ )
+
+
+classTurnCombatCmdSet(CmdSet):
+"""
+ CmdSet for the turn-based combat.
+ """
+
+ defat_cmdset_creation(self):
+ self.add(CmdTurnAttack())
+
+
+
The attacktarget Command will determine if the target has health (only things with health can be attacked) and that the room allows fighting. If the target is a pc, it will check so PvP is allowed.
+
It then proceeds to either start up a new command handler or reuse a new one, while adding the attacker and target to it. If the target was already in combat, this does nothing (same with the .start_combat() call).
+
As we create the EvMenu, we pass it the “menu index” we talked to about earlier, now with the actual node functions in every slot. We make the menu persistent so it survives a reload.
+
To make the command available, add the TurnCombatCmdSet to the Character’s default cmdset.
The combat can end for a bunch of reasons. When that happens, we must make sure to clean up the menu so we go back normal operation. We will add this to the remove_combatant method on the combat handler (we left a TODO there before):
+
+# in evadventure/combat_turnbased.py
+
+# ...
+
+classEvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+ defremove_combatant(self,combatant):
+"""
+ Remove a combatant from the battle.
+ """
+ self.combatants.pop(combatant,None)
+ # clean up menu if it exists
+ ifcombatant.ndb._evmenu:# <--- new
+ combatant.ndb._evmenu.close_menu()# ''
+
+
+
+
When the evmenu is active, it is avaiable on its user as .ndb._evmenu (see the EvMenu docs). When we are removed from combat, we use this to get the evmenu and call its close_menu() method to shut down the menu.
Unit testing of the Turnbased combat handler is straight forward, you follow the process of earlier lessons to test that each method on the handler returns what you expect with mocked inputs.
Unit testing the code is not enough to see that combat works. We need to also make a little ‘functional’ test to see how it works in practice.
+
This is what we need for a minimal test:
+
+
A room with combat enabled.
+
An NPC to attack (it won’t do anything back yet since we haven’t added any AI)
+
A weapon we can wield.
+
An item (like a potion) we can use.
+
+
+
In The Twitch combat lesson we used a batch-command script to create the testing environment in game. This runs in-game Evennia commands in sequence. For demonstration purposes we’ll instead use a batch-code script, which runs raw Python code in a repeatable way. A batch-code script is much more flexible than a batch-command script.
+
+
Create a new subfolder evadventure/batchscripts/ (if it doesn’t already exist)
+
+
+
Create a new Python module evadventure/batchscripts/combat_demo.py
+
+
A batchcode file is a valid Python module. The only difference is that it has a #HEADER block and one or more #CODE sections. When the processor runs, the #HEADER part will be added on top of each #CODE part before executing that code block in isolation. Since you can run the file from in-game (including refresh it without reloading the server), this gives the ability to run longer Python codes on-demand.
+
# Evadventure (Turnbased) combat demo - using a batch-code file.
+#
+# Sets up a combat area for testing turnbased combat.
+#
+# First add mygame/server/conf/settings.py:
+#
+# BASE_BATCH_PROCESS_PATHS += ["evadventure.batchscripts"]
+#
+# Run from in-game as `batchcode turnbased_combat_demo`
+#
+
+# HEADER
+
+fromevenniaimportDefaultExit,create_object,search_object
+fromevennia.contrib.tutorials.evadventure.charactersimportEvAdventureCharacter
+fromevennia.contrib.tutorials.evadventure.combat_turnbasedimportTurnCombatCmdSet
+fromevennia.contrib.tutorials.evadventure.npcsimportEvAdventureNPC
+fromevennia.contrib.tutorials.evadventure.roomsimportEvAdventureRoom
+
+# CODE
+
+# Make the player an EvAdventureCharacter
+player=caller# caller is injected by the batchcode runner, it's the one running this script # E: undefined name 'caller'
+player.swap_typeclass(EvAdventureCharacter)
+
+# add the Turnbased cmdset
+player.cmdset.add(TurnCombatCmdSet,persistent=True)
+
+# create a weapon and an item to use
+create_object(
+ "contrib.tutorials.evadventure.objects.EvAdventureWeapon",
+ key="Sword",
+ location=player,
+ attributes=[("desc","A sword.")],
+)
+
+create_object(
+ "contrib.tutorials.evadventure.objects.EvAdventureConsumable",
+ key="Potion",
+ location=player,
+ attributes=[("desc","A potion.")],
+)
+
+# start from limbo
+limbo=search_object("#2")[0]
+
+arena=create_object(EvAdventureRoom,key="Arena",attributes=[("desc","A large arena.")])
+
+# Create the exits
+arena_exit=create_object(DefaultExit,key="Arena",location=limbo,destination=arena)
+back_exit=create_object(DefaultExit,key="Back",location=arena,destination=limbo)
+
+# create the NPC dummy
+create_object(
+ EvAdventureNPC,
+ key="Dummy",
+ location=arena,
+ attributes=[("desc","A training dummy."),("hp",1000),("hp_max",1000)],
+)
+
+
+
+
If editing this in an IDE, you may get errors on the player=caller line. This is because caller is not defined anywhere in this file. Instead caller (the one running the script) is injected by the batchcode runner.
+
But apart from the #HEADER and #CODE specials, this just a series of normal Evennia api calls.
+
Log into the game with a developer/superuser account and run
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.)
+
You can now try attackdummy and should be able to pound away at the dummy (lower its health to test destroying it). If you need to fix something, use q to exit the menu and get access to the reload command (for the final combat, you can disable this ability by passing auto_quit=False when you create the EvMenu).
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.
+
Before our combat system is actually usable, we need our enemies to actually fight back. We’ll get to that next.
Note that this documentation doesn’t show in-game colors. If you are interested in an alternative, see the next lesson, where we’ll make a turnbased, menu-based system instead.
+
+
With “Twitch” combat, we refer to a type of combat system that runs without any clear divisions of ‘turns’ (the opposite of Turn-based combat). It is inspired by the way combat worked in the old DikuMUD codebase, but is more flexible.
+
+
Basically, a user enters an action and after a certain time that action will execute (normally an attack). If they don’t do anything, the attack will repeat over and over (with a random result) until the enemy or you is defeated.
+
You can change up your strategy by performing other actions (like drinking a potion or cast a spell). You can also simply move to another room to ‘flee’ the combat (but the enemy may of course follow you)
Here is the general design of the Twitch-based combat handler:
+
+
The twitch-version of the CombatHandler will be stored on each combatant whenever combat starts. When combat is over, or they leave the room with combat, the handler will be deleted.
+
The handler will queue each action independently, starting a timer until they fire.
We will make use of the Combat Actions, Action dicts and the parent EvAdventureCombatBaseHandlerwe created previously.
+
# in evadventure/combat_twitch.py
+
+from.combat_baseimport(
+ CombatActionAttack,
+ CombatActionHold,
+ CombatActionStunt,
+ CombatActionUseItem,
+ CombatActionWield,
+ EvAdventureCombatBaseHandler,
+)
+
+from.combat_baseimportEvAdventureCombatBaseHandler
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+"""
+ This is created on the combatant when combat starts. It tracks only
+ the combatant's side of the combat and handles when the next action
+ will happen.
+
+ """
+
+ defmsg(self,message,broadcast=True):
+"""See EvAdventureCombatBaseHandler.msg"""
+ super().msg(message,combatant=self.obj,
+ broadcast=broadcast,location=self.obj.location)
+
+
+
We make a child class of EvAdventureCombatBaseHandler for our Twitch combat. The parent class is a Script, and when a Script sits ‘on’ an Object, that Object is available on the script as self.obj. Since this handler is meant to sit ‘on’ the combatant, then self.obj is thus the combatant and self.obj.location is the current room the combatant is in. By using super() we can reuse the parent class’ msg() method with these Twitch-specific details.
# in evadventure/combat_twitch.py
+
+fromevennia.utilsimportinherits_from
+
+# ...
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defget_sides(self,combatant):
+"""
+ Get a listing of the two 'sides' of this combat, from the
+ perspective of the provided combatant. The sides don't need
+ to be balanced.
+
+ Args:
+ combatant (Character or NPC): The basis for the sides.
+
+ Returns:
+ tuple: A tuple of lists `(allies, enemies)`, from the
+ perspective of `combatant`. Note that combatant itself
+ is not included in either of these.
+
+ """
+ # get all entities involved in combat by looking up their combathandlers
+ combatants=[
+ comb
+ forcombinself.obj.location.contents
+ ifhasattr(comb,"scripts")andcomb.scripts.has(self.key)
+ ]
+ location=self.obj.location
+
+ ifhasattr(location,"allow_pvp")andlocation.allow_pvp:
+ # in pvp, everyone else is an enemy
+ allies=[combatant]
+ enemies=[combforcombincombatantsifcomb!=combatant]
+ else:
+ # otherwise, enemies/allies depend on who combatant is
+ pcs=[combforcombincombatantsifinherits_from(comb,EvAdventureCharacter)]
+ npcs=[combforcombincombatantsifcombnotinpcs]
+ ifcombatantinpcs:
+ # combatant is a PC, so NPCs are all enemies
+ allies=[combforcombinpcsifcomb!=combatant]
+ enemies=npcs
+ else:
+ # combatant is an NPC, so PCs are all enemies
+ allies=[combforcombinnpcsifcomb!=combatant]
+ enemies=pcs
+ returnallies,enemies
+
+
+
+
Next we add our own implementation of the get_sides() method. This presents the sides of combat from the perspective of the provided combatant. In Twitch combat, there are a few things that identifies a combatant:
+
+
That they are in the same location
+
That they each have a EvAdventureCombatTwitchHandler script running on themselves
+
+
+
In a PvP-open room, it’s all for themselves - everyone else is considered an ‘enemy’. Otherwise we separate PCs from NPCs by seeing if they inherit from EvAdventureCharacter (our PC class) or not - if you are a PC, then the NPCs are your enemies and vice versa. The inherits_from is very useful for doing these checks - it will pass also if you inherit from EvAdventureCharacter at any distance.
+
Note that allies does not include the combatant itself, so if you are fighting a lone enemy, the return from this method will be ([],[enemy_obj]).
# in evadventure/combat_twitch.py
+
+fromevenniaimportAttributeProperty
+
+# ...
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+ self.advantage_against=AttributeProperty(dict)
+ self.disadvantage_against=AttributeProperty(dict)
+
+ # ...
+
+ defgive_advantage(self,recipient,target):
+"""Let a recipient gain advantage against the target."""
+ self.advantage_against[target]=True
+
+ defgive_disadvantage(self,recipient,target):
+"""Let an affected party gain disadvantage against a target."""
+ self.disadvantage_against[target]=True
+
+ defhas_advantage(self,combatant,target):
+"""Check if the combatant has advantage against a target."""
+ returnself.advantage_against.get(target,False)
+
+ defhas_disadvantage(self,combatant,target):
+"""Check if the combatant has disadvantage against a target."""
+ returnself.disadvantage_against.get(target,False)1
+
+
+
+
As seen in the previous lesson, the Actions call these methods to store the fact that
+a given combatant has advantage.
+
In this Twitch-combat case, the one getting the advantage is always one on which the combathandler is defined, so we don’t actually need to use the recipient/combatant argument (it’s always going to be self.obj) - only target is important.
+
We create two new Attributes to store the relation as dicts.
# in evadventure/combat_twitch.py
+
+fromevennia.utilsimportrepeat,unrepeat
+from.combat_baseimport(
+ CombatActionAttack,
+ CombatActionHold,
+ CombatActionStunt,
+ CombatActionUseItem,
+ CombatActionWield,
+ EvAdventureCombatBaseHandler,
+)
+
+# ...
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+action_classes={
+"hold":CombatActionHold,
+ "attack":CombatActionAttack,
+ "stunt":CombatActionStunt,
+ "use":CombatActionUseItem,
+ "wield":CombatActionWield,
+ }
+
+ action_dict=AttributeProperty(dict,autocreate=False)
+current_ticker_ref=AttributeProperty(None,autocreate=False)
+
+ # ...
+
+defqueue_action(self,action_dict,combatant=None):
+"""
+ Schedule the next action to fire.
+
+ Args:
+ action_dict (dict): The new action-dict to initialize.
+ combatant (optional): Unused.
+
+ """
+ ifaction_dict["key"]notinself.action_classes:
+ self.obj.msg("This is an unkown action!")
+ return
+
+# store action dict and schedule it to run in dt time
+self.action_dict=action_dict
+dt=action_dict.get("dt",0)
+
+ ifself.current_ticker_ref:
+# we already have a current ticker going - abort it
+unrepeat(self.current_ticker_ref)
+ifdt<=0:
+ # no repeat
+ self.current_ticker_ref=None
+ else:
+ # always schedule the task to be repeating, cancel later
+ # otherwise. We store the tickerhandler's ref to make sure
+ # we can remove it later
+ self.current_ticker_ref=repeat(
+ dt,self.execute_next_action,id_string="combat")
+
+
+
+
Line 30: The queue_action method takes an “Action dict” representing an action the combatant wants to perform next. It must be one of the keyed Actions added to the handler in the action_classes property (Line 17). We make no use of the combatant keyword argument since we already know that the combatant is self.obj.
+
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 and evennia.utils.utils.unrepeat are convenient shortcuts to the TickerHandler. 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 thecurrent_ticket_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).
# in evadventure/combat_twitch.py
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+fallback_action_dict=AttributeProperty({"key":"hold","dt":0})
+
+ # ...
+
+ defexecute_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)
+
+ifaction.can_use():
+action.execute()
+ action.post_execute()
+
+ifnotaction_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).
+
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.
+
+
+
+
Line 18: Here we run through the usage methods of the action - where we perform the action. We let the action itself handle all the logics.
+
Line 22: We check for another optional flag on the action-dict: repeat. Unless it’s set, we use the fallback-action defined on Line 5. Many actions should not repeat - for example, it would not make sense to do wield for the same weapon over and over.
+
Line 27: It’s important that we know how to stop combat. We will write this method next.
# in evadventure/combat_twitch.py
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+ defcheck_stop_combat(self):
+"""
+ Check if the combat is over.
+ """
+
+allies,enemies=self.get_sides(self.obj)
+allies.append(self.obj)
+
+ location=self.obj.location
+
+ # only keep combatants that are alive and still in the same room
+allies=[combforcombinalliesifcomb.hp>0andcomb.location==location]
+enemies=[combforcombinenemiesifcomb.hp>0andcomb.location==location]
+
+ ifnotalliesandnotenemies:
+ self.msg("The combat is over. Noone stands.",broadcast=False)
+ self.stop_combat()
+ return
+ ifnotallies:
+ self.msg("The combat is over. You lost.",broadcast=False)
+ self.stop_combat()
+ ifnotenemies:
+ self.msg("The combat is over. You won!",broadcast=False)
+ self.stop_combat()
+
+ defstop_combat(self):
+ pass# We'll finish this last
+
+
+
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. Note that combatant is not included among the allies, so we need to add it back in on the following line.
+
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.
We should try to find the similarities between the commands we’ll need and group them into one parent class. When a Command fires, it will fire the following methods on itself, in sequence:
# in evadventure/combat_twitch.py
+
+fromevenniaimportCommand
+fromevenniaimportInterruptCommand
+
+# ...
+
+# after the combat handler class
+
+class_BaseTwitchCombatCommand(Command):
+"""
+ Parent class for all twitch-combat commnads.
+
+ """
+
+ defat_pre_command(self):
+"""
+ Called before parsing.
+
+ """
+ ifnotself.caller.locationornotself.caller.location.allow_combat:
+ self.msg("Can't fight here!")
+raiseInterruptCommand()
+
+ defparse(self):
+"""
+ Handle parsing of most supported combat syntaxes (except stunts).
+
+ <action> [<target>|<item>]
+ or
+ <action> <item> [on] <target>
+
+ Use 'on' to differentiate if names/items have spaces in the name.
+
+ """
+ self.args=args=self.args.strip()
+ self.lhs,self.rhs="",""
+
+ ifnotargs:
+ return
+
+ if" on "inargs:
+ lhs,rhs=args.split(" on ",1)
+ else:
+ lhs,*rhs=args.split(None,1)
+ rhs=" ".join(rhs)
+ self.lhs,self.rhs=lhs.strip(),rhs.strip()
+
+defget_or_create_combathandler(self,target=None,combathandler_name="combathandler"):
+"""
+ Get or create the combathandler assigned to this combatant.
+
+ """
+ iftarget:
+ # add/check combathandler to the target
+ iftarget.hp_maxisNone:
+ self.msg("You can't attack that!")
+ raiseInterruptCommand()
+
+ EvAdventureCombatTwitchHandler.get_or_create_combathandler(target)
+ returnEvAdventureCombatTwitchHandler.get_or_create_combathandler(self.caller)
+
+
+
+
Line 23: If the current location doesn’t allow combat, all combat commands should exit immediately. To stop the command before it reaches the .func(), we must raise the InterruptCommand().
+
Line 49: It’s convenient to add a helper method for getting the command handler because all our commands will be using it. It in turn calls the class method get_or_create_combathandler we inherit from the parent of EvAdventureCombatTwitchHandler.
# in evadventure/combat_twitch.py
+
+fromevenniaimportdefault_cmds
+fromevennia.utilsimportpad
+
+# ...
+
+classCmdLook(default_cmds.CmdLook,_BaseTwitchCombatCommand):
+ deffunc(self):
+ # get regular look, followed by a combat summary
+ super().func()
+ ifnotself.args:
+ combathandler=self.get_or_create_combathandler()
+ txt=str(combathandler.get_combat_summary(self.caller))
+ maxwidth=max(display_len(line)forlineintxt.strip().split("\n"))
+ self.msg(f"|r{pad(' Combat Status ',width=maxwidth,fillchar='-')}|n\n{txt}")
+
+
+
When in combat we want to be able to do look and get the normal look but with the extra combatsummary at the end (on the form Me(Hurt)vsTroll(Perfect)). So
+
The last line uses Evennia’s utils.pad function to put the text “Combat Status” surrounded by a line on both sides.
+
The result will be the look command output followed directly by
# in evadventure/combat_twitch.py
+
+# ...
+
+classCmdAttack(_BaseTwitchCombatCommand):
+"""
+ Attack a target. Will keep attacking the target until
+ combat ends or another combat action is taken.
+
+ Usage:
+ attack/hit <target>
+
+ """
+
+ key="attack"
+ aliases=["hit"]
+ help_category="combat"
+
+ deffunc(self):
+ target=self.caller.search(self.lhs)
+ ifnottarget:
+ return
+
+ combathandler=self.get_or_create_combathandler(target)
+ combathandler.queue_action(
+ {"key":"attack",
+ "target":target,
+ "dt":3,
+ "repeat":True}
+ )
+ combathandler.msg(f"$You() $conj(attack) $You({target.key})!",self.caller)
+
+
+
The attack command becomes quite simple because we do all the heavy lifting in the combathandler and in the ActionAttack class. Note that we set dt to a fixed 3 here, but in a more complex system one could imagine your skills, weapon and circumstance affecting how long your attack will take.
+
# in evadventure/combat_twitch.py
+
+from.enumsimportABILITY_REVERSE_MAP
+
+# ...
+
+classCmdStunt(_BaseTwitchCombatCommand):
+"""
+ Perform a combat stunt, that boosts an ally against a target, or
+ foils an enemy, giving them disadvantage against an ally.
+
+ Usage:
+ boost [ability] <recipient> <target>
+ foil [ability] <recipient> <target>
+ boost [ability] <target> (same as boost me <target>)
+ foil [ability] <target> (same as foil <target> me)
+
+ Example:
+ boost STR me Goblin
+ boost DEX Goblin
+ foil STR Goblin me
+ foil INT Goblin
+ boost INT Wizard Goblin
+
+ """
+
+ key="stunt"
+ aliases=(
+ "boost",
+ "foil",
+ )
+ help_category="combat"
+
+ defparse(self):
+ args=self.args
+
+ ifnotargsor" "notinargs:
+ self.msg("Usage: <ability> <recipient> <target>")
+ raiseInterruptCommand()
+
+ advantage=self.cmdname!="foil"
+
+ # extract data from the input
+
+ stunt_type,recipient,target=None,None,None
+
+ stunt_type,*args=args.split(None,1)
+ ifstunt_type:
+ stunt_type=stunt_type.strip().lower()
+
+ args=args[0]ifargselse""
+
+ recipient,*args=args.split(None,1)
+ target=args[0]ifargselseNone
+
+ # validate input and try to guess if not given
+
+ # ability is requried
+ ifnotstunt_typeorstunt_typenotinABILITY_REVERSE_MAP:
+ self.msg(
+ f"'{stunt_type}' is not a valid ability. Pick one of"
+ f" {', '.join(ABILITY_REVERSE_MAP.keys())}."
+ )
+ raiseInterruptCommand()
+
+ ifnotrecipient:
+ self.msg("Must give at least a recipient or target.")
+ raiseInterruptCommand()
+
+ ifnottarget:
+ # something like `boost str target`
+ target=recipientifadvantageelse"me"
+ recipient="me"ifadvantageelserecipient
+ westillhaveNone:satthispoint,wecan't continue
+ ifNonein(stunt_type,recipient,target):
+ self.msg("Both ability, recipient and target of stunt must be given.")
+ raiseInterruptCommand()
+
+ # save what we found so it can be accessed from func()
+ self.advantage=advantage
+ self.stunt_type=ABILITY_REVERSE_MAP[stunt_type]
+ self.recipient=recipient.strip()
+ self.target=target.strip()
+
+ deffunc(self):
+ target=self.caller.search(self.target)
+ ifnottarget:
+ return
+ recipient=self.caller.search(self.recipient)
+ ifnotrecipient:
+ return
+
+ combathandler=self.get_or_create_combathandler(target)
+
+ combathandler.queue_action(
+ {
+ "key":"stunt",
+ "recipient":recipient,
+ "target":target,
+ "advantage":self.advantage,
+ "stunt_type":self.stunt_type,
+ "defense_type":self.stunt_type,
+ "dt":3,
+ },
+ )
+ combathandler.msg("$You() prepare a stunt!",self.caller)
+
+
+
+
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.
+
Note the enums.ABILITY_REVERSE_MAP (created in the Utilities lesson) being useful to convert your input of ‘str’ into Ability.STR needed by the action dict.
+
Once we’ve sorted out the string parsing, the func is simple - we find the target and recipient and use them to build the needed action-dict to queue.
# in evadventure/combat_twitch.py
+
+# ...
+
+classCmdUseItem(_BaseTwitchCombatCommand):
+"""
+ Use an item in combat. The item must be in your inventory to use.
+
+ Usage:
+ use <item>
+ use <item> [on] <target>
+
+ Examples:
+ use potion
+ use throwing knife on goblin
+ use bomb goblin
+
+ """
+
+ key="use"
+ help_category="combat"
+
+ defparse(self):
+ super().parse()
+
+ ifnotself.args:
+ self.msg("What do you want to use?")
+ raiseInterruptCommand()
+
+ self.item=self.lhs
+ self.target=self.rhsor"me"
+
+ deffunc(self):
+ item=self.caller.search(
+ self.item,
+ candidates=self.caller.equipment.get_usable_objects_from_backpack()
+ )
+ ifnotitem:
+ self.msg("(You must carry the item to use it.)")
+ return
+ ifself.target:
+ target=self.caller.search(self.target)
+ ifnottarget:
+ return
+
+ combathandler=self.get_or_create_combathandler(self.target)
+ combathandler.queue_action(
+ {"key":"use",
+ "item":item,
+ "target":target,
+ "dt":3}
+ )
+ combathandler.msg(
+ f"$You() prepare to use {item.get_display_name(self.caller)}!",self.caller
+ )
+
+
+
To use an item, we need to make sure we are carrying it. Luckily our work in the Equipment lesson gives us easy methods we can use to search for suitable objects.
# in evadventure/combat_twitch.py
+
+# ...
+
+classCmdWield(_BaseTwitchCombatCommand):
+"""
+ Wield a weapon or spell-rune. You will the wield the item,
+ swapping with any other item(s) you were wielded before.
+
+ Usage:
+ wield <weapon or spell>
+
+ Examples:
+ wield sword
+ wield shield
+ wield fireball
+
+ Note that wielding a shield will not replace the sword in your hand,
+ while wielding a two-handed weapon (or a spell-rune) will take
+ two hands and swap out what you were carrying.
+
+ """
+
+ key="wield"
+ help_category="combat"
+
+ defparse(self):
+ ifnotself.args:
+ self.msg("What do you want to wield?")
+ raiseInterruptCommand()
+ super().parse()
+
+ deffunc(self):
+ item=self.caller.search(
+ self.args,candidates=self.caller.equipment.get_wieldable_objects_from_backpack()
+ )
+ ifnotitem:
+ self.msg("(You must carry the item to wield it.)")
+ return
+ combathandler=self.get_or_create_combathandler()
+ combathandler.queue_action({"key":"wield","item":item,"dt":3})
+ combathandler.msg(f"$You() reach for {item.get_display_name(self.caller)}!",self.caller)
+
+
+
+
The Wield command follows the same pattern as other commands.
To make these commands available to use we must add them to a Command Set.
+
# in evadventure/combat_twitch.py
+
+fromevenniaimportCmdSet
+
+# ...
+
+# after the commands
+
+classTwitchCombatCmdSet(CmdSet):
+"""
+ Add to character, to be able to attack others in a twitch-style way.
+ """
+
+ defat_cmdset_creation(self):
+ self.add(CmdAttack())
+ self.add(CmdHold())
+ self.add(CmdStunt())
+ self.add(CmdUseItem())
+ self.add(CmdWield())
+
+
+classTwitchLookCmdSet(CmdSet):
+"""
+ This will be added/removed dynamically when in combat.
+ """
+
+ defat_cmdset_creation(self):
+ self.add(CmdLook())
+
+
+
+
+
The first cmdset, TwitchCombatCmdSet is intended to be added to the Character. We can do so permanently by adding the cmdset to the default character cmdset (as outlined in the Beginner Command lesson). In the testing section below, we’ll do this in another way.
+
What about that TwitchLookCmdSet? We can’t add it to our character permanently, because we only want this particular version of look to operate while we are in combat.
+
We must make sure to add and clean this up when combat starts and ends.
# in evadventure/combat_twitch.py
+
+# ...
+
+classEvAdventureCombatTwitchHandler(EvAdventureCombatBaseHandler):
+
+ # ...
+
+defat_init(self):
+self.obj.cmdset.add(TwitchLookCmdSet,persistent=False)
+
+ defstop_combat(self):
+self.queue_action({"key":"hold","dt":0})# make sure ticker is killed
+delself.obj.ndb.combathandler
+self.obj.cmdset.remove(TwitchLookCmdSet)
+self.delete()
+
+
+
Now that we have the Look command set, we can finish the Twitch combat handler.
+
+
Line 9: The at_init method is a standard Evennia method available on all typeclassed entities (including Scripts, which is what our combat handler is). Unlike at_object_creation (which only fires once, when the object is first created), at_init will be called every time the object is loaded into memory (normally after you do a server reload). So we add the TwitchLookCmdSet here. We do so non-persistently, since we don’t want to get an ever growing number of cmdsets added every time we reload.
+
Line 13: By queuing a hold action with dt of 0, we make sure to kill the repeat action that is going on. If not, it would still fire later - and find that the combat handler is gone.
+
Line 14: If looking at how we defined the get_or_create_combathandler classmethod (the one we have been using to get/create the combathandler during the combat), you’ll see that it caches the handler as .ndb.combathandler on the object we send to it. So we delete that cached reference here to make sure it’s gone.
+
Line 15: We remove the look-cmdset from ourselves (remember self.obj is you, the combatant that now just finished combat).
Create evadventure/tests/test_combat.py (if you don’t already have it).
+
+
Both the Twitch command handler and commands can and should be unit tested. Testing of commands are made easier by Evennia’s special EvenniaCommandTestMixin class. This makes the .call method available and makes it easy to check if a command returns what you expect.
The EvenniaCommandTestMixin as a few default objects, including self.char1, which we make use of here.
+
The two @patch lines are 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”.
+
We do this patching as an easy way to avoid creating timers in the unit test - these timers would finish after the test finished (which includes deleting its objects) and thus fail.
+
Inside the test, we use the self.call() method to explicitly fire the Command (with no argument) and check that the output is what we expect. Lastly we check that the combathandler is set up correctly, having stored the action-dict on itself.
Showing that the individual pieces of code works (unit testing) is not enough to be sure that your combat system is actually working. We need to test all the pieces together. This is often called functional testing. While functional testing can also be automated, wouldn’t it be fun to be able to actually see our code in action?
+
This is what we need for a minimal test:
+
+
A room with combat enabled.
+
An NPC to attack (it won’t do anything back yet since we haven’t added any AI)
+
A weapon we can wield
+
An item (like a potion) we can use.
+
+
While you can create these manually in-game, it can be convenient to create a batch-command script to set up your testing environment.
+
+
create a new subfolder evadventure/batchscripts/ (if it doesn’t already exist)
+
+
+
create a new file evadventure/combat_demo.ev (note, it’s .ev not .py!)
+
+
A batch-command file is a text file with normal in-game commands, one per line, separated by lines starting with # (these are required between all command lines). Here’s how it looks:
+
# Evadventure combat demo
+
+# start from limbo
+
+tel#2
+
+# turn ourselves into a evadventure-character
+
+typeself=evadventure.characters.EvAdventureCharacter
+
+# assign us the twitch combat cmdset (requires superuser/developer perms)
+
+pyself.cmdset.add("evadventure.combat_twitch.TwitchCombatCmdSet",persistent=True)
+
+# Create a weapon in our inventory (using all defaults)
+
+createsword:evadventure.objects.EvAdventureWeapon
+
+# create a consumable to use
+
+createpotion:evadventure.objects.EvAdventureConsumable
+
+# dig a combat arena
+
+digarena:evadventure.rooms.EvAdventureRoom=arena,back
+
+# go to arena
+
+arena
+
+# allow combat in this room
+
+sethere/allow_combat=True
+
+# create a dummy enemy to hit on
+
+create/dropdummypuppet;dummy:evadventure.npcs.EvAdventureNPC
+
+# describe the dummy
+
+descdummy=Thisisisanuglytrainingdummymadeoutofhayandwood.
+
+# make the dummy crazy tough
+
+setdummy/hp_max=1000
+
+#
+
+setdummy/hp=1000
+
+
+
Log into the game with a developer/superuser account and run
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. )
+
You can now try attackdummy and should be able to pound away at the dummy (lower its health to test destroying it). Use back to ‘flee’ the combat.
This was a big lesson! Even though our combat system is not very complex, there are still many moving parts to keep in mind.
+
Also, while pretty simple, there is also a lot of growth possible with this system. You could easily expand from this or use it as inspiration for your own game.
+
Next we’ll try to achieve the same thing within a turn-based framework!