Updated HTML docs

This commit is contained in:
Evennia docbuilder action 2022-09-17 23:44:19 +00:00
parent 937794ad0b
commit dcc4cbe66f
316 changed files with 34330 additions and 3279 deletions

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
Now that we have learned a little about how to find things in the Evennia library, let's use it.
In the [Python classes and objects](./Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
In the [Python classes and objects](./Beginner-Tutorial-Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
the server or `quit()` out of python mode they are gone.
@ -251,7 +251,7 @@ You are specifying exactly which typeclass you want to use to build the Giantess
desc = You see nothing special.
-------------------------------------------------------------------------------
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
We used the `examine` command briefly in the [lesson about building in-game](./Beginner-Tutorial-Building-Quickstart.md). Now these lines
may be more useful to us:
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
unique 'primary key' or _dbref_ for this entity in the database.
@ -357,7 +357,7 @@ You got a lot longer output this time. You have a lot more going on than a simpl
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
- **Account** shows, well the `Account` object associated with this Character and Session.
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
get to them in the [next lesson](./Beginner-Tutorial-Adding-Commands.md). For now it's enough to know these consitute all the
commands available to you at a given moment.
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,462 @@
# Planning our tutorial game
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial. We'll call it ... _EvAdventure_.
Remembering that we need to keep the scope down, let's establish some parameters.
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded to something more later.
- We want to have a clear game-loop, with clear goals.
- Let's go with a fantasy theme, it's well understood.
- We will use a small, existing tabletop RPG rule set ([Knave](https://www.drivethrurpg.com/product/250888/Knave), more info later)
- We want to be able to create and customize a character of our own.
- While not roleplay-focused, it should still be possible to socialize and to collaborate.
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution and combat.
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
- We want some sort of quest system and merchants to buy stuff from.
## Game concept
With these points in mind, here's a quick blurb for our game:
_Recently, the nearby village discovered that the old abandoned well contained a dark secret. The bottom of the well led to a previously undiscovered dungeon of ever shifting passages. No one knew why it was there or what its purpose was, but local rumors abound. The first adventurer that went down didn't come back. The second ... brought back a handful of glittering riches._
_Now the rush is on - there's a dungeon to explore and coin to earn. Knaves, cutthroats, adventurers and maybe even a hero or two are coming from all over the realm to challenge whatever lurks at the bottom of that well._
_Local merchants and opportunists have seen a chance for profit. A camp of tents has sprung up around the old well, providing food and drink, equipment, entertainment and rumors for a price. It's a festival to enjoy before paying the entrance fee for dropping down the well to find your fate among the shadows below ..._
Our game will consist of two main game modes - above ground and below. The player starts above ground and is expected to do 'expeditions' into the dark. The design goal is for them to be forced back up again when their health, equipment and luck is about to run out.
- Above, in the "dungeon festival", the player can restock and heal up, buy things and do a small set of quests. It's the only place where the characters can sleep and fully heal. They also need to spend coin here to gain XP and levels. This is a place for players to socialize and RP. There is no combat above ground except for an optional spot for non-lethal PvP.
- Below is the mysterious dungeon. This is a procedurally generated set of rooms. Players can collaborate if they go down the well together, they will not be able to run into each other otherwise (so this works as an instance). Each room generally presents some challenge (normally a battle). Pushing deeper is more dangerous but can grant greater rewards. While the rooms could in theory go on forever, there should be a boss encounter once a player reaches deep enough.
Here's an overview of the topside camp for inspiration (quickly thrown together in the free version of [Inkarnate](https://inkarnate.com/)). We'll explore how to break this up into "rooms" (locations) when we get to creating the game world later.
![Last Step Camp](../../../_static/images/starting_tutorial/Dungeon_Merchant_Camp.jpg)
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Beginner-Tutorial-Game-Planning.md) lesson.
## Administration
### Should your game rules be enforced by coded systems by human game masters?
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering you want them to do. You may need to expand communication channels so you can easily talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
as the GM sitting with the rule books and using a dice-roller for visibility.
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours of the day to serve an international player base. The computer never needs sleep, so having the ability for players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer or by the interactions between players. Such games still need an active staff, but nowhere as much active involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
players is low.
**EvAdventure Answer:**
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing stopping a GM from stepping in and run an adventure for some players should they want to.
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
The default hierarchy is
- `Player` - regular players
- `Player Helper` - can create/edit help entries
- `Builder` - can use build commands
- `Admin` - can kick and ban accounts
- `Developer` - full access, usually also trusted with server access
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
goes outside the regular hierarchy and should usually only.
**EvAdventure Answer**
We are okay with keeping the default permission structure for our game.
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
other. By default, the `public` channel is created for general discourse.
Channels are logged to a file and when you are coming back to the game you can view the history of a channel in case you missed something.
> public Hello world!
[Public] MyName: Hello world!
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels would have an in-game meaning:
- Members of a guild could be linked telepathically.
- Survivors of the apocalypse can communicate over walkie-talkies.
- Radio stations you can tune into or have to discover.
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel, the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board system.
**EvAdventure Answer**
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any bulletin boards; instead the merchant NPCs will act as quest givers.
## Building
### How will the world be built?
There are two main ways to handle this:
- Traditionally, from in-game with build-commands: This means builders creating content in their game client. This has the advantage of not requiring Python skills nor server access. This can often be a quite intuitive way to build since you are sort-of walking around in your creation as you build it. However, the developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to create the content you want for your game.
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients. The drawback is that for their changes to go live they either need server access or they need to send their batchcode to the game administrator so they can apply the changes. Or use version control.
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the builder to have server access or to use version control to share their work with the rest of the development team.
**EvAdventure Answer**
For EvAdventure, we will build the above-ground part of the game world using batch-scripts. The world below-ground we will build procedurally, using raw code.
### Can only privileged Builders create things or should regular players also have limited build-capability?
In some game styles, players have the ability to create objects and even script them. While giving regular users the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is not something Evennia supports natively.
Regular, untrusted users should never be allowed to execute raw Python
code (such as what you can do with the `py` command). You can
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting, it's suggested that this is accomplished by adding more powerful build-commands for them to use.
**EvAdventure Answer**
For our tutorial-game, we will only allow privileged builders and admins to modify the world.
## Systems
### Do you base your game off an existing RPG system or make up your own?
There is a plethora of options out there, and what you choose depends on the game you want. It can be tempting to grab a short free-form ruleset, but remember that the computer does not have any intuitiion or common sense to interpret the rules like a human GM could. Conversely, if you pick a very 'crunchy' game system, with detailed simulation of the real world, remember that you'll need to actually _code_ all those exceptions and tables yourself.
For speediest development, what you want is a game with a _consolidated_ resolution mechanic - one you can code once and then use in a lot of situations. But you still want enough rules to help telling the computer how various situations should be resolved (combat is the most common system that needs such structure).
**EvAdventure Answer**
For this tutorial, we will make use of [Knave](https://www.drivethrurpg.com/product/250888/Knave), a very light [OSR](https://en.wikipedia.org/wiki/Old_School_Renaissance) ruleset by Ben Milton. It's only a few pages long but highly compatible with old-school D&D games. It's consolidates all rules around a few opposed d20 rolls and includes clear rules for combat, inventory, equipment and so on. Since _Knave_ is a tabletop RPG, we will have to do some minor changes here and there to fit it to the computer medium.
_Knave_ is available under a Creative Commons Attributions 4.0 License, meaning it can be used for derivative work (even commercially). The above link allows you to purchase the PDF and supporting the author. Alternatively you can find unofficial fan releases of the rules [on this page](https://dungeonsandpossums.com/2020/04/some-great-knave-rpg-resources/).
### What are the game mechanics? How do you decide if an action succeeds or fails?
This follows from the RPG system decided upon in the previous question.
**EvAdventure Answer**
_Knave_ gives every character a set of six traditional stats: Strength, Intelligence, Dexterity, Constitution, Intelligence, Wisdom and Charisma. Each has a value from +1 to +10. To find its "Defense" value, you add 10.
You have Strength +1. Your Strength-Defense is 10 + 1 = 11
To make a check, say an arm-wrestling challenge you roll a twenty-sided die (d20) and add your stat. You have to roll higher than the opponents defense for that stat.
I have Strength +1, my opponent has a Strength of +2. To beat them in arm wrestling I must roll d20 + 1 and hope to get higher than 12, which is their Strength defense (10 + 2).
If you attack someone you do the same, except you roll against their `Armor` defense. If you rolled higher, you roll for how much damage you do (depends on your weapon).
You can have _advantage_ or _disadvantage_ on a roll. This means rolling 2d20 and picking highest or lowest value.
In Knave, combat is turn-based. In our implementation we'll also play turn-based, but we'll resolve everything _simultaneously_. This changes _Knave_'s feel quite a bit, but is a case where the computer can do things not practical to do when playing around a table.
There are also a few tables we'll need to implement. For example, if you lose all health, there's a one-in-six chance you'll die outright. We'll keep this perma-death aspect, but make it very easy to start a new character and jump back in.
> In this tutorial we will not add opportunities to make use of all of the character stats, making some, like strength, intelligence and dexterity more useful than others. In a full game, one would want to expand so a user can utilize all of their character's strengths.
### Does the flow of time matter in your game - does night and day change? What about seasons?
Most commonly, game-time runs faster than real-world time. There are
a few advantages with this:
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for something, like NPC shops opening.
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene over dinner, the game world reports that two days have passed. Having a slower game time than real-time is a less common, but possible solution for such games.
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this is that people will join your game from all around the world, and they will often only be able to play at particular times of their day. With a game-time drifting relative real-time, everyone will eventually be able to experience both day and night in the game.
**EvAdventure Answer**
The passage of time will have no impact on our particular game example, so we'll go with Evennia's default, which is that the game-time runs two times faster than real time.
### Do you want changing, global weather or should weather just be set manually in roleplay?
A weather system is a good example of a game-global system that affects a subset of game entities (outdoor rooms).
**EvAdventure Answer**
We'll not change the weather, but will add some random messages to echo through
the game world at random intervals just to show the principle.
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
This is a big question and depends on how deep and interconnected the virtual transactions are that are happening in the game. Shop prices could rice and drop due to supply and demand, supply chains could involve crafting and production. One also could consider adding money sinks and manipulate the in-game market to combat inflation.
The [Barter](../../../Contribs/Contrib-Barter.md) contrib provides a full interface for trading with another player in a safe way.
**EvAdventure Answer**
We will not deal with any of this complexity. We will allow for players to buy from npc sellers and players will be able to trade using the normal `give` command.
### Do you have concepts like reputation and influence?
These are useful things for a more social-interaction heavy game.
**EvAdventure Answer**
We will not include them for this tutorial. Adding the Barter contrib is simple though.
### Will your characters be known by their name or only by their physical appearance?
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex emoting and posing.
Implementing such a system is not trivial, but the [RPsystem](../../../Contribs/Contrib-RPSystem.md) Evennia contrib offers a ready system with everything needed for free emoting, recognizing people by their appearance and more.
**EvAdventure Answer**
We will not use any special RP systems for this tutorial. Adding the RPSystem contrib is a good extra expansion though!
## Rooms
### Is a simple room description enough or should the description be able to change?
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to include that.
There is an [Extended Room](../../../Contribs/Contrib-Extended-Room.md) contrib that adds a Room type that is aware of the time-of-day as well as seasonal variations.
**EvAdventure Answer**
We will stick to a normal room in this tutorial and let the world be in a perpetual daylight. Making Rooms into ExtendedRooms is not hard though.
### Should the room have different statuses?
One could picture weather making outdoor rooms wet, cold or burnt. In rain, bow strings could get wet and fireballs fizz out. In a hot room, characters could require drinking more water, or even take damage if not finding shelter.
**EvAdventure Answer**
For the above-ground we need to be able to disable combat all rooms except for the PvP location. We also need to consider how to auto-generate the rooms under ground. So we probably will need some statuses to control that.
Since each room under ground should present some sort of challenge, we may need a few different room types different from the above-ground Rooms.
### Can objects be hidden in the room? Can a person hide in the room?
This ties into if you have hide/stealth mechanics. Maybe you could evesdrop or attack out of hiding.
**EvAdventure Answer**
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
## Objects
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
This also depends on the type of game. In a pure freeform RPG, most objects may be 'imaginary' and just appearing in fiction. If the game is more coded, you want objects with properties that the computer can measure, track and calculate. In many roleplaying-heavy games, you find a mixture of the two, with players imagining items for roleplaying scenes, but only using 'real' objects to resolve conflicts.
**EvAdventure Answer**
We will want objects with properties, like weapons and potions and such. Monsters should drop loot even though our list of objects will not be huge in this example game.
### Is each coin a separate object or do you just store a bank account value?
The advantage of having multiple items is that it can be more immersive. The drawback is that it's also very fiddly to deal with individual coins, especially if you have to deal with different currencies.
**EvAdventure Answer**
_Knave_ uses the "copper" as the base coin and so will we. Knave considers the weight of coin and one inventory "slot" can hold 100 coins. So we'll implement a "coin item" to represent many coins.
### Do multiple similar objects form stack and how are those stacks handled in that case?
If you drop two identical apples on the ground, Evennia will default to show this in the room as "two apples", but this is just a visual effect - there are still two apple-objects in the room. One could picture instead merging the two into a single object "X nr of apples" when you drop the apples.
**EvAdventure Answer**
We will keep Evennia's default.
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important for players to pick only the equipment they need. Carrying limits can easily come across as annoying to players though, so one needs to be careful with it.
**EvAdventure Answer**
_Knave_ limits your inventory to `Constitution + 10` "slots", where most items take up one slot and some large things, like armor, uses two. Small items (like rings) can fit 2-10 per slot and you can fit 100 coins in a slot. This is an important game mechanic to limit players from hoarding. Especially since you need coin to level up.
### Can objects be broken? Can they be repaired?
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not too common, then it becomes annoying) and repairing things gives work for crafting players.
**EvAdventure Answer**
In _Knave_, items will break if you make a critical failure on using them (rolls a native 1 on d20). This means they lose a level of `quality` and once at 0, it's unusable. We will not allow players to repair, but we could allow merchants to repair items for a fee.
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
simplification, but with Python classes and inheritance, it's not actually more work to just let all items in game work as a weapon in a pinch.
**EvAdventure Answer**
Since _Knave_ deals with weapon lists and positions where items can be wielded, we will have a separate "Weapon" class for everything you can use for fighting. So, you won't be able to fight with a chair (unless we make it a weapon-inherited chair).
### Will characters be able to craft new objects?
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check to combine base ingredients from a fixed recipe in order to create a new item. The classic example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
A full-fledged crafting system could require multiple levels of crafting, including having to mine for ore or cut down trees for wood.
Evennia's [Crafting](../../../Contribs/Contrib-Crafting.md) contrib adds a full crafting system to any game. It's based on [Tags](../../../Components/Tags.md), meaning that pretty much any object can be made usable for crafting, even used in an unexpected way.
**EvAdventure Answer**
In our case we will not add any crafting in order to limit the scope of our game. Maybe NPCs will be able to repair items - for a cost?
### Should mobs/NPCs have some sort of AI?
As a rule, you should not hope to fool anyone into thinking your AI is actually intelligent. The best you will be able to do is to give interesting results and unless you have a side-gig as an AI researcher, users will likely not notice any practical difference between a simple state-machine and you spending a lot of time learning
how to train a neural net.
**EvAdventure Answer**
For this tutorial, we will show how to add a simple state-machine AI for monsters. NPCs will only be shop-keepers and quest-gives so they won't need any real AI to speak of.
### Are NPCs and mobs different entities? How do they differ?
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally different these days it's often easier to just make NPCs and mobs essentially the same thing.
**EvAdventure Answer**
In EvAdventure, Monsters and NPCs do very different things, so they will be different classes, sharing some code where possible.
### _Should there be NPCs giving quests? If so, how do you track Quest status?
Quests are a staple of many classic RPGs.
**EvAdventure Answer**
We will design a simple quest system with some simple conditions for success, like carrying the right item or items back to the quest giver.
## Characters
### Can players have more than one Character active at a time or are they allowed to multi-play?
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts` and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE` setting, which has a value from `0` (default) to `3`.
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
account from another client you'll be disconnected from the first. When creating a new account, a Character
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
which had no separation between Account and Character.
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
multiple clients and see the same output in all of them.
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
open, but only one client per Character.
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
can control each Character from multiple clients, seeing the same output from each Character.
**EvAdventure Answer**
Due to the nature of _Knave_, characters are squishy and probably short-lived. So it makes little sense to keep a stable of them. We'll use use mode 0 or 1.
### How does the character-generation work?
There are a few common ways to do character generation:
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify your character. When you are done you move to the next room. Only use this if you have another reason for using a room, like having a training dummy to test skills on, for example.
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
using custom commands they will likely never use again after this.
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented with a sequential menu.
**EvAdventure Answer**
Knave randomizes almost aspects of the Character generation. We'll use a menu to let the player add their name and sex as well as do the minor re-assignment of stats allowed by the rules.
### How do you implement different "classes" or "races"?
The way classes and races work in most RPGs is that they act as static 'templates' that inform which bonuses and special abilities you have. Much of this only comes into play during character generation or when leveling up.
Often all we need to store on the Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just be looked up when we need it.
**EvAdventure Answer**
There are no races and no classes in _Knave_. Every character is a human.
### If a Character can hide in a room, what skill will decide if they are detected?
Hiding means a few things.
- The Character should not appear in the room's description / character list
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
or `look <name>` if the named character is in hiding.
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
find the person (probably based on skill checks).
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
**EvAdventure Answer**
We will not be including a hide-mechanic in EvAdventure.
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
ways to implement this:
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's easy to tell when you should gain it.
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
just be lurking and not be actually playing the game at any given time.
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
how often you post, how long your emotes are etc.
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
- XP from fails - you only gain XP when failing rolls.
- XP from other players - other players can award you XP for good RP.
**EvAdventure Answer**
We will use an alternative rule in _Knave_, where Characters gain XP by spending coins they carry back from their adventures. The above-ground merchants will allow you to spend your coins and exchange them for XP 1:1. Each level costs 1000 coins. Every level you have `1d8 * new level` (minimum what you had before + 1) HP, and can raise 3 different ability scores by 1 (max +10). There are no skills in _Knave_, but the principle of increasing them would be the same.
### May player-characters attack each other (PvP)?
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They will not be as accepting if they perceive another player as being overpowered. PvP means that you
have to be very careful to balance the game - all characters does not have to be exactly equal but they should all be viable to play a fun game with.
PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in a political game or gaining market share when selling their crafted merchandise.
**EvAdventure Answer**
We will allow PvP only in one place - a special Dueling location where players can play-fight each other for training and prestige, but not actually get killed. Otherwise no PvP will be allowed. Note that without a full Barter system in place (just regular `give`, it makes it theoretically easier for players to scam one another.
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
This is another big decision that strongly affects the mood and style of your game.
Perma-death means that once your character dies, it's gone and you have to make a new one.
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
your triumph is an actual feat.
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
up for newcomers.
- It lowers inflation, since the hoarded resources of a dead character can be removed.
- It gives capital punishment genuine discouraging power.
- It's realistic.
Perma-death comes with some severe disadvantages however.
- Many players say they like the _idea_ of permadeath except when it could happen to them.
- Some players refuse to take any risks if death is permanent.
- It may make players even more reluctant to play conflict-driving 'bad guys'.
- Balancing PvP becomes very hard. Fairness and avoiding exploits becomes critical when the outcome
is permanent.
For these reasons, it's very common to do hybrid systems. Some tried variations:
- NPCs cannot kill you, only other players can.
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely hurt/incapacitated.
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
you die permanently.
- Death just means harsh penalties, not actual death.
- When you die you can fight your way back to life from some sort of afterlife.
- You'll only die permanently if you as a player explicitly allows it.
**EvAdventure Answer**
In _Knave_, when you hit 0 HP, you roll on a death table, with a 1/8 chance of immediate death (otherwise you lose
points in a random stat). We will offer an "Insurance" that allows you to resurrect if you carry enough coin on you when
you die. If not, you are perma-dead and have to create a new character (which is easy and quick since it's mostly
randomized).
## Conclusions
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are many, many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
playable game!
In the last of these planning lessons we'll sketch out how these ideas will map to Evennia.

View file

@ -1,244 +0,0 @@
# Planning the use of some useful contribs
Evennia is deliberately bare-bones out of the box. The idea is that you should be as unrestricted as possible
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
prescribe any major game systems on you.
That said, Evennia _does_ offer some more game-opinionated _optional_ stuff. These are referred to as _Contribs_
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
from to use, tweak or take inspiration from when you make your game.
The [Contrib overview](../../../Contribs/Contribs-Overview.md) page gives the full list of the current roster of contributions. On
this page we will review a few contribs we will make use of for our game. We will do the actual installation
of them when we start coding in the next part of this tutorial series. While we will introduce them here, you
are wise to read their doc-strings yourself for the details.
This is the things we know we need:
- A barter system
- Character generation
- Some concept of wearing armor
- The ability to roll dice
- Rooms with awareness of day, night and season
- Roleplaying with short-descs, poses and emotes
- Quests
- Combat (with players and against monsters)
## Barter contrib
[source](../../../api/evennia.contrib.game_systems.barter.md)
Reviewing this contrib suggests that it allows for safe trading between two parties. The basic principle
is that the parties puts up the stuff they want to sell and the system will guarantee that these systems are
exactly what is being offered. Both sides can modify their offers (bartering) until both mark themselves happy
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
works just fine for money too - just put coin objects on one side of the transaction.
Sue > trade Tom: Hi, I have a necklace to sell; wanna trade for a healing potion?
Tom > trade Sue: Hm, I could use a necklace ...
<both accepted trade. Start trade>
Sue > offer necklace: This necklace is really worth it.
Tom > evaluate necklace:
<Tom sees necklace stats>
Tom > offer ration: I don't have a healing potion, but I'll trade you an iron ration!
Sue > Hey, this is a nice necklace, I need more than a ration for it...
Tom > offer ration, 10gold: Ok, a ration and 10 gold as well.
Sue > accept: Ok, that sounds fair!
Tom > accept: Good! Nice doing business with you.
<goods change hands automatically. Trade ends>
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
barter system guarantees trading safety if you don't trust your counterpart to try to give you the wrong thing or
to run away with your money.
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
things than boring gold coin.
## Clothing contrib
[source](../../../api/evennia.contrib.game_systems.clothing.md)
This contrib provides a full system primarily aimed at wearing clothes, but it could also work for armor. You wear
an object in a particular location and this will then be reflected in your character's description. You can
also add roleplaying flavor:
> wear helmet slightly askew on her head
look self
Username is wearing a helmet slightly askew on her head.
By default there are no 'body locations' in this contrib, we will need to expand on it a little to make it useful
for things like armor. It's a good contrib to build from though, so that's what we'll do.
## Dice contrib
[source](../../../api/evennia.contrib.rpg.dice.md)
The dice contrib presents a general dice roller to use in game.
> roll 2d6
Roll(s): 2 and 5. Total result is 7.
> roll 1d100 + 2
Roll(s): 43. Total result is 47
> roll 1d20 > 12
Roll(s): 7. Total result is 7. This is a failure (by 5)
> roll/hidden 1d20 > 12
Roll(s): 18. Total result is 17. This is a success (by 6). (not echoed)
The contrib also has a python function for producing these results in-code. However, while
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
module.
So while this contrib is fun to have around for GMs or for players who want to get a random result
or play a game, we will not need it for the core of our game.
## Extended room contrib
[source](../../../api/evennia.contrib.grid.extended_room.md)
This is a custom Room typeclass that changes its description based on time of day and season.
For example, at night, in wintertime you could show the room as being dark and frost-covered while in daylight
at summer it could describe a flowering meadow. The description can also contain special markers, so
`<morning> ... </morning>` would include text only visible at morning.
The extended room also supports _details_, which are things to "look at" in the room without there having
to be a separate database object created for it. For example, a player in a church may do `look window` and
get a description of the windows without there needing to be an actual `window` object in the room.
Adding all those extra descriptions can be a lot of work, so they are optional; if not given the room works
like a normal room.
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
game, why not!
## RP-System contrib
[source](../../../api/evennia.contrib.rpg.rpsystem.md)
This contrib adds a full roleplaying subsystem to your game. It gives every character a "short-description"
(sdesc) that is what people will see when first meeting them. Let's say Tom has an sdesc "A tall man" and
Sue has the sdesc "A muscular, blonde woman"
Tom > look
Tom: <room desc> ... You see: A muscular, blonde woman
Tom > emote /me smiles to /muscular.
Tom: Tom smiles to A muscular, blonde woman.
Sue: A tall man smiles to Sue.
Tom > emote Leaning forward, /me says, "Well hello, what's yer name?"
Tom: Leaning forward, Tom says, "Well hello..."
Sue: Leaning forward, A tall man says, "Well hello, what's yer name?"
Sue > emote /me grins. "I'm Angelica", she says.
Sue: Sue grins. "I'm Angelica", she says.
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
Tom > recog muscular Angelica
Tom > emote /me nods to /angelica: "I have a message for you ..."
Tom: Tom nods to Angelica: "I have a message for you ..."
Sue: A tall man nods to Sue: "I have a message for you ..."
Above, Sue introduces herself as "Angelica" and Tom uses this info to `recoc` her as "Angelica" hereafter. He
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
The RPSystem module also includes options for `poses`, which help to establish your position in the room
when others look at you.
Tom > pose stands by the bar, looking bored.
Sue > look
Sue: <room desc> ... A tall man stands by the bar, looking bored.
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
like `a person with a mask`.
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
[rplanguage](../../../api/evennia.contrib.rpg.rpsystem.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
the language spoken. In order to restrict the scope we will not include languages for the tutorial game.
## Talking NPC contrib
[source](../../../api/evennia.contrib.tutorials.talking_npc.md)
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
good as inspiration for how we'll do quest-givers later.
## Traits contrib
[source](../../../api/evennia.contrib.rpg.traits.md)
An issue with dealing with roleplaying attributes like strength, dexterity, or skills like hunting, sword etc
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
Your swordmanship may be worse because you are encumbered. And when you drink your health potion you must make
sure that those +20 health does not bring your health higher than its maximum. All this adds complexity.
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
installed, the traits are accessed on a new handler `.traits`, for example
> py self.traits.hp.value
100
> py self.traits.hp -= 20 # getting hurt
> py self.traits.hp.value
80
> py self.traits.hp.reset() # drink a potion
> py self.traits.hp.value
100
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
automatically. They can also be added together in various mathematical operations.
The contrib introduces three main Trait-classes
- _Static_ traits for single values like str, dex, things that at most gets a modifier.
- _Counters_ is a value that never moves outside a given range, even with modifiers. For example a skill
that can at most get a maximum amount of buff. Counters can also easily be _timed_ so that they decrease
or increase with a certain rate per second. This could be good for a time-limited curse for example.
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
things like health, stamina and the like. Gauges can also change with a rate, which works well for the
effects of slow poisons and healing both.
```
> py self.traits.hp.value
100
> py self.traits.hp.rate = -1 # poisoned!
> py self.traits.hp.ratetarget = 50 # stop at 50 hp
# Wait 30s
> py self.traits.hp.value
70
# Wait another 30s
> py self.traits.hp.value
50 # stopped at 50
> py self.traits.hp.rate = 0 # no more poison
> py self.traits.hp.rate = 5 # healing magic!
# wait 5s
> pyself.traits.hp.value
75
```
Traits will be very practical to use for our character sheets.
## Turnbattle contrib
[source](../../../api/evennia.contrib.game_systems.turnbattle.md)
This contrib consists of several implementations of a turn-based combat system, divivided into complexity:
- basic - initiative and turn order, attacks against defense values, damage.
- equip - considers weapons and armor, wielding and weapon accuracy.
- items - adds usable items with conditions and status effects
- magic - adds spellcasting system using MP.
- range - adds abstract positioning and 1D movement to differentiate between melee and ranged attacks.
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
a complete system. It's also not built with _Traits_ in mind, so we will need to adjust it for that.
## Conclusions
With some contribs selected, we have pieces to build from and don't have to write everything from scratch.
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
to our needs.
We will now move into actually starting to implement our tutorial game
in the next part of this tutorial series. When doing this for yourself, remember to refer
back to your planning and adjust it as you learn what works and what does not.

View file

@ -1,425 +0,0 @@
# Planning our tutorial game
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial.
Remembering that we need to keep the scope down, let's establish some parameters.
Note that for your own
game you don't _need_ to agree/adopt any of these. Many game-types need more or much less than this.
But this makes for good, instructive examples.
- To have something to refer to rather than just saying "our tutorial game" over and over, we'll
name it ... _EvAdventure_.
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded
to something more later.
- Let's go with a fantasy theme, it's well understood.
- We'll use some existing, simple RPG system.
- We want to be able to create and customize a character of our own.
- We want the tools to roleplay with other players.
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution
and combat.
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
- We want to be able to buy and sell stuff, both with NPCs and other players.
- We want some sort of crafting system.
- We want some sort of quest system.
Let's answer the questions from the previous lesson and discuss some of the possibilities.
## Administration
### Should your game rules be enforced by coded systems by human game masters?
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To
support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering
you want them to do. You may need to expand communication channels so you can easily
talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
as the GM sitting with the rule books and using a dice-roller for visibility.
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours
of the day to serve an international player base. The computer never needs sleep, so having the ability for
players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer
or by the interactions between players. Such games still need an active staff, but nowhere as much active
involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
players is low.
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing
stopping a GM from stepping in and run an adventure for some players should they want to.
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
The default hierarchy is
- `Player` - regular players
- `Player Helper` - can create/edit help entries
- `Builder` - can use build commands
- `Admin` - can kick and ban accounts
- `Developer` - full access, usually also trusted with server access
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
goes outside the regular hierarchy and should usually only.
We are okay with keeping this structure for our game.
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
other. By default, the `public` channel is created for general discourse.
Channels are logged to a file and when you are coming back to the game you can view the history of a channel
in case you missed something.
> public Hello world!
[Public] MyName: Hello world!
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels
would have an in-game meaning:
- Members of a guild could be linked telepathically.
- Survivors of the apocalypse can communicate over walkie-talkies.
- Radio stations you can tune into or have to discover.
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel,
the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board
system.
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any
bulletin boards.
## Building
### How will the world be built?
There are two main ways to handle this:
- Traditionally, from in-game with build-commands: This means builders creating content in their game
client. This has the advantage of not requiring Python skills nor server access. This can often be a quite
intuitive way to build since you are sort-of walking around in your creation as you build it. However, the
developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to
create the content you want for your game.
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them
in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients.
The drawback is that for their changes to go live they either need server access or they need to send their
batchcode to the game administrator so they can apply the changes. Or use version control.
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the
builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the
builder to have server access or to use version control to share their work with the rest of the development team.
In this tutorial, we will show examples of all these ways, but since we don't have a team of builders we'll
build the brunt of things using Evennia's Batchcode system.
### Can only privileged Builders create things or should regular players also have limited build-capability?
In some game styles, players have the ability to create objects and even script them. While giving regular users
the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is
not something Evennia supports natively. Regular, untrusted users should never be allowed to execute raw Python
code (such as what you can do with the `py` command). You can
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting,
it's suggested that this is accomplished by adding more powerful build-commands for them to use.
For our tutorial-game, we will only allow privileged builders to modify the world. The exception is crafting,
which we will limit to repairing broken items by combining them with other repair-related items.
## Systems
### Do you base your game off an existing RPG system or make up your own?
We will make use of [Open Adventure](http://www.geekguild.com/openadventure/), a simple 'old school' RPG-system
that is available for free under the Creative Commons license. We'll only use a subset of the rules from
the blue "basic" book. For the sake of keeping down the length of this tutorial we will limit what features
we will include:
- Only two 'archetypes' (classes) - Arcanist (wizard) and Warrior, these are examples of two different play
styles.
- Two races only (dwarves and elves), to show off how to implement races and race bonuses.
- No extra features of the races/archetypes such as foci and special feats. While these are good for fleshing
out a character, these will work the same as other bonuses and are thus not that instructive.
- We will add only a small number of items/weapons from the Open Adventure rulebook to show how it's done.
### What are the game mechanics? How do you decide if an action succeeds or fails?
Open Adventure's conflict resolution is based on adding a trait (such as Strength) with a random number in
order to beat a target. We will emulate this in code.
Having a "skill" means getting a bonus to that roll for a more narrow action.
Since the computer will need to know exactly what those skills are, we will add them more explicitly than
in the rules, but we will only add the minimum to show off the functionality we need.
### Does the flow of time matter in your game - does night and day change? What about seasons?
Most commonly, game-time runs faster than real-world time. There are
a few advantages with this:
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for
something, like NPC shops opening.
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene
over dinner, the game world reports that two days have passed. Having a slower game time than real-time is
a less common, but possible solution for such games.
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this
is that people will join your game from all around the world, and they will often only be able to play at
particular times of their day. With a game-time drifting relative real-time, everyone will eventually be
able to experience both day and night in the game.
For this tutorial-game we will go with Evennia's default, which is that the game-time runs two times faster
than real time.
### Do you want changing, global weather or should weather just be set manually in roleplay?
A weather system is a good example of a game-global system that affects a subset of game entities
(outdoor rooms). We will not be doing any advanced weather simulation, but we'll show how to do
random weather changes happening across the game world.
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
We will allow for money and barter/trade between NPCs/Players and Player/Player, but will not care about
inflation. A real economic simulation could do things like modify shop prices based on supply and demand.
We will not go down that rabbit hole.
### Do you have concepts like reputation and influence?
These are useful things for a more social-interaction heavy game. We will not include them for this
tutorial however.
### Will your characters be known by their name or only by their physical appearance?
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you
introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex
emoting and posing.
Adding such a system from scratch is complex and way beyond the scope of this tutorial. However,
there is an existing Evennia contrib that adds all of this functionality and more, so we will
include that and explain briefly how it works.
## Rooms
### Is a simple room description enough or should the description be able to change?
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks
very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to
include that.
### Should the room have different statuses?
We will have different weather in outdoor rooms, but this will not have any gameplay effect - bow strings
will not get wet and fireballs will not fizzle if it rains.
### Can objects be hidden in the room? Can a person hide in the room?
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
## Objects
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
Since we are not going for a pure freeform RPG here, we will want objects with properties, like weapons
and potions and such. Monsters should drop loot even though our list of objects will not be huge.
### Is each coin a separate object or do you just store a bank account value?
Since we will use bartering, placing coin objects on one side of the barter makes for a simple way to
handle payments. So we will use coins as-objects.
### Do multiple similar objects form stacks and how are those stacks handled in that case?
Since we'll use coins, it's practical to have them and other items stack together. While Evennia does not
do this natively, we will make use of a contrib for this.
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important
for players to pick only the equipment they need. Carrying limits can easily come across as
annoying to players though, so one needs to be careful with it.
Open Adventure rules include weight limits, so we will include them.
### Can objects be broken? Can they be repaired?
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not
too common, then it becomes annoying) and repairing things gives work for crafting players.
We wanted a crafting system, so this is what we will limit it to - repairing items using some sort
of raw materials.
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
simplification, but with Python classes and inheritance, it's not actually more work to just
let all items in game work as a weapon in a pinch.
So for our game we will let a character use any item they want as a weapon. The difference will
be that non-weapon items will do less damage and also break and become unusable much quicker.
### Will characters be able to craft new objects?
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check
to combine base ingredients from a fixed recipe in order to create a new item. The classic
example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
A full-fledged crafting system could require multiple levels of crafting, including having to mine
for ore or cut down trees for wood.
In our case we will limit our crafting to repairing broken items. To show how it's done, we will require
extra items (a recipe) in order to facilitate the repairs.
### Should mobs/NPCs have some sort of AI?
A rule of adding Artificial Intelligence is that with today's technology you should not hope to fool
anyone with it anytime soon. Unless you have a side-gig as an AI researcher, users will likely
not notice any practical difference between a simple state-machine and you spending a lot of time learning
how to train a neural net.
For this tutorial, we will show how to add a simple state-machine for monsters. NPCs will only be
shop-keepers and quest-gives so they won't need any real AI to speak of.
### Are NPCs and mobs different entities? How do they differ?
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could
also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally
different these days it's often easier to just make NPCs and mobs essentially the same thing.
In EvAdventure, both Monsters and NPCs will be the same type of thing; A monster could give you a quest
and an NPC might fight you as a mob as well as trade with you.
### _Should there be NPCs giving quests? If so, how do you track Quest status?
We will design a simple quest system to track the status of ongoing quests.
## Characters
### Can players have more than one Character active at a time or are they allowed to multi-play?
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts`
and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE`
setting, which has a value from `0` (default) to `3`.
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
account from another client you'll be disconnected from the first. When creating a new account, a Character
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
which had no separation between Account and Character.
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
multiple clients and see the same output in all of them.
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
open, but only one client per Character.
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
can control each Character from multiple clients, seeing the same output from each Character.
We will go with a multi-role game, so we will use `MULTISESSION_MODE=3` for this tutorial.
### How does the character-generation work?
There are a few common ways to do character generation:
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify
your character. When you are done you move to the next room. Only use this if you have another reason for
using a room, like having a training dummy to test skills on, for example.
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk
between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
using custom commands they will likely never use again after this.
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented
with a sequential menu.
For the tutorial we will use a menu to let the user modify each section of their character sheet in any order
until they are happy.
### How do you implement different "classes" or "races"?
The way classes and races work in most RPGs (as well as in OpenAdventure) is that they act as static 'templates'
that inform which bonuses and special abilities you have. This means that all we need to store on the
Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just
be looked up when we need it.
### If a Character can hide in a room, what skill will decide if they are detected?
Hiding means a few things.
- The Character should not appear in the room's description / character list
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
or `look <name>` if the named character is in hiding.
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
find the person (probably based on skill checks).
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
We will _not_ be including a hide-mechanic in EvAdventure though.
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
ways to implement this:
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's
easy to tell when you should gain it.
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
just be lurking and not be actually playing the game at any given time.
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
how often you post, how long your emotes are etc.
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
- XP from fails - you only gain XP when failing rolls.
- XP from other players - other players can award you XP for good RP.
For EvAdventure we will use Open Adventure's rules for XP, which will be driven by kills and quest successes.
### May player-characters attack each other (PvP)?
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new
can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They
will not be as accepting if they perceive another player is perceived as being overpowered. PvP means that you
have to be very careful to balance the game - all characters does not have to be exactly equal but they should
all be viable to play a fun game with. PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in
a political game or gaining market share when selling their crafted merchandise.
For the EvAdventure we will support both Player-vs-environment combat and turn-based PvP. We will allow players
to barter with each other (so potentially scam others?) but that's the extent of it. We will focus on showing
off techniques and will not focus on making a balanced game.
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
This is another big decision that strongly affects the mood and style of your game.
Perma-death means that once your character dies, it's gone and you have to make a new one.
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
your triumph is an actual feat.
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
up for newcomers.
- It lowers inflation, since the hoarded resources of a dead character can be removed.
- It gives capital punishment genuine discouraging power.
- It's realistic.
Perma-death comes with some severe disadvantages however.
- It's impopular. Many players will just not play a game where they risk losing their beloved character
just like that.
- Many players say they like the _idea_ of permadeath except when it could happen to them.
- It can limit roleplaying freedom and make people refuse to take any risks.
- It may make players even more reluctant to play conflict-driving 'bad guys'.
- Game balance is much, much more important when results are "final". This escalates the severity of 'unfairness'
a hundred-fold. Things like bugs or exploits can also lead to much more server effects.
For these reasons, it's very common to do hybrid systems. Some tried variations:
- NPCs cannot kill you, only other players can.
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely
hurt/incapacitated.
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
you die permanently.
- Death just means harsh penalties, not actual death.
- When you die you can fight your way back to life from some sort of afterlife.
- You'll only die permanently if you as a player explicitly allows it.
For our tutorial-game we will not be messing with perma-death; instead your defeat will mean you will re-spawn
back at your home location with a fraction of your health.
## Conclusions
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are
many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
playable game!
Before starting to code in earnest a good coder should always do an inventory of all the stuff they _don't_ need
to code themselves. So in the next lesson we will check out what help we have from Evennia's _contribs_.

View file

@ -1,802 +0,0 @@
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
# Making a sittable object
In this lesson we will go through how to make a chair you can sit on. Sounds easy, right?
Well it is. But in the process of making the chair we will need to consider the various ways
to do it depending on how we want our game to work.
The goals of this lesson are as follows:
- We want a new 'sittable' object, a Chair in particular".
- We want to be able to use a command to sit in the chair.
- Once we are sitting in the chair it should affect us somehow. To demonstrate this we'll
set a flag "Resting" on the Character sitting in the Chair.
- When you sit down you should not be able to walk to another room without first standing up.
- A character should be able to stand up and move away from the chair.
There are two main ways to design the commands for sitting and standing up.
- You can store the commands on the chair so they are only available when a chair is in the room
- You can store the commands on the Character so they are always available and you must always specify
which chair to sit on.
Both of these are very useful to know about, so in this lesson we'll try both. But first
we need to handle some basics.
## Don't move us when resting
When you are sitting in a chair you can't just walk off without first standing up.
This requires a change to our Character typeclass. Open `mygame/typeclasses/characters.py`:
```python
# ...
class Character(DefaultCharacter):
# ...
def at_pre_move(self, destination):
"""
Called by self.move_to when trying to move somewhere. If this returns
False, the move is immediately cancelled.
"""
if self.db.is_resting:
self.msg("You can't go anywhere while resting.")
return False
return True
```
When moving somewhere, [character.move_to](evennia.objects.objects.DefaultObject.move_to) is called. This in turn
will call `character.at_pre_move`. Here we look for an Attribute `is_resting` (which we will assign below)
to determine if we are stuck on the chair or not.
## Making the Chair itself
Next we need the Chair itself, or rather a whole family of "things you can sit on" that we will call
_sittables_. We can't just use a default Object since we want a sittable to contain some custom code. We need
a new, custom Typeclass. Create a new module `mygame/typeclasses/sittables.py` with the following content:
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
def at_object_creation(self):
self.db.sitter = None
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
current = self.db.sitter
if current:
if current == sitter:
sitter.msg("You are already sitting on {self.key}.")
else:
sitter.msg(f"You can't sit on {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
sitter.msg(f"You sit on {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
stander.msg(f"You are not sitting on {self.key}.")
else:
self.db.sitting = None
stander.db.is_resting = False
stander.msg(f"You stand up from {self.key}")
```
Here we have a small Typeclass that handles someone trying to sit on it. It has two methods that we can simply
call from a Command later. We set the `is_resting` Attribute on the one sitting down.
One could imagine that one could have the future `sit` command check if someone is already sitting in the
chair instead. This would work too, but letting the `Sittable` class handle the logic around who can sit on it makes
logical sense.
We let the typeclass handle the logic, and also let it do all the return messaging. This makes it easy to churn out
a bunch of chairs for people to sit on. But it's not perfect. The `Sittable` class is general. What if you want to
make an armchair. You sit "in" an armchair rather than "on" it. We _could_ make a child class of `Sittable` named
`SittableIn` that makes this change, but that feels excessive. Instead we will make it so that Sittables can
modify this per-instance:
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
adjective = self.db.adjective
current = self.db.sitter
if current:
if current == sitter:
sitter.msg(f"You are already sitting {adjective} {self.key}.")
else:
sitter.msg(
f"You can't sit {adjective} {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
sitter.msg(f"You sit {adjective} {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
else:
self.db.sitting = None
stander.db.is_resting = False
stander.msg(f"You stand up from {self.key}")
```
We added a new Attribute `adjective` which will probably usually be `in` or `on` but could also be `at` if you
want to be able to sit _at a desk_ for example. A regular builder would use it like this:
> create/drop armchair : sittables.Sittable
> set armchair/adjective = in
This is probably enough. But all those strings are hard-coded. What if we want some more dramatic flair when you
sit down?
You sit down and a whoopie cushion makes a loud fart noise!
For this we need to allow some further customization. Let's let the current strings be defaults that
we can replace.
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
"""
An object one can sit on
Customizable Attributes:
adjective: How to sit (on, in, at etc)
Return messages (set as Attributes):
msg_already_sitting: Already sitting here
format tokens {adjective} and {key}
msg_other_sitting: Someone else is sitting here.
format tokens {adjective}, {key} and {other}
msg_sitting_down: Successfully sit down
format tokens {adjective}, {key}
msg_standing_fail: Fail to stand because not sitting.
format tokens {adjective}, {key}
msg_standing_up: Successfully stand up
format tokens {adjective}, {key}
"""
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
adjective = self.db.adjective
current = self.db.sitter
if current:
if current == sitter:
if self.db.msg_already_sitting:
sitter.msg(
self.db.msg_already_sitting.format(
adjective=self.db.adjective, key=self.key))
else:
sitter.msg(f"You are already sitting {adjective} {self.key}.")
else:
if self.db.msg_other_sitting:
sitter.msg(self.db.msg_already_sitting.format(
other=current.key, adjective=self.db.adjective, key=self.key))
else:
sitter.msg(f"You can't sit {adjective} {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
if self.db.msg_sitting_down:
sitter.msg(self.db.msg_sitting_down.format(adjective=adjective, key=self.key))
else:
sitter.msg(f"You sit {adjective} {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
if self.db.msg_standing_fail:
stander.msg(self.db.msg_standing_fail.format(
adjective=self.db.adjective, key=self.key))
else:
stander.msg(f"You are not sitting {self.db.adjective} {self.key}")
else:
self.db.sitting = None
stander.db.is_resting = False
if self.db.msg_standing_up:
stander.msg(self.db.msg_standing_up.format(
adjective=self.db.adjective, key=self.key))
else:
stander.msg(f"You stand up from {self.key}")
```
Here we really went all out with flexibility. If you need this much is up to you.
We added a bunch of optional Attributes to hold alternative versions of all the messages.
There are some things to note:
- We don't actually initiate those Attributes in `at_object_creation`. This is a simple
optimization. The assumption is that _most_ chairs will probably not be this customized.
So initiating a bunch of Attributes to, say, empty strings would be a lot of useless database calls.
The drawback is that the available Attributes become less visible when reading the code. So we add a long
describing docstring to the end to explain all you can use.
- We use `.format` to inject formatting-tokens in the text. The good thing about such formatting
markers is that they are _optional_. They are there if you want them, but Python will not complain
if you don't include some or any of them. Let's see an example:
> reload # if you have new code
> create/drop armchair : sittables.Sittable
> set armchair/adjective = in
> set armchair/msg_sitting_down = As you sit down {adjective} {key}, life feels easier.
> set armchair/msg_standing_up = You stand up from {key}. Life resumes.
The `{key}` and `{adjective}` are examples of optional formatting markers. Whenever the message is
returned, the format-tokens within will be replaced with `armchair` and `in` respectively. Should we
rename the chair later, this will show in the messages automatically (since `{key}` will change).
We have no Command to use this chair yet. But we can try it out with `py`:
> py self.search("armchair").do_sit(self)
As you sit down in armchair, life feels easier.
> self.db.resting
True
> py self.search("armchair").do_stand(self)
You stand up from armchair. Life resumes
> self.db.resting
False
If you follow along and get a result like this, all seems to be working well!
## Command variant 1: Commands on the chair
This way to implement `sit` and `stand` puts new cmdsets on the Sittable itself.
As we've learned before, commands on objects are made available to others in the room.
This makes the command easy but instead adds some complexity in the management of the CmdSet.
This is how it will look if `armchair` is in the room:
> sit
As you sit down in armchair, life feels easier.
What happens if there are sittables `sofa` and `barstool` also in the room? Evennia will automatically
handle this for us and allow us to specify which one we want:
> sit
More than one match for 'sit' (please narrow target):
sit-1 (armchair)
sit-2 (sofa)
sit-3 (barstool)
> sit-1
As you sit down in armchair, life feels easier.
To keep things separate we'll make a new module `mygame/commands/sittables.py`:
```{sidebar} Separate Commands and Typeclasses?
You can organize these things as you like. If you wanted you could put the sit-command + cmdset
together with the `Sittable` typeclass in `mygame/typeclasses/sittables.py`. That has the advantage of
keeping everything related to sitting in one place. But there is also some organizational merit to
keeping all Commands in one place as we do here.
```
```python
from evennia import Command, CmdSet
class CmdSit(Command):
"""
Sit down.
"""
key = "sit"
def func(self):
self.obj.do_sit(self.caller)
class CmdStand(Command):
"""
Stand up.
"""
key = "stand"
def func(self):
self.obj.do_stand(self.caller)
class CmdSetSit(CmdSet):
priority = 1
def at_cmdset_creation(self):
self.add(CmdSit)
self.add(CmdStand)
```
As seen, the commands are nearly trivial. `self.obj` is the object to which we added the cmdset with this
Command (so for example a chair). We just call the `do_sit/stand` on that object and the `Sittable` will
do the rest.
Why that `priority = 1` on `CmdSetSit`? This makes same-named Commands from this cmdset merge with a bit higher
priority than Commands from the Character-cmdset. Why this is a good idea will become clear shortly.
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
```python
from evennia import DefaultObject
from commands.sittables import CmdSetSit # <- new
class Sittable(DefaultObject):
"""
(docstring)
"""
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
self.cmdset.add_default(CmdSetSit) # <- new
```
Any _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not,
since `at_object_creation` will not re-run for already existing objects. We can update it manually:
> reload
> update armchair
We could also update all existing sittables (all on one line):
> py from typeclasses.sittables import Sittable ;
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
> The above shows an example of a _list comprehension_. Think of it as an efficient way to construct a new list
all in one line. You can read more about list comprehensions
[here in the Python docs](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions).
We should now be able to use `sit` while in the room with the armchair.
> sit
As you sit down in armchair, life feels easier.
> stand
You stand up from armchair.
One issue with placing the `sit` (or `stand`) Command "on" the chair is that it will not be available when in a
room without a Sittable object:
> sit
Command 'sit' is not available. ...
This is practical but not so good-looking; it makes it harder for the user to know a `sit` action is at all
possible. Here is a trick for fixing this. Let's add _another_ Command to the bottom
of `mygame/commands/sittables.py`:
```python
# ...
class CmdNoSitStand(Command):
"""
Sit down or Stand up
"""
key = "sit"
aliases = ["stand"]
def func(self):
if self.cmdname == "sit":
self.msg("You have nothing to sit on.")
else:
self.msg("You are not sitting down.")
```
Here we have a Command that is actually two - it will answer to both `sit` and `stand` since we
added `stand` to its `aliases`. In the command we look at `self.cmdname`, which is the string
_actually used_ to call this command. We use this to return different messages.
We don't need a separate CmdSet for this, instead we will add this
to the default Character cmdset. Open `mygame/commands/default_cmdsets.py`:
```python
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdNoSitStand)
```
To test we'll build a new location without any comfy armchairs and go there:
> reload
> tunnel n = kitchen
north
> sit
You have nothing to sit on.
> south
sit
As you sit down in armchair, life feels easier.
We now have a fully functioning `sit` action that is contained with the chair itself. When no chair is around, a
default error message is shown.
How does this work? There are two cmdsets at play, both of which have a `sit` Command. As you may remember we
set the chair's cmdset to `priority = 1`. This is where that matters. The default Character cmdset has a
priority of 0. This means that whenever we enter a room with a Sittable thing, the `sit` command
from _its_ cmdset will take _precedence_ over the Character cmdset's version. So we are actually picking
_different_ `sit` commands depending on circumstance! The user will never be the wiser.
So this handles `sit`. What about `stand`? That will work just fine:
> stand
You stand up from armchair.
> north
> stand
You are not sitting down.
We have one remaining problem with `stand` though - what happens when you are sitting down and try to
`stand` in a room with more than one chair:
> stand
More than one match for 'stand' (please narrow target):
stand-1 (armchair)
stand-2 (sofa)
stand-3 (barstool)
Since all the sittables have the `stand` Command on them, you'll get a multi-match error. This _works_ ... but
you could pick _any_ of those sittables to "stand up from". That's really weird and non-intuitive. With `sit` it
was okay to get a choice - Evennia can't know which chair we intended to sit on. But we know which chair we
sit on so we should only get _its_ `stand` command.
We will fix this with a `lock` and a custom `lock function`. We want a lock on the `stand` Command that only
makes it available when the caller is actually sitting on the chair the `stand` command is on.
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
```python
# ...
class CmdStand(Command):
"""
Stand up.
"""
key = "stand"
lock = "cmd:sitsonthis()" # < this is new
def func(self):
self.obj.do_stand(self.caller)
# ...
```
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
the lock. The `cmd` means that it will check the lock when determining if a user has access to this command or not.
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
Open `mygame/server/conf/lockfuncs.py` to add it!
```python
"""
(module lockstring)
"""
# ...
def sitsonthis(accessing_obj, accessed_obj, *args, **kwargs):
"""
True if accessing_obj is sitting on/in the accessed_obj.
"""
return accessed_obj.db.sitting == accessing_obj
# ...
```
Evennia knows that all functions in `mygame/server/conf/lockfuncs` should be possible to use in a lock definition.
The arguments are required and Evennia will pass all relevant objects to them:
```{sidebar} Lockfuncs
Evennia provides a large number of default lockfuncs, such as checking permission-levels,
if you are carrying or are inside the accessed object etc. There is no concept of 'sitting'
in default Evennia however, so this we need to specify ourselves.
```
- `accessing_obj` is the one trying to access the lock. So us, in this case.
- `accessed_obj` is the entity we are trying to gain a particular type of access to. So the chair.
- `args` is a tuple holding any arguments passed to the lockfunc. Since we use `sitsondthis()` this will
be empty (and if we add anything, it will be ignored).
- `kwargs` is a tuple of keyword arguments passed to the lockfuncs. This will be empty as well in our example.
If you are superuser, it's important that you `quell` yourself before trying this out. This is because the superuser
bypasses all locks - it can never get locked out, but it means it will also not see the effects of a lock like this.
> reload
> quell
> stand
You stand up from armchair
None of the other sittables' `stand` commands passed the lock and only the one we are actually sitting on did.
Adding a Command to the chair object like this is powerful and a good technique to know. It does come with some
caveats though that one needs to keep in mind.
We'll now try another way to add the `sit/stand` commands.
## Command variant 2: Command on Character
Before we start with this, delete the chairs you've created (`del armchair` etc) and then do the following
changes:
- In `mygame/typeclasses/sittables.py`, comment out the line `self.cmdset.add_default(CmdSetSit)`.
- In `mygame/commands/default_cmdsets.py`, comment out the line `self.add(sittables.CmdNoSitStand)`.
This disables the on-object command solution so we can try an alternative. Make sure to `reload` so the
changes are known to Evennia.
In this variation we will put the `sit` and `stand` commands on the `Character` instead of on the chair. This
makes some things easier, but makes the Commands themselves more complex because they will not know which
chair to sit on. We can't just do `sit` anymore. This is how it will work.
> sit <chair>
You sit on chair.
> stand
You stand up from chair.
Open `mygame/commands.sittables.py` again. We'll add a new sit-command. We name the class `CmdSit2` since
we already have `CmdSit` from the previous example. We put everything at the end of the module to
keep it separate.
```python
from evennia import Command, CmdSet
from evennia import InterruptCommand # <- this is new
class CmdSit(Command):
# ...
# ...
# new from here
class CmdSit2(Command):
"""
Sit down.
Usage:
sit <sittable>
"""
key = "sit"
def parse(self):
self.args = self.args.strip()
if not self.args:
self.caller.msg("Sit on what?")
raise InterruptCommand
def func(self):
# self.search handles all error messages etc.
sittable = self.caller.search(self.args)
if not sittable:
return
try:
sittable.do_sit(self.caller)
except AttributeError:
self.caller.msg("You can't sit on that!")
```
With this Command-variation we need to search for the sittable. A series of methods on the Command
are run in sequence:
1. `Command.at_pre_command` - this is not used by default
2. `Command.parse` - this should parse the input
3. `Command.func` - this should implement the actual Command functionality
4. `Command.at_post_func` - this is not used by default
So if we just `return` in `.parse`, `.func` will still run, which is not what we want. To immediately
abort this sequence we need to `raise InterruptCommand`.
```{sidebar} Raising exceptions
Raising an exception allows for immediately interrupting the current program flow. Python
automatically raises error-exceptions when detecting problems with the code. It will be
raised up through the sequence of called code (the 'stack') until it's either `caught` with
a `try ... except` or reaches the outermost scope where it'll be logged or displayed.
```
`InterruptCommand` is an _exception_ that the Command-system catches with the understanding that we want
to do a clean abort. In the `.parse` method we strip any whitespaces from the argument and
sure there actuall _is_ an argument. We abort immediately if there isn't.
We we get to `.func` at all, we know that we have an argument. We search for this and abort if we there was
a problem finding the target.
> We could have done `raise InterruptCommand` in `.func` as well, but `return` is a little shorter to write
> and there is no harm done if `at_post_func` runs since it's empty.
Next we call the found sittable's `do_sit` method. Note that we wrap this call like this:
```python
try:
# code
except AttributeError:
# stuff to do if AttributeError exception was raised
```
The reason is that `caller.search` has no idea we are looking for a Sittable. The user could have tried
`sit wall` or `sit sword`. These don't have a `do_sit` method _but we call it anyway and handle the error_.
This is a very "Pythonic" thing to do. The concept is often called "leap before you look" or "it's easier to
ask for forgiveness than for permission". If `sittable.do_sit` does not exist, Python will raise an `AttributeError`.
We catch this with `try ... except AttributeError` and convert it to a proper error message.
While it's useful to learn about `try ... except`, there is also a way to leverage Evennia to do this without
`try ... except`:
```python
# ...
def func(self):
# self.search handles all error messages etc.
sittable = self.caller.search(
self.args,
typeclass="typeclasses.sittables.Sittable")
if not sittable:
return
sittable.do_sit(self.caller)
```
```{sidebar} Continuing across multiple lines
Note how the `.search()` method's arguments are spread out over multiple
lines. This works for all lists, tuples and other listings and is
a good way to avoid very long and hard-to-read lines.
```
The `caller.search` method has an keyword argument `typeclass` that can take either a python-path to a
typeclass, the typeclass itself, or a list of either to widen the allowed options. In this case we know
for sure that the `sittable` we get is actually a `Sittable` class and we can call `sittable.do_sit` without
needing to worry about catching errors.
Let's do the `stand` command while we are at it. Again, since the Command is external to the chair we don't
know which object we are sitting in and have to search for it.
```python
class CmdStand2(Command):
"""
Stand up.
Usage:
stand
"""
key = "stand"
def func(self):
caller = self.caller
# find the thing we are sitting on/in, by finding the object
# in the current location that as an Attribute "sitter" set
# to the caller
sittable = caller.search(
caller,
candidates=caller.location.contents,
attribute_name="sitter",
typeclass="typeclasses.sittables.Sittable")
# if this is None, the error was already reported to user
if not sittable:
return
sittable.do_stand(caller)
```
This forced us to to use the full power of the `caller.search` method. If we wanted to search for something
more complex we would likely need to break out a [Django query](../Part1/Django-queries.md) to do it. The key here is that
we know that the object we are looking for is a `Sittable` and that it must have an Attribute named `sitter`
which should be set to us, the one sitting on/in the thing. Once we have that we just call `.do_stand` on it
and let the Typeclass handle the rest.
All that is left now is to make this available to us. This type of Command should be available to us all the time
so we can put it in the default Cmdset` on the Character. Open `mygame/default_cmdsets.py`
```python
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdSit2)
self.add(sittables.CmdStand2)
```
Now let's try it out:
> reload
> create/drop sofa : sittables.Sittable
> sit sofa
You sit down on sofa.
> stand
You stand up from sofa.
## Conclusions
In this lesson we accomplished quite a bit:
- We modified our `Character` class to avoid moving when sitting down.
- We made a new `Sittable` typeclass
- We tried two ways to allow a user to interact with sittables using `sit` and `stand` commands.
Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (variant 1) would work just fine
together with the `sit` command sitting "on" the Character (variant 2). There is nothing stopping you from
mixing them, or even try a third solution that better fits what you have in mind.
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)

View file

@ -0,0 +1,414 @@
# Player Characters
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
assumptions about the "Player Character" entity:
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
- It should have a `.heal(amount)` method.
So we have some guidelines of how it should look! A Character is a database entity with values that
should be able to be changed over time. It makes sense to base it off Evennia's
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
RPG, it will hold everything relevant to that PC.
## Inheritance structure
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
we could use a class inheritance like this:
```python
from evennia import DefaultCharacter
class EvAdventureCharacter(DefaultCharacter):
# stuff
class EvAdventureNPC(EvAdventureCharacter):
# more stuff
class EvAdventureMob(EvAdventureNPC):
# more stuff
```
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
PCs like this:
```python
from evennia import DefaultCharacter
class EvAdventureCharacter(DefaultCharacter):
# stuff
class EvAdventureNPC(DefaultCharacter):
# separate stuff
class EvAdventureMob(EvadventureNPC):
# more separate stuff
```
Nevertheless, there are some things that _should_ be common for all 'living things':
- All can take damage.
- All can die.
- All can heal
- All can hold and lose coins
- All can loot their fallen foes.
- All can get looted when defeated.
We don't want to code this separately for every class but we no longer have a common parent
class to put it on. So instead we'll use the concept of a _mixin_ class:
```python
from evennia import DefaultCharacter
class LivingMixin:
# stuff common for all living things
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# stuff
class EvAdventureNPC(LivingMixin, DefaultCharacter):
# stuff
class EvAdventureMob(LivingMixin, EvadventureNPC):
# more stuff
```
```{sidebar}
In [evennia/contrib/tutorials/evadventure/characters.py](evennia.contrib.tutorials.evadventure.characters)
is an example of a character class structure.
```
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
extra functionality all living things should be able to do. This is an example of
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
since it can also get confusing to follow the code.
## Living mixin class
> Create a new module `mygame/evadventure/characters.py`
Let's get some useful common methods all living things should have in our game.
```python
# in mygame/evadventure/characters.py
from .rules import dice
class LivingMixin:
# makes it easy for mobs to know to attack PCs
is_pc = False
def heal(self, hp):
"""
Heal hp amount of health, not allowing to exceed our max hp
"""
damage = self.hp_max - self.hp
healed = min(damage, hp)
self.hp += healed
self.msg("You heal for {healed} HP.")
def at_pay(self, amount):
"""When paying coins, make sure to never detract more than we have"""
amount = min(amount, self.coins)
self.coins -= amount
return amount
def at_damage(self, damage, attacker=None):
"""Called when attacked and taking damage."""
self.hp -= damage
def at_defeat(self):
"""Called when defeated. By default this means death."""
self.at_death()
def at_death(self):
"""Called when this thing dies."""
# this will mean different things for different living things
pass
def at_do_loot(self, looted):
"""Called when looting another entity"""
looted.at_looted(self)
def at_looted(self, looter):
"""Called when looted by another entity"""
# default to stealing some coins
max_steal = dice.roll("1d10")
stolen = self.at_pay(max_steal)
looter.coins += stolen
```
Most of these are empty since they will behave differently for characters and npcs. But having them
in the mixin means we can expect these methods to be available for all living things.
## Character class
We will now start making the basic Character class, based on what we need from _Knave_.
```python
# in mygame/evadventure/characters.py
from evennia import DefaultCharacter, AttributeProperty
from .rules import dice
class LivingMixin:
# ...
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"""
A character to use for EvAdventure.
"""
is_pc = True
strength = AttributeProperty(1)
dexterity = AttributeProperty(1)
constitution = AttributeProperty(1)
intelligence = AttributeProperty(1)
wisdom = AttributeProperty(1)
charisma = AttributeProperty(1)
hp = AttributeProperty(8)
hp_max = AttributeProperty(8)
level = AttributeProperty(1)
xp = AttributeProperty(0)
coins = AttributeProperty(0)
def at_defeat(self):
"""Characters roll on the death table"""
if self.location.allow_death:
# this allow rooms to have non-lethal battles
dice.roll_death(self)
else:
self.location.msg_contents(
"$You() $conj(collapse) in a heap, alive but beaten.",
from_obj=self)
self.heal(self.hp_max)
def at_death(self):
"""We rolled 'dead' on the death table."""
self.location.msg_contents(
"$You() collapse in a heap, embraced by death.",
from_obj=self)
# TODO - go back into chargen to make a new character!
```
We make an assumption about our rooms here - that they have a property `.allow_death`. We need
to make a note to actually add such a property to rooms later!
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible
on every character in several ways:
- As `character.strength`
- As `character.db.strength`
- As `character.attributes.get("strength")`
See [Attributes](../../../Components/Attributes.md) for seeing how Attributes work.
Unlike in base _Knave_, we store `coins` as a separate Attribute rather than as items in the inventory,
this makes it easier to handle barter and trading later.
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()`
from the `LivingMixin` class.
### Funcparser inlines
This piece of code is worth some more explanation:
```python
self.location.msg_contents(
"$You() $conj(collapse) in a heap, alive but beaten.",
from_obj=self)
```
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a
message to everything inside my current location". In other words, send a message to everyone
in the same place as the character.
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that
execute
in the string. The resulting string may look different for different audiences. The `$You()` inline
function will use `from_obj` to figure out who 'you' are and either show your name or 'You'.
The `$conj()` (verb conjugator) will tweak the (English) verb to match.
- You will see: `"You collapse in a heap, alive but beaten."`
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
Note how `$conj()` chose `collapse/collapses` to make the sentences grammatically correct.
### Backtracking
We make our first use of the `rules.dice` roller to roll on the death table! As you may recall, in the
previous lesson, we didn't know just what to do when rolling 'dead' on this table. Now we know - we
should be calling `at_death` on the character. So let's add that where we had TODOs before:
```python
# mygame/evadventure/rules.py
class EvAdventureRollEngine:
# ...
def roll_death(self, character):
ability_name = self.roll_random_table("1d8", death_table)
if ability_name == "dead":
# kill the character!
character.at_death() # <------ TODO no more
else:
# ...
if current_ability < -10:
# kill the character!
character.at_death() # <------- TODO no more
else:
# ...
```
## Connecting the Character with Evennia
You can easily make yourself an `EvAdventureCharacter` in-game by using the
`type` command:
type self = evadventure.characters.EvAdventureCharacter
You can now do `examine self` to check your type updated.
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
the `Character` class in `mygame/typeclasses/characters.py`).
There are thus two ways to weave your new Character class into Evennia:
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_CLASS = "evadventure.characters.EvAdventureCharacter"`.
2. Or, change `typeclasses.characters.Character` to inherit from `EvAdventureCharacter`.
You must always reload the server for changes like this to take effect.
```{important}
In this tutorial we are making all changes in a folder `mygame/evadventure/`. This means we can isolate
our code but means we need to do some extra steps to tie the character (and other objects) into Evennia.
For your own game it would be just fine to start editing `mygame/typeclasses/characters.py` directly
instead.
```
## Unit Testing
> Create a new module `mygame/evadventure/tests/test_characters.py`
For testing, we just need to create a new EvAdventure character and check
that calling the methods on it doesn't error out.
```python
# mygame/evadventure/tests/test_characters.py
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest
from ..characters import EvAdventureCharacter
class TestCharacters(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.character = create.create_object(EvAdventureCharacter, key="testchar")
def test_heal(self):
self.character.hp = 0
self.character.hp_max = 8
self.character.heal(1)
self.assertEqual(self.character.hp, 1)
# make sure we can't heal more than max
self.character.heal(100)
self.assertEqual(self.character.hp, 8)
def test_at_pay(self):
self.character.coins = 100
result = self.character.at_pay(60)
self.assertEqual(result, 60)
self.assertEqual(self.character.coins, 40)
# can't get more coins than we have
result = self.character.at_pay(100)
self.assertEqual(result, 40)
self.assertEqual(self.character.coins, 0)
# tests for other methods ...
```
If you followed the previous lessons, these tests should look familiar. Consider adding
tests for other methods as practice. Refer to previous lessons for details.
For running the tests you do:
evennia test --settings settings.py .evadventure.tests.test_character
## About races and classes
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
add these functions.
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
an Attribute on your Character:
```python
# mygame/evadventure/characters.py
from evennia import DefaultCharacter, AttributeProperty
# ...
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# ...
charclass = AttributeProperty("Fighter")
charrace = AttributeProperty("Human")
```
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
`race` as `charrace` thus matches in style.
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
## Summary
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
like under _Knave_.
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
the command
type self = evadventure.characters.EvAdventureCharacter
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
Check out your strength with
py self.strength = 3
```{important}
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
Attributes added with `AttributeProperty` are not available until they have been accessed at
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
then on.
```

View file

@ -0,0 +1,711 @@
# Character Generation
In previous lessons we have established how a character looks. Now we need to give the player a
chance to create one.
## How it will work
A fresh Evennia install will automatically create a new Character with the same name as your
Account when you log in. This is quick and simple and mimics older MUD styles. You could picture
doing this, and then customizing the Character in-place.
We will be a little more sophisticated though. We want the user to be able to create a character
using a menu when they log in.
We do this by editing `mygame/server/conf/settings.py` and adding the line
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
When doing this, connecting with the game with a new account will land you in "OOC" mode. The
ooc-version of `look` (sitting in the Account cmdset) will show a list of available characters
if you have any. You can also enter `charcreate` to make a new character. The `charcreate` is a
simple command coming with Evennia that just lets you make a new character with a given name and
description. We will later modify that to kick off our chargen. For now we'll just keep in mind
that's how we'll start off the menu.
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
STR +1
DEX +2
CON +1
INT +3
WIS +1
CHA +2
You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing.
You were a herbalist, but you were pursued and ended up a knave. You are honest but also
suspicious. You are of the neutral alignment.
Your belongings:
Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch,
tinderbox, chisel, whistle
----------------------------------------------------------------------------------------
1. Change your name
2. Swap two of your ability scores (once)
3. Accept and create character
```
If you select 1, you get a new menu node:
```
Your current name is Silas. Enter a new name or leave empty to abort.
-----------------------------------------------------------------------------------------
```
You can now enter a new name. When pressing return you'll get back to the first menu node
showing your character, now with the new name.
If you select 2, you go to another menu node:
```
Your current abilities:
STR +1
DEX +2
CON +1
INT +3
WIS +1
CHA +2
You can swap the values of two abilities around.
You can only do this once, so choose carefully!
To swap the values of e.g. STR and INT, write 'STR INT'. Empty to abort.
------------------------------------------------------------------------------------------
```
If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back
to the main node to see your new character, but this time the option to swap will no longer be
available (you can only do it once).
If you finally select the `Accept and create character` option, the character will be created
and you'll leave the menu;
Character was created!
## Random tables
```{sidebar}
Full Knave random tables are found in
[evennia/contrib/tutorials/evadventure/random_tables.py](evennia.contrib.tutorials.evadventure.random_tables).
```
> Make a new module `mygame/evadventure/random_tables.py`.
Since most of _Knave_'s character generation is random we will need to roll on random tables
from the _Knave_ rulebook. While we added the ability to roll on a random table back in the
[Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
```
# in mygame/evadventure/random_tables.py
chargen_tables = {
"physique": [
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
],
"face": [
"bloated", "blunt", "bony", # ...
], # ...
}
```
The tables are just copied from the _Knave_ rules. We group the aspects in a dict
`character_generation` to separate chargen-only tables from other random tables we'll also
keep in here.
## Storing state of the menu
```{sidebar}
There is a full implementation of the chargen in
[evennia/contrib/tutorials/evadventure/chargen.py](evennia.contrib.tutorials.evadventure.chargen).
```
> create a new module `mygame/evadventure/chargen.py`.
During character generation we will need an entity to store/retain the changes, like a
'temporary character sheet'.
```python
# in mygame/evadventure/chargen.py
from .random_tables import chargen_tables
from .rules import dice
class TemporaryCharacterSheet:
def _random_ability(self):
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
def __init__(self):
self.ability_changes = 0 # how many times we tried swap abilities
# name will likely be modified later
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
# base attribute values
self.strength = self._random_ability()
self.dexterity = self._random_ability()
self.constitution = self._random_ability()
self.intelligence = self._random_ability()
self.wisdom = self._random_ability()
self.charisma = self._random_ability()
# physical attributes (only for rp purposes)
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
face = dice.roll_random_table("1d20", chargen_tables["face"])
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
background = dice.roll_random_table("1d20", chargen_tables["background"])
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
self.desc = (
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
f" and {clothing} clothing. You were a {background.title()}, but you were"
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
f" {alignment} alignment."
)
#
self.hp_max = max(5, dice.roll("1d8"))
self.hp = self.hp_max
self.xp = 0
self.level = 1
# random equipment
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
self.backpack = [
"ration",
"ration",
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
]
```
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.
### Showing the sheet
Now that we have our temporary character sheet, we should make it easy to visualize it.
```python
# in mygame/evadventure/chargen.py
_TEMP_SHEET = """
{name}
STR +{strength}
DEX +{dexterity}
CON +{constitution}
INT +{intelligence}
WIS +{wisdom}
CHA +{charisma}
{description}
Your belongings:
{equipment}
"""
class TemporaryCharacterSheet:
# ...
def show_sheet(self):
equipment = (
str(item)
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
if item
)
return _TEMP_SHEET.format(
name=self.name,
strength=self.strength,
dexterity=self.dexterity,
constitution=self.constitution,
intelligence=self.intelligence,
wisdom=self.wisdom,
charisma=self.charisma,
description=self.desc,
equipment=", ".join(equipment),
)
```
The new `show_sheet` method collect the data from the temporary sheet and return it in a pretty
form. Making a 'template' string like `_TEMP_SHEET` makes it easier to change things later if you want
to change how things look.
### Apply character
Once we are happy with our character, we need to actually create it with the stats we chose.
This is a bit more involved.
```python
# in mygame/evadventure/chargen.py
# ...
from .characters import EvAdventureCharacter
from evennia import create_object
from evennia.prototypes.spawner import spawn
class TemporaryCharacterSheet:
# ...
def apply(self):
# create character object with given abilities
new_character = create_object(
EvAdventureCharacter,
key=self.name,
attrs=(
("strength", self.strength),
("dexterity", self.dexterity),
("constitution", self.constitution),
("intelligence", self.intelligence),
("wisdom", self.wisdom),
("charisma", self.wisdom),
("hp", self.hp),
("hp_max", self.hp_max),
("desc", self.desc),
),
)
# spawn equipment (will require prototypes created before it works)
if self.weapon:
weapon = spawn(self.weapon)
new_character.equipment.move(weapon)
if self.shield:
shield = spawn(self.shield)
new_character.equipment.move(shield)
if self.armor:
armor = spawn(self.armor)
new_character.equipment.move(armor)
if self.helmet:
helmet = spawn(self.helmet)
new_character.equipment.move(helmet)
for item in self.backpack:
item = spawn(item)
new_character.equipment.store(item)
return new_character
```
We use `create_object` to create a new `EvAdventureCharacter`. We feed it with all relevant data
from the temporary character sheet. This is when these become an actual character.
```{sidebar}
A prototype is basically a `dict` describing how the object should be created. Since
it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
things from those prototypes.
```
Each piece of equipment is an object in in its own right. We will here assume that all game
items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
armor" etc.
We haven't actually created those prototypes yet, so for now we'll need to assume they are there.
Once a piece of equipment has been spawned, we make sure to move it into the `EquipmentHandler` we
created in the [Equipment lesson](./Beginner-Tutorial-Equipment.md).
## Initializing EvMenu
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
[EvMenu](../../../Components/EvMenu.md).
```python
# in mygame/evadventure/chargen.py
from evennia import EvMenu
# ...
# chargen menu
# this goes to the bottom of the module
def start_chargen(caller, session=None):
"""
This is a start point for spinning up the chargen from a command later.
"""
menutree = {} # TODO!
# this generates all random components of the character
tmp_character = TemporaryCharacterSheet()
EvMenu(caller, menutree, session=session, tmp_character=tmp_character)
```
This first function is what we will call from elsewhere (for example from a custom `charcreate`
command) to kick the menu into gear.
It takes the `caller` (the one to want to start the menu) and a `session` argument. The latter will help
track just which client-connection we are using (depending on Evennia settings, you could be
connecting with multiple clients).
We create a `TemporaryCharacterSheet` and call `.generate()` to make a random character. We then
feed all this into `EvMenu`.
The moment this happens, the user will be in the menu, there are no further steps needed.
The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump
between.
## Main Node: Choosing what to do
This is the first menu node. It will act as a central hub, from which one can choose different
actions.
```python
# in mygame/evadventure/chargen.py
# ...
# at the end of the module, but before the `start_chargen` function
def node_chargen(caller, raw_string, **kwargs):
tmp_character = kwargs["tmp_character"]
text = tmp_character.show_sheet()
options = [
{
"desc": "Change your name",
"goto": ("node_change_name", kwargs)
}
]
if tmp_character.ability_changes <= 0:
options.append(
{
"desc": "Swap two of your ability scores (once)",
"goto": ("node_swap_abilities", kwargs),
}
)
options.append(
{
"desc": "Accept and create character",
"goto": ("node_apply_character", kwargs)
},
)
return text, options
# ...
```
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While
not required, it helps you track what is a node and not.
Every menu-node, should accept `caller, raw_string, **kwargs` as arguments. Here `caller` is the
`caller` you passed into the `EvMenu` call. `raw_string` is the input given by the user in order
to _get to this node_, so currently empty. The `**kwargs` are all extra keyword arguments passed
into `EvMenu`. They can also be passed between nodes. In this case, we passed the
keyword `tmp_character` to `EvMenu`. We now have the temporary character sheet available in the
node!
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will
show to the user when looking at this node. The `options` are, well, what options should be
presented to move on from here to some other place.
For the text, we simply get a pretty-print of the temporary character sheet. A single option is
defined as a `dict` like this:
```python
{
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
"desc": "text to describe what happens when selecting option",.
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable)
}
```
Multiple option-dicts are returned in a list or tuple. The `goto` option-key is important to
understand. The job of this is to either point directly to another node (by giving its name), or
by pointing to a Python callable (like a function) _that then returns that name_. You can also
pass kwargs (as a dict). This will be made available as `**kwargs` in the callable or next node.
While an option can have a `key`, you can also skip it to just get a running number.
In our `node_chargen` node, we point to three nodes by name: `node_change_name`,
`node_swap_abilities`, and `node_apply_character`. We also make sure to pass along `kwargs`
to each node, since that contains our temporary character sheet.
The middle of these options only appear if we haven't already switched two abilities around - to
know this, we check the `.ability_changes` property to make sure it's still 0.
## Node: Changing your name
This is where you end up if you opted to change your name in `node_chargen`.
```python
# in mygame/evadventure/chargen.py
# ...
# after previous node
def _update_name(caller, raw_string, **kwargs):
"""
Used by node_change_name below to check what user
entered and update the name if appropriate.
"""
if raw_string:
tmp_character = kwargs["tmp_character"]
tmp_character.name = raw_string.lower().capitalize()
return "node_chargen", kwargs
def node_change_name(caller, raw_string, **kwargs):
"""
Change the random name of the character.
"""
tmp_character = kwargs["tmp_character"]
text = (
f"Your current name is |w{tmp_character.name}|n. "
"Enter a new name or leave empty to abort."
)
options = {
"key": "_default",
"goto": (_update_name, kwargs)
}
return text, options
```
There are two functions here - the menu node itself (`node_change_name`) and a
helper _goto_function_ (`_update_name`) to handle the user's input.
For the (single) option, we use a special `key` named `_default`. This makes this option
a catch-all: If the user enters something that does not match any other option, this is
the option that will be used.
Since we have no other options here, we will always use this option no matter what the user enters.
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.
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`.
> Hint: If returning `None` from a goto-callable, you will always return to the last node you
> were at.
## Node: Swapping Abilities around
You get here by selecting the second option from the `node_chargen` node.
```python
# in mygame/evadventure/chargen.py
# ...
# after previous node
_ABILITIES = {
"STR": "strength",
"DEX": "dexterity",
"CON": "constitution",
"INT": "intelligence",
"WIS": "wisdom",
"CHA": "charisma",
}
def _swap_abilities(caller, raw_string, **kwargs):
"""
Used by node_swap_abilities to parse the user's input and swap ability
values.
"""
if raw_string:
abi1, *abi2 = raw_string.split(" ", 1)
if not abi2:
caller.msg("That doesn't look right.")
return None, kwargs
abi2 = abi2[0]
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
caller.msg("Not a familiar set of abilites.")
return None, kwargs
# looks okay = swap values. We need to convert STR to strength etc
tmp_character = kwargs["tmp_character"]
abi1 = _ABILITIES[abi1]
abi2 = _ABILITIES[abi2]
abival1 = getattr(tmp_character, abi1)
abival2 = getattr(tmp_character, abi2)
setattr(tmp_character, abi1, abival2)
setattr(tmp_character, abi2, abival1)
tmp_character.ability_changes += 1
return "node_chargen", kwargs
def node_swap_abilities(caller, raw_string, **kwargs):
"""
One is allowed to swap the values of two abilities around, once.
"""
tmp_character = kwargs["tmp_character"]
text = f"""
Your current abilities:
STR +{tmp_character.strength}
DEX +{tmp_character.dexterity}
CON +{tmp_character.constitution}
INT +{tmp_character.intelligence}
WIS +{tmp_character.wisdom}
CHA +{tmp_character.charisma}
You can swap the values of two abilities around.
You can only do this once, so choose carefully!
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
"""
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
return text, options
```
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and
and a goto-callable helper (`_swap_abilities`). We catch everything the user writes on the
node (such as `WIS CON`) and feed it into the helper.
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.
Finally, we return to `node_chargen` again.
## Node: Creating the Character
We get here from the main node by opting to finish chargen.
```python
node_apply_character(caller, raw_string, **kwargs):
"""
End chargen and create the character. We will also puppet it.
"""
tmp_character = kwargs["tmp_character"]
new_character = tmp_character.apply(caller)
caller.account.db._playable_characters = [new_character]
text = "Character created!"
return text, None
```
When entering the node, we will take the Temporary character sheet and use its `.appy` method to
create a new Character with all equipment.
This is what is called an _end node_, because it returns `None` instead of options. After this,
the menu will exit. We will be back to the default character selection screen. The characters
found on that screen are the ones listed in the `_playable_characters` Attribute, so we need to
also the new character to it.
## Tying the nodes together
```python
def start_chargen(caller, session=None):
"""
This is a start point for spinning up the chargen from a command later.
"""
menutree = { # <----- can now add this!
"node_chargen": node_chargen,
"node_change_name": node_change_name,
"node_swap_abilities": node_swap_abilities,
"node_apply_character": node_apply_character
}
# this generates all random components of the character
tmp_character = TemporaryCharacterSheet()
tmp_character.generate()
EvMenu(caller, menutree, session=session,
startnode="node_chargen", # <-----
tmp_character=tmp_character)
```
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.
## Conclusions
This lesson taught us how to use `EvMenu` to make an interactive character generator. In an RPG
more complex than _Knave_, the menu would be bigger and more intricate, but the same principles
apply.
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.

View file

@ -0,0 +1,5 @@
# In-game Commands
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,5 @@
# Dynamically generated Dungeon
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,599 @@
# Handling Equipment
In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`.
All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items
than you have slot-space for. Also items wielded or worn count towards the slots.
We still need to track what the character is using however: What weapon they have readied affects the damage
they can do. The shield, helmet and armor they use affects their defense.
We have already set up the possible 'wear/wield locations' when we defined our Objects
[in the previous lesson](./Beginner-Tutorial-Objects.md). This is what we have in `enums.py`:
```python
# mygame/evadventure/enums.py
# ...
class WieldLocation(Enum):
BACKPACK = "backpack"
WEAPON_HAND = "weapon_hand"
SHIELD_HAND = "shield_hand"
TWO_HANDS = "two_handed_weapons"
BODY = "body" # armor
HEAD = "head" # helmets
```
Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none).
The BACKPACK is special - it contains any number of items (up to the maximum slot usage).
## EquipmentHandler that saves
> Create a new module `mygame/evadventure/equipment.py`.
```{sidebar}
If you want to understand more about behind how Evennia uses handlers, there is a
[dedicated tutorial](../../Tutorial-Persistent-Handler.md) talking about the principle.
```
In default Evennia, everything you pick up will end up "inside" your character object (that is, have
you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us'
when we pick them up, but we will add more functionality using an _Equipment handler_.
A handler is (for our purposes) an object that sits "on" another entity, containing functionality
for doing one specific thing (managing equipment, in our case).
This is the start of our handler:
```python
# in mygame/evadventure/equipment.py
from .enums import WieldLocation
class EquipmentHandler:
save_attribute = "inventory_slots"
def __init__(self, obj):
# here obj is the character we store the handler on
self.obj = obj
self._load()
def _load(self):
"""Load our data from an Attribute on `self.obj`"""
self.slots = self.obj.attributes.get(
self.save_attribute,
category="inventory",
default={
WieldLocation.WEAPON_HAND: None,
WieldLocation.SHIELD_HAND: None,
WieldLocation.TWO_HANDS: None,
WieldLocation.BODY: None,
WieldLocation.HEAD: None,
WieldLocation.BACKPACK: []
}
)
def _save(self):
"""Save our data back to the same Attribute"""
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
```
This is a compact and functional little handler. Before analyzing how it works, this is how
we will add it to the Character:
```python
# mygame/evadventure/characters.py
# ...
from evennia.utils.utils import lazy_property
from .equipment import EquipmentHandler
# ...
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# ...
@lazy_property
def equipment(self):
return EquipmentHandler(self)
```
After reloading the server, the equipment-handler will now be accessible on character-instances as
character.equipment
The `@lazy_property` works such that it will not load the handler until someone actually tries to
fetch it with `character.equipment`. When that
happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what
enters `__init__` as `.obj` in the `EquipmentHandler` code above.
So we now have a handler on the character, and the handler has a back-reference to the character it sits
on.
Since the handler itself is just a regular Python object, we need to use the `Character` to store
our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember
them even after reloading.
Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named
[Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with
other Attributes.
Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only
have one item except `WieldLocation.BACKPACK`, which is a list.
## Connecting the EquipmentHandler
Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the
object that moves, on the source-location and on its destination. This is the same for all moving things -
whether it's a character moving between rooms or an item being dropping from your hand to the ground.
We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md),
or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll
find out what hooks Evennia will call. Here `self` is the object being moved from
`source_location` to `destination`:
1. `self.at_pre_move(destination)` (abort if return False)
2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
4. `source_location.at_object_leave(self, destination)`
5. `self.announce_move_from(destination)`
6. (move happens here)
7. `self.announce_move_to(source_location)`
8. `destination.at_object_receive(self, source_location)`
9. `self.at_post_move(source_location)`
All of these hooks can be overridden to customize movement behavior. In this case we are interested in
controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as
them 'carrying' it. We have three good hook-candidates to use for this.
- `.at_pre_object_receive` - used to check if you can actually pick something up, or if your equipment-store is full.
- `.at_object_receive` - used to add the item to the equipmenthandler
- `.at_object_leave` - used to remove the item from the equipmenthandler
You could also picture using `.at_pre_object_leave` to restrict dropping (cursed?) items, but
we will skip that for this tutorial.
```python
# mygame/evadventure/character.py
# ...
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# ...
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
"""Called by Evennia before object arrives 'in' this character (that is,
if they pick up something). If it returns False, move is aborted.
"""
return self.equipment.validate_slot_usage(moved_object)
def at_object_receive(self, moved_object, source_location, **kwargs):
"""
Called by Evennia when an object arrives 'in' the character.
"""
self.equipment.add(moved_object)
def at_object_leave(self, moved_object, destination, **kwargs):
"""
Called by Evennia when object leaves the Character.
"""
self.equipment.remove(moved_object)
```
Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`,
`.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before
we can use this, we need to go actually adding those methods.
## Expanding the Equipmenthandler
## `.validate_slot_usage`
Let's start with implementing the first method we came up with above, `validate_slot_usage`:
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
class EquipmentError(TypeError):
"""All types of equipment-errors"""
pass
class EquipmentHandler:
# ...
@property
def max_slots(self):
"""Max amount of slots, based on CON defense (CON + 10)"""
return getattr(self.obj, Ability.CON.value, 1) + 10
def count_slots(self):
"""Count current slot usage"""
slots = self.slots
wield_usage = sum(
getattr(slotobj, "size", 0) or 0
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
backpack_usage = sum(
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
)
return wield_usage + backpack_usage
def validate_slot_usage(self, obj):
"""
Check if obj can fit in equipment, based on its size.
"""
if not inherits_from(obj, EvAdventureObject):
# in case we mix with non-evadventure objects
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
size = obj.size
max_slots = self.max_slots
current_slot_usage = self.count_slots()
return current_slot_usage + size <= max_slots:
```
```{sidebar}
The `@property` decorator turns a method into a property so you don't need to 'call' it.
That is, you can access `.max_slots` instead of `.max_slots()`. In this case, it's just a
little less to type.
```
We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current
slots being in use. Let's figure out how they work.
### `.max_slots`
For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we
put this handler on. `getattr` is a Python method for retrieving a named property on an object.
The `Enum` `Ability.CON.value` is the string `Constitution` (check out the
[first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall).
So to be clear,
```python
getattr(self.obj, Ability.CON.value) + 10
```
is the same as writing
```python
getattr(your_character, "Constitution") + 10
```
which is the same as doing something like this:
```python
your_character.Constitution + 10
```
In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there
should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just
return 1.
### `.count_slots`
In this helper we use two Python tools - the `sum()` function and a
[list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former
simply adds the values of any iterable together. The latter is a more efficient way to create a list:
new_list = [item for item in some_iterable if condition]
all_above_5 = [num for num in range(10) if num > 5] # [6, 7, 8, 9]
all_below_5 = [num for num in range(10) if num < 5] # [0, 1, 2, 3, 4]
To make it easier to understand, try reading the last line above as "for every number in the range 0-9,
pick all with a value below 5 and make a list of them". You can also embed such comprehensions
directly in a function call like `sum()` without using `[]` around it.
In `count_slots` we have this code:
```python
wield_usage = sum(
getattr(slotobj, "size", 0)
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
```
We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()`
to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can
be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is.
If it is _not_ in the backpack, get its size and add it to the list. Sum over all these
sizes".
A less compact but maybe more readonable way to write this would be:
```python
backpack_item_sizes = []
for slot, slotobj in slots.items():
if slot is not WieldLocation.BACKPACK:
size = getattr(slotobj, "size", 0)
backpack_item_sizes.append(size)
wield_usage = sum(backpack_item_sizes)
```
The same is done for the items actually in the BACKPACK slot. The total sizes are added
together.
### Validating slots
With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry.
We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size
would be too much for us.
## `.add` and `.remove`
We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever
it is (even if it was in your hands).
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def add(self, obj):
"""
Put something in the backpack.
"""
self.validate_slot_usage(obj)
self.slots[WieldLocation.BACKPACK].append(obj)
self._save()
def remove(self, slot):
"""
Remove contents of a particular slot, for
example `equipment.remove(WieldLocation.SHIELD_HAND)`
"""
slots = self.slots
ret = []
if slot is WieldLocation.BACKPACK:
# empty entire backpack!
ret.extend(slots[slot])
slots[slot] = []
else:
ret.append(slots[slot])
slots[slot] = None
if ret:
self._save()
return ret
```
Both of these should be straight forward to follow. In `.add`, we make use of `validate_slot_usage` to
double-check we can actually fit the thing, then we add the item to the backpack.
In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it is and return
the item within (if any). If we gave `BACKPACK` as the slot, we empty the backpack and
return all items.
Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will
be lost after a server reload.
## Moving things around
With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment
location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method
on the `EquipmentHandler` to do this:
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def move(self, obj):
"""Move object from backpack to its intended `inventory_use_slot`."""
# make sure to remove from equipment/backpack first, to avoid double-adding
self.remove(obj)
slots = self.slots
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
to_backpack = []
if use_slot is WieldLocation.TWO_HANDS:
# two-handed weapons can't co-exist with weapon/shield-hand used items
to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
slots[use_slot] = obj
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
# can't keep a two-handed weapon if adding a one-handed weapon or shield
to_backpack = [slots[WieldLocation.TWO_HANDS]]
slots[WieldLocation.TWO_HANDS] = None
slots[use_slot] = obj
elif use_slot is WieldLocation.BACKPACK:
# it belongs in backpack, so goes back to it
to_backpack = [obj]
else:
# for others (body, head), just replace whatever's there
replaced = [obj]
slots[use_slot] = obj
for to_backpack_obj in to_backpack:
# put stuff in backpack
slots[use_slot].append(to_backpack_obj)
# store new state
self._save()
```
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where
it goes. So we just need to move the object to that slot, replacing whatever is in that place
from before. Anything we replace goes back to the backpack.
## Get everything
In order to visualize our inventory, we need some method to get everything we are carrying.
```python
# mygame/evadventure/equipment.py
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
def all(self):
"""
Get all objects in inventory, regardless of location.
"""
slots = self.slots
lst = [
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
(slots[WieldLocation.BODY], WieldLocation.BODY),
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
return lst
```
Here we get all the equipment locations and add their contents together into a list of tuples
`[(item, WieldLocation), ...]`. This is convenient for display.
## Weapon and armor
It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded
and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is
in which wield-slot and to add up armor slots manually every time you need to know.
```python
# mygame/evadventure/equipment.py
from .objects import WeaponEmptyHand
from .enums import WieldLocation, Ability
# ...
class EquipmentHandler:
# ...
@property
def armor(self):
slots = self.slots
return sum(
(
# armor is listed using its defense, so we remove 10 from it
# (11 is base no-armor value in Knave)
getattr(slots[WieldLocation.BODY], "armor", 1),
# shields and helmets are listed by their bonus to armor
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
getattr(slots[WieldLocation.HEAD], "armor", 0),
)
)
@property
def weapon(self):
# first checks two-handed wield, then one-handed; the two
# should never appear simultaneously anyhow (checked in `move` method).
slots = self.slots
weapon = slots[WieldLocation.TWO_HANDS]
if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
return weapon
```
In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head),
and grab their `armor` Attribute. We then `sum()` them all up.
In `.weapon()`, we simply check which of the possible weapon slots (weapon-hand or two-hands) have
something in them. If not we fall back to the 'fake' weapon `WeaponEmptyHand` which is just a 'dummy'
object that represents your bare hands with damage and all.
(created in [The Object tutorial](./Beginner-Tutorial-Objects.md#your-bare-hands) earlier).
## Extra credits
This covers the basic functionality of the equipment handler. There are other useful methods that
can be added:
- Given an item, figure out which equipment slot it is currently in
- Make a string representing the current loadout
- Get everything in the backpack (only)
- Get all wieldable items (weapons, shields) from backpack
- Get all usable items (items with a use-location of `BACKPACK`) from the backpack
Experiment with adding those. A full example is found in
[evennia/contrib/tutorials/evadventure/equipment.py](evennia.contrib.tutorials.evadventure.equipment).
## Unit Testing
> Create a new module `mygame/evadventure/tests/test_equipment.py`.
```{sidebar}
See [evennia/contrib/tutorials/evadventure/tests/test_equipment.py](evennia.contrib.tutorials.evadventure.tests.test_equipment)
for a finished testing example.
```
To test the `EquipmentHandler`, easiest is create an `EvAdventureCharacter` (this should by now
have `EquipmentHandler` available on itself as `.equipment`) and a few test objects; then test
passing these into the handler's methods.
```python
# mygame/evadventure/tests/test_equipment.py
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest
from ..objects import EvAdventureRoom
from ..enums import WieldLocation
class TestEquipment(BaseEvenniaTest):
def setUp(self):
self.character = create.create_object(EvAdventureCharacter, key='testchar')
self.helmet = create.create_object(EvAdventureHelmet, key="helmet")
self.weapon = create.create_object(EvAdventureWeapon, key="weapon")
def test_add_remove):
self.character.equipment.add(self.helmet)
self.assertEqual(
self.character.equipment.slots[WieldLocation.BACKPACK],
[self.helmet]
)
self.character.equipment.remove(self.helmet)
self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])
# ...
```
## Summary
_Handlers_ are useful for grouping functionality together. Now that we spent our time making the
`EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all
the details for us. As long as we call its methods, the details can be forgotten about.
We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia.
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character
generation - where players get to make their own character!

View file

@ -0,0 +1,5 @@
# Non-Player-Characters (NPCs)
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,360 @@
# In-game Objects and items
In the previous lesson we established what a 'Character' is in our game. Before we continue
we also need to have a notion what an 'item' or 'object' is.
Looking at _Knave_'s item lists, we can get some ideas of what we need to track:
- `size` - this is how many 'slots' the item uses in the character's inventory.
- `value` - a base value if we want to sell or buy the item.
- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be
worn on the head and a shield in the shield hand. Some items can't be used this way at all, but
only belong in the backpack.
- `obj_type` - Which 'type' of item this is.
## New Enums
We added a few enumberations for Abilities back in the [Utilities tutorial](./Beginner-Tutorial-Utilities.md).
Before we continue, let's expand with enums for use-slots and object types.
```python
# mygame/evadventure/enums.py
# ...
class WieldLocation(Enum):
BACKPACK = "backpack"
WEAPON_HAND = "weapon_hand"
SHIELD_HAND = "shield_hand"
TWO_HANDS = "two_handed_weapons"
BODY = "body" # armor
HEAD = "head" # helmets
class ObjType(Enum):
WEAPON = "weapon"
ARMOR = "armor"
SHIELD = "shield"
HELMET = "helmet"
CONSUMABLE = "consumable"
GEAR = "gear"
MAGIC = "magic"
QUEST = "quest"
TREASURE = "treasure"
```
Once we have these enums, we will use them for referencing things.
## The base object
> Create a new module `mygame/evadventure/objects.py`
```{sidebar}
[evennia/contrib/tutorials/evadventure/objects.py](evennia.contrib.tutorials.evadventure.objects) has
a full set of objects implemented.
```
<div style="clear: right;"></div>
We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add
child classes to represent the relevant types:
```python
# mygame/evadventure/objects.py
from evennia import AttributeProperty, DefaultObject
from evennia.utils.utils import make_iter
from .utils import get_obj_stats
from .enums import WieldLocation, ObjType
class EvAdventureObject(DefaultObject):
"""
Base for all evadventure objects.
"""
inventory_use_slot = WieldLocation.BACKPACK
size = AttributeProperty(1, autocreate=False)
value = AttributeProperty(0, autocreate=False)
# this can be either a single type or a list of types (for objects able to be
# act as multiple). This is used to tag this object during creation.
obj_type = ObjType.GEAR
def at_object_creation(self):
"""Called when this object is first created. We convert the .obj_type
property to a database tag."""
for obj_type in make_iter(self.obj_type):
self.tags.add(self.obj_type.value, category="obj_type")
def get_help(self):
"""Get any help text for this item"""
return "No help for this item"
```
### Using Attributes or not
In theory, `size` and `value` does not change and _could_ also be just set as a regular Python
property on the class:
```python
class EvAdventureObject(DefaultObject):
inventory_use_slot = WieldLocation.BACKPACK
size = 1
value = 0
```
The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to
make a new class for it. We can't change it on the fly because the change would only be in memory and
be lost on next server reload.
Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we
create the object (or later), and the Attributes will remember our changes to that object indefinitely.
To make this a little more efficient, we use `autocreate=False`. Normally when you create a
new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at
the same time. So normally, the object would be created along with two Attributes `size` and `value`.
With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as
long as your object has `size=1` no database `Attribute` will be created at all. This saves time and
resources when creating large number of objects.
The drawback is that since no Attribute is created you can't refer to it
with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query
the database for all objects with `size=1`, since most objects would not yet have an in-database
`size` Attribute to search for.
In our case, we'll only refer to these properties as `obj.size` etc, and have no need to find
all objects of a particular size. So we should be safe.
### Creating tags in `at_object_creation`
The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is
first created.
We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the
object like this means you can later efficiently find all objects of a given type (or combination of
types) with Evennia's search functions:
```python
from .enums import ObjType
from evennia.utils import search
# get all shields in the game
all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type")
```
We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the
evennia utility library to make sure we don't balk at either. This means you could have a Shield that
is also Magical, for example.
## Other object types
Some of the other object types are very simple so far.
```python
# mygame/evadventure/objects.py
from evennia import AttributeProperty, DefaultObject
from .enums import ObjType
class EvAdventureObject(DefaultObject):
# ...
class EvAdventureQuestObject(EvAdventureObject):
"""Quest objects should usually not be possible to sell or trade."""
obj_type = ObjType.QUEST
class EvAdventureTreasure(EvAdventureObject):
"""Treasure is usually just for selling for coin"""
obj_type = ObjType.TREASURE
value = AttributeProperty(100, autocreate=False)
```
## Consumables
A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used
anymore. An example would be a health potion.
```python
# mygame/evadventure/objects.py
# ...
class EvAdventureConsumable(EvAdventureObject):
"""An item that can be used up"""
obj_type = ObjType.CONSUMABLE
value = AttributeProperty(0.25, autocreate=False)
uses = AttributeProperty(1, autocreate=False)
def at_pre_use(self, user, *args, **kwargs):
"""Called before using. If returning False, abort use."""
return uses > 0
def at_use(self, user, *args, **kwargs):
"""Called when using the item"""
pass
def at_post_use(self. user, *args, **kwargs):
"""Called after using the item"""
# detract a usage, deleting the item if used up.
self.uses -= 1
if self.uses <= 0:
user.msg(f"{self.key} was used up.")
self.delete()
```
What exactly each consumable does will vary - we will need to implement children of this class
later, overriding `at_use` with different effects.
## Weapons
All weapons need properties that describe how efficient they are in battle.
```python
# mygame/evadventure/objects.py
from .enums import WieldLocation, ObjType, Ability
# ...
class EvAdventureWeapon(EvAdventureObject):
"""Base class for all weapons"""
obj_type = ObjType.WEAPON
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
quality = AttributeProperty(3, autocreate=False)
attack_type = AttibuteProperty(Ability.STR, autocreate=False)
defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False)
damage_roll = AttibuteProperty("1d6", autocreate=False)
```
The `quality` is something we need to track in _Knave_. When getting critical failures on attacks,
a weapon's quality will go down. When it reaches 0, it will break.
The attack/defend type tracks how we resolve attacks with the weapon, like `roll + STR vs ARMOR + 10`.
## Magic
In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both
hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon'
that is also a 'consumable' of sorts.
```python
# mygame/evadventure/objects.py
# ...
class EvAdventureConsumable(EvAdventureObject):
# ...
class EvAdventureWeapon(EvAdventureObject):
# ...
class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
"""Base for all magical rune stones"""
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
quality = AttributeProperty(3, autocreate=False)
attack_type = AttibuteProperty(Ability.INT, autocreate=False)
defend_type = AttibuteProperty(Ability.DEX, autocreate=False)
damage_roll = AttibuteProperty("1d8", autocreate=False)
def at_post_use(self, user, *args, **kwargs):
"""Called after usage/spell was cast"""
self.uses -= 1
# we don't delete the rune stone here, but
# it must be reset on next rest.
def refresh(self):
"""Refresh the rune stone (normally after rest)"""
self.uses = 1
```
We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses`
again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `at_use` methods
are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted
when it runs out of uses.
We add a little convenience method `refresh` - we should call this when the character rests, to
make the runestone active again.
Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this
base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot
of custom code.
## Armor
Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the
defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know,
defending is always `bonus + 10`, so the result will be the same - this means
we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case.
``
```python
# mygame/evadventure/objects.py
# ...
class EvAdventureAmor(EvAdventureObject):
obj_type = ObjType.ARMOR
inventory_use_slot = WieldLocation.BODY
armor = AttributeProperty(1, autocreate=False)
quality = AttributeProperty(3, autocreate=False)
class EvAdventureShield(EvAdventureArmor):
obj_type = ObjType.SHIELD
inventory_use_slot = WieldLocation.SHIELD_HAND
class EvAdventureHelmet(EvAdventureArmor):
obj_type = ObjType.HELMET
inventory_use_slot = WieldLocation.HEAD
```
## Your Bare hands
This is a 'dummy' object that is not stored in the database. We will use this in the upcoming
[Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing'
in your hands. This way we don't need to add any special case for this.
```python
class WeaponEmptyHand:
obj_type = ObjType.WEAPON
key = "Empty Fists"
inventory_use_slot = WieldLocation.WEAPON_HAND
attack_type = Ability.STR
defense_type = Ability.ARMOR
damage_roll = "1d4"
quality = 100000 # let's assume fists are always available ...
def __repr__(self):
return "<WeaponEmptyHand>"
```
## Testing and Extra credits
Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier?
We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game.
Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data
from the object it receives.
When you change this function you must also update the related unit test - so your existing test becomes a
nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types
to `get_obj_stats`.
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).

View file

@ -1,5 +1,11 @@
# Part 3: How we get there
```{warning}
The tutorial game is under development and is not yet complete, nor tested. Use the existing
lessons as inspiration and to help get you going, but don't expect out-of-the-box perfection
from it at this time.
```
```{eval-rst}
.. sidebar:: Beginner Tutorial Parts
@ -17,47 +23,59 @@
Taking our new game online and let players try it out
```
In part three of the Evennia Beginner tutorial we will go through the creation of several key parts of our tutorial
game _EvAdventure_. This is a pretty big part with plenty of examples.
In part three of the Evennia Beginner tutorial we will go through the actual creation of
our tutorial game _EvAdventure_, based on the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
RPG ruleset.
If you followed the previous parts of this tutorial you will have some notions about Python and where to find
and make use of things in Evennia. We also have a good idea of the type of game we want.
Even if this is not the game-style you are interested in, following along will give you a lot of experience
with using Evennia. This be of much use when doing your own thing later.
This is a big part. You'll be seeing a lot of code and there are plenty of lessons to go through.
Take your time!
If you followed the previous parts of this tutorial you will have some notions about Python and where to
find and make use of things in Evennia. We also have a good idea of the type of game we will
create.
Even if this is not the game-style you are interested in, following along will give you a lot
of experience using Evennia and be really helpful for doing your own thing later!
Fully coded examples of all code we make in this part can be found in the
[evennia/contrib/tutorials/evadventure](evennia.contrib.tutorials.evadventure) package.
## Lessons
_TODO_
```{toctree}
:maxdepth: 1
Implementing-a-game-rule-system
Turn-based-Combat-System
A-Sittable-Object
Beginner-Tutorial-Utilities
Beginner-Tutorial-Rules
Beginner-Tutorial-Characters
Beginner-Tutorial-Objects
Beginner-Tutorial-Equipment
Beginner-Tutorial-Chargen
Beginner-Tutorial-Rooms
Beginner-Tutorial-NPCs
Beginner-Tutorial-Turnbased-Combat
Beginner-Tutorial-Quests
Beginner-Tutorial-Shops
Beginner-Tutorial-Dungeon
Beginner-Tutorial-Commands
```
1. [Changing settings](../../../Unimplemented.md)
1. [Applying contribs](../../../Unimplemented.md)
1. [Creating a rule module](../../../Unimplemented.md)
1. [Tweaking the base Typeclasses](../../../Unimplemented.md)
1. [Character creation menu](../../../Unimplemented.md)
1. [Wearing armor and wielding weapons](../../../Unimplemented.md)
1. [Two types of combat](../../../Unimplemented.md)
1. [Monsters and AI](../../../Unimplemented.md)
1. [Questing and rewards](../../../Unimplemented.md)
1. [Overview of Tech demo](../../../Unimplemented.md)
## Table of Contents
_TODO_
```{toctree}
:maxdepth: 1
Implementing-a-game-rule-system
Turn-Based-Combat-System
A-Sittable-Object
Beginner-Tutorial-Utilities
Beginner-Tutorial-Rules
Beginner-Tutorial-Characters
Beginner-Tutorial-Objects
Beginner-Tutorial-Equipment
Beginner-Tutorial-Chargen
Beginner-Tutorial-Rooms
Beginner-Tutorial-NPCs
Beginner-Tutorial-Turnbased-Combat
Beginner-Tutorial-Quests
Beginner-Tutorial-Shops
Beginner-Tutorial-Dungeon
Beginner-Tutorial-Commands
```

View file

@ -0,0 +1,5 @@
# Game Quests
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,5 @@
# In-game Rooms
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,633 @@
# Rules and dice rolling
In _EvAdventure_ we have decided to use the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
RPG ruleset. This is commercial, but released under Creative Commons 4.0, meaning it's okay to share and
adapt _Knave_ for any purpose, even commercially. If you don't want to buy it but still follow
along, you can find a [free fan-version here](http://abominablefancy.blogspot.com/2018/10/knaves-fancypants.html).
## Summary of _Knave_ rules
Knave, being inspired by early Dungeons & Dragons, is very simple.
- It uses six Ability bonuses
_Strength_ (STR), _Dexterity_ (DEX), _Constitution_ (CON), _Intelligence_ (INT), _Wisdom_ (WIS)
and _Charisma_ (CHA). These are rated from `+1` to `+10`.
- Rolls are made with a twenty-sided die (`1d20`), usually adding a suitable Ability bonus to the roll.
- If you roll _with advantage_, you roll `2d20` and pick the
_highest_ value, If you roll _with disadvantage_, you roll `2d20` and pick the _lowest_.
- Rolling a natural `1` is a _critical failure_. A natural `20` is a _critical success_. Rolling such
in combat means your weapon or armor loses quality, which will eventually destroy it.
- A _saving throw_ (trying to succeed against the environment) means making a roll to beat `15` (always).
So if you are lifting a heavy stone and have `STR +2`, you'd roll `1d20 + 2` and hope the result
is higher than `15`.
- An _opposed saving throw_ means beating the enemy's suitable Ability 'defense', which is always their
`Ability bonus + 10`. So if you have `STR +1` and are arm wrestling someone with `STR +2`, you roll
`1d20 + 1` and hope to roll higher than `2 + 10 = 12`.
- A special bonus is `Armor`, `+1` is unarmored, additional armor is given by equipment. Melee attacks
test `STR` versus the `Armor` defense value while ranged attacks uses `WIS` vs `Armor`.
- _Knave_ has no skills or classes. Everyone can use all items and using magic means having a special
'rune stone' in your hands; one spell per stone and day.
- A character has `CON + 10` carry 'slots'. Most normal items uses one slot, armor and large weapons uses
two or three.
- Healing is random, `1d8 + CON` health healed after food and sleep.
- Monster difficulty is listed by hy many 1d8 HP they have; this is called their "hit die" or HD. If
needing to test Abilities, monsters have HD bonus in every Ability.
- Monsters have a _morale rating_. When things go bad, they have a chance to panic and flee if
rolling `2d6` over their morale rating.
- All Characters in _Knave_ are mostly randomly generated. HP is `<level>d8` but we give every
new character max HP to start.
- _Knave_ also have random tables, such as for starting equipment and to see if dying when
hitting 0. Death, if it happens, is permanent.
## Making a rule module
> Create a new module mygame/evadventure/rules.py
```{sidebar}
A complete version of the rule module is found in
[evennia/contrib/tutorials/evadventure/rules.py](evennia.contrib.tutorials.evadventure.rules).
```
There are three broad sets of rules for most RPGS:
- Character generation rules, often only used during character creation
- Regular gameplay rules - rolling dice and resolving game situations
- Character improvement - getting and spending experience to improve the character
We want our `rules` module to cover as many aspeects of what we'd otherwise would have to look up
in a rulebook.
## Rolling dice
We will start by making a dice roller. Let's group all of our dice rolling into a structure like this
(not functional code yet):
```python
class EvAdventureRollEngine:
def roll(...):
# get result of one generic roll, for any type and number of dice
def roll_with_advantage_or_disadvantage(...)
# get result of normal d20 roll, with advantage/disadvantage (or not)
def saving_throw(...):
# do a saving throw against a specific target number
def opposed_saving_throw(...):
# do an opposed saving throw against a target's defense
def roll_random_table(...):
# make a roll against a random table (loaded elsewere)
def morale_check(...):
# roll a 2d6 morale check for a target
def heal_from_rest(...):
# heal 1d8 when resting+eating, but not more than max value.
def roll_death(...):
# roll to determine penalty when hitting 0 HP.
dice = EvAdventureRollEngine()
```
```{sidebar}
This groups all dice-related code into one 'container' that is easy to import. But it's mostly a matter
of taste. You _could_ also break up the class' methods into normal functions at the top-level of the
module if you wanted.
```
This structure (called a _singleton_) means we group all dice rolls into one class that we then initiate
into a variable `dice` at the end of the module. This means that we can do the following from other
modules:
```python
from .rules import dice
dice.roll("1d8")
```
### Generic dice roller
We want to be able to do `roll("1d20")` and get a random result back from the roll.
```python
# in mygame/evadventure/rules.py
from random import randint
class EvAdventureRollEngine:
def roll(self, roll_string):
"""
Roll XdY dice, where X is the number of dice
and Y the number of sides per die.
Args:
roll_string (str): A dice string on the form XdY.
Returns:
int: The result of the roll.
"""
# split the XdY input on the 'd' one time
number, diesize = roll_string.split("d", 1)
# convert from string to integers
number = int(number)
diesize = int(diesize)
# make the roll
return sum(randint(1, diesize) for _ in range(number))
```
```{sidebar}
For this tutorial we have opted to not use any contribs, so we create
our own dice roller. But normally you could instead use the [dice](../../../Contribs/Contrib-Dice.md) contrib for this.
We'll point out possible helpful contribs in sidebars as we proceed.
```
The `randint` standard Python library module produces a random integer
in a specific range. The line
```python
sum(randint(1, diesize) for _ in range(number))
```
works like this:
- For a certain `number` of times ...
- ... create a random integer between `1` and `diesize` ...
- ... and `sum` all those integers together.
You could write the same thing less compactly like this:
```python
rolls = []
for _ in range(number):
random_result = randint(1, diesize)
rolls.append(random_result)
return sum(rolls)
```
```{sidebar}
Note that `range` generates a value `0...number-1`. We use `_` in the `for` loop to
indicate we don't really care what this value is - we just want to repeat the loop
a certain amount of times.
```
We don't ever expect end users to call this method; if we did, we would have to validate the inputs
much more - We would have to make sure that `number` or `diesize` are valid inputs and not
crazy big so the loop takes forever!
### Rolling with advantage
Now that we have the generic roller, we can start using it to do a more complex roll.
```
# in mygame/evadventure/rules.py
# ...
class EvAdventureRollEngine:
def roll(roll_string):
# ...
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
if not (advantage or disadvantage) or (advantage and disadvantage):
# normal roll - advantage/disadvantage not set or they cancel
# each other out
return self.roll("1d20")
elif advantage:
# highest of two d20 rolls
return max(self.roll("1d20"), self.roll("1d20"))
else:
# disadvantage - lowest of two d20 rolls
return min(self.roll("1d20"), self.roll("1d20"))
```
The `min()` and `max()` functions are standard Python fare for getting the biggest/smallest
of two arguments.
### Saving throws
We want the saving throw to itself figure out if it succeeded or not. This means it needs to know
the Ability bonus (like STR `+1`). It would be convenient if we could just pass the entity
doing the saving throw to this method, tell it what type of save was needed, and then
have it figure things out:
```python
result, quality = dice.saving_throw(character, Ability.STR)
```
The return will be a boolean `True/False` if they pass, as well as a `quality` that tells us if
a perfect fail/success was rolled or not.
To make the saving throw method this clever, we need to think some more about how we want to store our
data on the character.
For our purposes it sounds reasonable that we will be using [Attributes](../../../Components/Attributes.md) for storing
the Ability scores. To make it easy, we will name them the same as the
[Enum values](./Beginner-Tutorial-Utilities.md#enums) we set up in the previous lesson. So if we have
an enum `STR = "strength"`, we want to store the Ability on the character as an Attribute `strength`.
From the Attribute documentation, we can see that we can use `AttributeProperty` to make it so the
Attribute is available as `character.strength`, and this is what we will do.
So, in short, we'll create the saving throws method with the assumption that we will be able to do
`character.strength`, `character.constitution`, `character.charisma` etc to get the relevant Abilities.
```python
# in mygame/evadventure/rules.py
# ...
from .enums import Ability
class EvAdventureRollEngine:
def roll(...)
# ...
def roll_with_advantage_or_disadvantage(...)
# ...
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
advantage=False, disadvantage=False):
"""
Do a saving throw, trying to beat a target.
Args:
character (Character): A character (assumed to have Ability bonuses
stored on itself as Attributes).
bonus_type (Ability): A valid Ability bonus enum.
target (int): The target number to beat. Always 15 in Knave.
advantage (bool): If character has advantage on this roll.
disadvantage (bool): If character has disadvantage on this roll.
Returns:
tuple: A tuple (bool, Ability), showing if the throw succeeded and
the quality is one of None or Ability.CRITICAL_FAILURE/SUCCESS
"""
# make a roll
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
# figure out if we had critical failure/success
quality = None
if dice_roll == 1:
quality = Ability.CRITICAL_FAILURE
elif dice_roll == 20:
quality = Ability.CRITICAL_SUCCESS
# figure out bonus
bonus = getattr(character, bonus_type.value, 1)
# return a tuple (bool, quality)
return (dice_roll + bonus) > target, quality
```
The `getattr(obj, attrname, default)` function is a very useful Python tool for getting an attribute
off an object and getting a default value if the attribute is not defined.
### Opposed saving throw
With the building pieces we already created, this method is simple. Remember that the defense you have
to beat is always the relevant bonus + 10 in _Knave_. So if the enemy defends with `STR +3`, you must
roll higher than `13`.
```python
# in mygame/evadventure/rules.py
from .enums import Ability
class EvAdventureRollEngine:
def roll(...):
# ...
def roll_with_advantage_or_disadvantage(...):
# ...
def saving_throw(...):
# ...
def opposed_saving_throw(self, attacker, defender,
attack_type=Ability.STR, defense_type=Ability.ARMOR,
advantage=False, disadvantage=False):
defender_defense = getattr(defender, defense_type.value, 1) + 10
result, quality = self.saving_throw(attacker, bonus_type=attack_type,
target=defender_defense,
advantage=advantave, disadvantage=disadvantage)
return result, quality
```
### Morale check
We will make the assumption that the `morale` value is available from the creature simply as
`monster.morale` - we need to remember to make this so later!
In _Knave_, a creature have roll with `2d6` equal or under its morale to not flee or surrender
when things go south. The standard morale value is 9.
```python
# in mygame/evadventure/rules.py
class EvAdventureRollEngine:
# ...
def morale_check(self, defender):
return self.roll("2d6") <= getattr(defender, "morale", 9)
```
### Roll for Healing
To be able to handle healing, we need to make some more assumptions about how we store
health on game entities. We will need `hp_max` (the total amount of available HP) and `hp`
(the current health value). We again assume these will be available as `obj.hp` and `obj.hp_max`.
According to the rules, after consuming a ration and having a full night's sleep, a character regains
`1d8 + CON` HP.
```python
# in mygame/evadventure/rules.py
from .enums import Ability
class EvAdventureRollEngine:
# ...
def heal_from_rest(self, character):
"""
A night's rest retains 1d8 + CON HP
"""
con_bonus = getattr(character, Ability.CON.value, 1)
character.heal(self.roll("1d8") + con_bonus)
```
We make another assumption here - that `character.heal()` is a thing. We tell this function how
much the character should heal, and it will do so, making sure to not heal more than its max
number of HPs
> Knowing what is available on the character and what rule rolls we need is a bit of a chicken-and-egg
> problem. We will make sure to implement the matching _Character_ class next lesson.
### Rolling on a table
We occasionally need to roll on a 'table' - a selection of choices. There are two main table-types
we need to support:
Simply one element per row of the table (same odds to get each result).
| Result |
|:------:|
| item1 |
| item2 |
| item3 |
| item4 |
This we will simply represent as a plain list
```python
["item1", "item2", "item3", "item4"]
```
Ranges per item (varying odds per result):
| Range | Result |
|:-----:|:------:|
| 1-5 | item1 |
| 6-15 | item2 |
| 16-19 | item3 |
| 20 | item4 |
This we will represent as a list of tuples:
```python
[("1-5", "item1"), ("6-15", "item2"), ("16-19", "item4"), ("20", "item5")]
```
We also need to know what die to roll to get a result on the table (it may not always
be obvious, and in some games you could be asked to roll a lower dice to only get
early table results, for example).
```python
# in mygame/evadventure/rules.py
from random import randint, choice
class EvAdventureRollEngine:
# ...
def roll_random_table(self, dieroll, table_choices):
"""
Args:
dieroll (str): A die roll string, like "1d20".
table_choices (iterable): A list of either single elements or
of tuples.
Returns:
Any: A random result from the given list of choices.
Raises:
RuntimeError: If rolling dice giving results outside the table.
"""
roll_result = self.roll(dieroll)
if isinstance(table_choices[0], (tuple, list)):
# the first element is a tuple/list; treat as on the form [("1-5", "item"),...]
for (valrange, choice) in table_choices:
minval, *maxval = valrange.split("-", 1)
minval = abs(int(minval))
maxval = abs(int(maxval[0]) if maxval else minval)
if minval <= roll_result <= maxval:
return choice
# if we get here we must have set a dieroll producing a value
# outside of the table boundaries - raise error
raise RuntimeError("roll_random_table: Invalid die roll")
else:
# a simple regular list
roll_result = max(1, min(len(table_choices), roll_result))
return table_choices[roll_result - 1]
```
Check that you understand what this does.
This may be confusing:
```python
minval, *maxval = valrange.split("-", 1)
minval = abs(int(minval))
maxval = abs(int(maxval[0]) if maxval else minval)
```
If `valrange` is the string `1-5`, then `valrange.split("-", 1)` would result in a tuple `("1", "5")`.
But if the string was in fact just `"20"` (possible for a single entry in an RPG table), this would
lead to an error since it would only split out a single element - and we expected two.
By using `*maxval` (with the `*`), `maxval` is told to expect _0 or more_ elements in a tuple.
So the result for `1-5` will be `("1", ("5",))` and for `20` it will become `("20", ())`. In the line
```python
maxval = abs(int(maxval[0]) if maxval else minval)
```
we check if `maxval` actually has a value `("5",)` or if its empty `()`. The result is either
`"5"` or the value of `minval`.
### Roll for death
While original Knave suggests hitting 0 HP means insta-death, we will grab the optional "death table"
from the "prettified" Knave's optional rules to make it a little less punishing. We also changed the
result of `2` to 'dead' since we don't simulate 'dismemberment' in this tutorial:
| Roll | Result | -1d4 Loss of Ability |
|:---: |:--------:|:--------------------:|
| 1-2 | dead | -
| 3 | weakened | STR |
|4 | unsteady | DEX |
| 5 | sickly | CON |
| 6 | addled | INT |
| 7 | rattled | WIS |
| 8 | disfigured | CHA |
All the non-dead values map to a loss of 1d4 in one of the six Abilities (but you get HP back).
We need to map back to this from the above table. One also cannot have less than -10 Ability bonus,
if you do, you die too.
```python
# in mygame/evadventure/rules.py
death_table = (
("1-2", "dead"),
("3": "strength",
("4": "dexterity"),
("5": "constitution"),
("6": "intelligence"),
("7": "wisdom"),
("8": "charisma"),
)
class EvAdventureRollEngine:
# ...
def roll_random_table(...)
# ...
def roll_death(self, character):
ability_name = self.roll_random_table("1d8", death_table)
if ability_name == "dead":
# TODO - kill the character!
pass
else:
loss = self.roll("1d4")
current_ability = getattr(character, ability_name)
current_ability -= loss
if current_ability < -10:
# TODO - kill the character!
pass
else:
# refresh 1d4 health, but suffer 1d4 ability loss
self.heal(character, self.roll("1d4")
setattr(character, ability_name, current_ability)
character.msg(
"You survive your brush with death, and while you recover "
f"some health, you permanently lose {loss} {ability_name} instead."
)
dice = EvAdventureRollEngine()
```
Here we roll on the 'death table' from the rules to see what happens. We give the character
a message if they survive, to let them know what happened.
We don't yet know what 'killing the character' technically means, so we mark this as `TODO` and
return to it in a later lesson. We just know that we need to do _something_ here to kill off the
character!
## Testing
> Make a new module `mygame/evadventure/tests/test_rules.py`
Testing the `rules` module will also showcase some very useful tools when testing.
```python
# mygame/evadventure/tests/test_rules.py
from unittest.mock import patch
from evennia.utils.test_resources import BaseEvenniaTest
from .. import rules
class TestEvAdventureRuleEngine(BaseEvenniaTest):
def setUp(self):
"""Called before every test method"""
super().setUp()
self.roll_engine = rules.EvAdventureRollEngine()
@patch("evadventure.rules.randint")
def test_roll(self, mock_randint):
mock_randint.return_value = 4
self.assertEqual(self.roll_engine.roll("1d6", 4)
self.assertEqual(self.roll_engine.roll("2d6", 2 * 4)
# test of the other rule methods below ...
```
As before, run the specific test with
evennia test --settings settings.py .evadventure.tests.test_rules
### Mocking and patching
```{sidebar}
In [evennia/contrib/tutorials/evadventure/tests/test_rules.py](evennia.contrib.tutorials.evadventure.tests.test_rules)
has a complete example of rule testing.
```
The `setUp` method is a special method of the testing class. It will be run before every
test method. We use `super().setUp()` to make sure the parent class' version of this method
always fire. Then we create a fresh `EvAdventureRollEngine` we can test with.
In our test, we import `patch` from the `unittest.mock` library. This is a very useful tool for testing.
Normally the `randint` function we imported in `rules` will return a random value. That's very hard to
test for, since the value will be different every test.
With `@patch` (this is called a _decorator_), we temporarily replace `rules.randint` with a 'mock' - a
dummy entity. This mock is passed into the testing method. We then take this `mock_randint` and set
`.return_value = 4` on it.
Adding `return_value` to the mock means that every time this mock is called, it will return 4. For the
duration of the test we can now check with `self.assertEqual` that our `roll` method always returns a
result as-if the random result was 4.
There are [many resources for understanding mock](https://realpython.com/python-mock-library/), refer to
them for further help.
> The `EvAdventureRollEngine` have many methods to test. We leave this as an extra exercise!
## Summary
This concludes all the core rule mechanics of _Knave_ - the rules used during play. We noticed here
that we are going to soon need to establish how our _Character_ actually stores data. So we will
address that next.

View file

@ -0,0 +1,5 @@
# In-game Shops
```{warning}
This part of the Beginner tutorial is still being developed.
```

View file

@ -0,0 +1,322 @@
# Code structure and Utilities
In this lesson we will set up the file structure for _EvAdventure_. We will make some
utilities that will be useful later. We will also learn how to write _tests_.
## Folder structure
Create a new folder under your `mygame` folder, named `evadventure`. Inside it, create
another folder `tests/` and make sure to put empty `__init__.py` files in both. This turns both
folders into packages Python understands to import from.
```
mygame/
commands/
evadventure/ <---
__init__.py <---
tests/ <---
__init__.py <---
__init__.py
README.md
server/
typeclasses/
web/
world/
```
Importing anything from inside this folder from anywhere else under `mygame` will be done by
```python
# from anywhere in mygame/
from evadventure.yourmodulename import whatever
```
This is the 'absolute path` type of import.
Between two modules both in `evadventure/`, you can use a 'relative' import with `.`:
```python
# from a module inside mygame/evadventure
from .yourmodulename import whatever
```
From e.g. inside `mygame/evadventure/tests/` you can import from one level above using `..`:
```python
# from mygame/evadventure/tests/
from ..yourmodulename import whatever
```
## Enums
```{sidebar}
A full example of the enum module is found in
[evennia/contrib/tutorials/evadventure/enums.py](evennia.contrib.tutorials.evadventure.enums).
```
Create a new file `mygame/evadventure/enums.py`.
An [enum](https://docs.python.org/3/library/enum.html) (enumeration) is a way to establish constants
in Python. Best is to show an example:
```python
# in a file mygame/evadventure/enums.py
from enum import Enum
class Ability(Enum):
STR = "strength"
```
You access an enum like this:
```
# from another module in mygame/evadventure
from .enums import Ability
Ability.STR # the enum itself
Ability.STR.value # this is the string "strength"
```
Having enums is recommended practice. With them set up, it means we can make sure to refer to the
same thing every time. Having all enums in one place also means you have a good overview of the
constants you are dealing with.
The alternative would be to for example pass around a string `"constitution"`. If you mis-spell
this (`"consitution"`), you would not necessarily know it right away - the error would happen later
when the string is not recognized. If you make a typo getting `Ability.COM` instead of `Ability.CON`,
Python will immediately raise an error since this enum is not recognized.
With enums you can also do nice direct comparisons like `if ability is Ability.WIS: <do stuff>`.
Note that the `Ability.STR` enum does not have the actual _value_ of e.g. your Strength.
It's just a fixed label for the Strength ability.
Here is the `enum.py` module needed for _Knave_. It covers the basic aspects of
rule systems we need to track (check out the _Knave_ rules. If you use another rule system you'll
likely gradually expand on your enums as you figure out what you'll need).
```python
# mygame/evadventure/enums.py
class Ability(Enum):
"""
The six base ability-bonuses and other
abilities
"""
STR = "strength"
DEX = "dexterity"
CON = "constitution"
INT = "intelligence"
WIS = "wisdom"
CHA = "charisma"
ARMOR = "armor"
CRITICAL_FAILURE = "critical_failure"
CRITICAL_SUCCESS = "critical_success"
ALLEGIANCE_HOSTILE = "hostile"
ALLEGIANCE_NEUTRAL = "neutral"
ALLEGIANCE_FRIENDLY = "friendly"
```
Here the `Ability` class holds basic properties of a character sheet.
## Utility module
> Create a new module `mygame/evadventure/utils.py`
```{sidebar}
An example of the utility module is found in
[evennia/contrib/tutorials/evadventure/utils.py](evennia.contrib.tutorials.evadventure.utils)
```
This is for general functions we may need from all over. In this case we only picture one utility,
a function that produces a pretty display of any object we pass to it.
This is an example of the string we want to see:
```
Chipped Sword
Value: ~10 coins [wielded in Weapon hand]
A simple sword used by mercenaries all over
the world.
Slots: 1, Used from: weapon hand
Quality: 3, Uses: None
Attacks using strength against armor.
Damage roll: 1d6
```
Here's the start of how the function could look:
```python
# in mygame/evadventure/utils.py
_OBJ_STATS = """
|c{key}|n
Value: ~|y{value}|n coins{carried}
{desc}
Slots: |w{size}|n, Used from: |w{use_slot_name}|n
Quality: |w{quality}|n, Uses: |wuses|n
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
Damage roll: |w{damage_roll}|n
""".strip()
def get_obj_stats(obj, owner=None):
"""
Get a string of stats about the object.
Args:
obj (Object): The object to get stats for.
owner (Object): The one currently owning/carrying `obj`, if any. Can be
used to show e.g. where they are wielding it.
Returns:
str: A nice info string to display about the object.
"""
return _OBJ_STATS.format(
key=obj.key,
value=10,
carried="[Not carried]",
desc=obj.db.desc,
size=1,
quality=3,
uses="infinite"
use_slot_name="backpack",
attack_type_name="strength"
defense_type_name="armor"
damage_roll="1d6"
)
```
Here we set up the string template with place holders for where every piece of info should go.
Study this string so you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are
[Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color respectively.
We can guess some things, such that `obj.key` is the name of the object, and that `obj.db.desc` will
hold its description (this is how it is in default Evennia).
But so far we have not established how to get any of the other properties like `size` or `attack_type`.
So we just set them to dummy values. We'll need to get back to this when we have more code in place!
## Testing
```{important}
It's useful for any game dev to know how to effectively test their code. So we'll try to include a
*Testing* section at the end of each of the implementation lessons to follow. Writing tests for your code
is optional but highly recommended; it can feel a little cumbersome at first, but you'll thank yourself later.
```
> create a new module `mygame/evadventure/tests/test_utils.py`
How do you know if you made a typo in the code above? You could _manually_ test it by reloading your
Evennia server and do the following from in-game:
py from evadventure.utils import get_obj_stats;print(get_obj_stats(self))
You should get back a nice string about yourself! If that works, great! But you'll need to remember
doing that test when you change this code later.
```{sidebar}
In [evennia/contrib/tutorials/evadventure/tests/test_utils.py](evennia.contrib.tutorials.
evadventure.tests.test_utils)
is an example of the testing module. To dive deeper into unit testing in Evennia, see the
[Unit testing](../../../Coding/Unit-Testing.md) documentation.
```
A _unit test_ allows you to set up automated testing of code. Once you've written your test you
can run it over and over and make sure later changes to your code didn't break things.
In this particular case, we _expect_ to later have to update the test when `get_obj_stats` becomes more
complete and returns more reasonable data.
Evennia comes with extensive functionality to help you test your code. Here's a module for
testing `get_obj_stats`.
```python
# mygame/evadventure/tests/test_utils.py
from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest
from ..import utils
class TestUtils(BaseEvenniaTest):
def test_get_obj_stats(self):
# make a simple object to test with
obj = create.create_object(
key="testobj",
attributes=(("desc", "A test object"),)
)
# run it through the function
result = utils.get_obj_stats(obj)
# check that the result is what we expected
self.assertEqual(
result,
"""
|ctestobj|n
Value: ~|y10|n coins
A test object
Slots: |w1|n, Used from: |wbackpack|n
Quality: |w3|n, Uses: |winfinite|n
Attacks using |wstrength|n against |warmor|n
Damage roll: |w1d6|n
""".strip()
)
```
What happens here is that we create a new test-class `TestUtils` that inherits from `BaseEvenniaTest`.
This inheritance is what makes this a testing class.
We can have any number of methods on this class. To have a method recognized as one containing
code to test, its name _must_ start with `test_`. We have one - `test_get_obj_stats`.
In this method we create a dummy `obj` and gives it a `key` "testobj". Note how we add the
`desc` [Attribute](../../../Components/Attributes.md) directly in the `create_object` call by specifying the attribute as a
tuple `(name, value)`!
We then get the result of passing this dummy-object through `get_obj_stats` we imported earlier.
The `assertEqual` method is available on all testing classes and checks that the `result` is equal
to the string we specify. If they are the same, the test _passes_, otherwise it _fails_ and we
need to investigate what went wrong.
### Running your test
To run your test you need to stand inside your `mygame` folder and execute the following command:
evennia test --settings settings.py .evadventure.tests
This will run all your `evadventure` tests (if you had more of them). To only run your utility tests
you could do
evennia test --settings settings.py .evadventure.tests.test_utils
If all goes well, you should get an `OK` back. Otherwise you need to check the failure, maybe
your return string doesn't quite match what you expected.
## Summary
It's very important to understand how you import code between modules in Python, so if this is still
confusing to you, it's worth to read up on this more.
That said, many newcomers are confused with how to begin, so by creating the folder structure, some
small modules and even making your first unit test, you are off to a great start!

View file

@ -1,265 +0,0 @@
# Implementing a game rule system
The simplest way to create an online roleplaying game (at least from a code perspective) is to
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
players the results. This is only one step away from a traditional tabletop game and puts heavy
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
are very dedicated.
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
roleplaying MUSHes.
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
variation - they use coded systems for things like combat and skill progression but leave role play
to be mostly freeform, overseen by staff game masters.
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
thus player fairness) is the most important aspect. In such games the only events with in-game value
are those resulting from code. Such games are very common and include everything from hack-and-slash
MUDs to various tactical simulations.
So your first decision needs to be just what type of system you are aiming for. This page will try
to give some ideas for how to organize the "coded" part of your system, however big that may be.
## Overall system infrastructure
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
game.
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
game master). The rest of your game will ask this black box questions and get answers back. Exactly
how it arrives at those results should not need to be known outside the box. Doing it this way
makes it easier to change and update things in one place later.
- Store only the minimum stuff you need with each game object. That is, if your Characters need
values for Health, a list of skills etc, store those things on the Character - don't store how to
roll or change them.
- Next is to determine just how you want to store things on your Objects and Characters. You can
choose to either store things as individual [Attributes](../../../Components/Attributes.md), like `character.db.STR=34` and
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
to look at the Ainneve [Trait
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
with a [custom django model](../../../Concepts/New-Models.md). Which is the better depends on your game and the
complexity of your system.
- Make a clear [API](https://en.wikipedia.org/wiki/Application_programming_interface) into your
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
want to check. That is, you want something similar to this:
```python
from world import rules
result = rules.roll_skill(character, "hunting")
result = rules.roll_challenge(character1, character2, "swords")
```
You might need to make these functions more or less complex depending on your game. For example the
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
Establishing just what you need to send into your game mechanic module is a great way to also get a
feel for what you need to add to your engine.
## Coded systems
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
end Evennia offers a full [dice
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
`random` module. No matter how it's implemented, we will in this text refer to the action of
determining an outcome as a "roll".
In a freeform system, the result of the roll is just compared with values and people (or the game
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
There are many things that may happen as a result of rule enforcement:
- Health may be added or deducted. This can effect the character in various ways.
- Experience may need to be added, and if a level-based system is used, the player might need to be
informed they have increased a level.
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
There are also a slew of other things that fall under "Coded systems", including things like
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
Game master would control in a tabletop role playing game can be mimicked to some level by coded
systems.
## Example of Rule module
Here is a simple example of a rule module. This is what we assume about our simple example game:
- Characters have only four numerical values:
- Their `level`, which starts at 1.
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
10.
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
- Their Health points, `HP`, which starts at 100.
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
failure message (as a stand-in for death code).
- Abilities are stored as simple Attributes on the Character.
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
example we ignore weapons and assume `STR` is all that matters).
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
### Character
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
an empty `Character` class there that Evennia will look to and use.
```python
from random import randint
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
Custom rule-restricted character. We randomize
the initial skill and ability values bettween 1-10.
"""
def at_object_creation(self):
"Called only when first created"
self.db.level = 1
self.db.HP = 100
self.db.XP = 0
self.db.STR = randint(1, 10)
self.db.combat = randint(5, 10)
```
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
Characters. Your Character was already created and will thus not have them. To force a reload, use
the following command:
```
@typeclass/force/reset self
```
The `examine self` command will now show the new Attributes.
### Rule module
This is a module `mygame/world/rules.py`.
```python
from random import randint
def roll_hit():
"Roll 1d100"
return randint(1, 100)
def roll_dmg():
"Roll 1d6"
return randint(1, 6)
def check_defeat(character):
"Checks if a character is 'defeated'."
if character.db.HP <= 0:
character.msg("You fall down, defeated!")
character.db.HP = 100 # reset
def add_XP(character, amount):
"Add XP to character, tracking level increases."
character.db.XP += amount
if character.db.XP >= (character.db.level + 1) ** 2:
character.db.level += 1
character.db.STR += 1
character.db.combat += 2
character.msg(f"You are now level {character.db.level}!")
def skill_combat(*args):
"""
This determines outcome of combat. The one who
rolls under their combat skill AND higher than
their opponent's roll hits.
"""
char1, char2 = args
roll1, roll2 = roll_hit(), roll_hit()
failtext_template = "You are hit by {attacker} for {dmg} damage!"
wintext_template = "You hit {target} for {dmg} damage!"
xp_gain = randint(1, 3)
if char1.db.combat >= roll1 > roll2:
# char 1 hits
dmg = roll_dmg() + char1.db.STR
char1.msg(wintext_template.format(target=char2, dmg=dmg))
add_XP(char1, xp_gain)
char2.msg(failtext_template.format(attacker=char1, dmg=dmg))
char2.db.HP -= dmg
check_defeat(char2)
elif char2.db.combat >= roll2 > roll1:
# char 2 hits
dmg = roll_dmg() + char2.db.STR
char1.msg(failtext_template.format(attacker=char2, dmg=dmg))
char1.db.HP -= dmg
check_defeat(char1)
char2.msg(wintext_template.format(target=char1, dmg=dmg))
add_XP(char2, xp_gain)
else:
# a draw
drawtext = "Neither of you can find an opening."
char1.msg(drawtext)
char2.msg(drawtext)
SKILLS = {"combat": skill_combat}
def roll_challenge(character1, character2, skillname):
"""
Determine the outcome of a skill challenge between
two characters based on the skillname given.
"""
if skillname in SKILLS:
SKILLS[skillname](character1, character2)
else:
raise RunTimeError(f"Skillname {skillname} not found.")
```
These few functions implement the entirety of our simple rule system. We have a function to check
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
look up the skills regardless of what their actual functions are called. Finally, the access
function `roll_challenge` just picks the skill and gets the result.
In this example, the skill function actually does a lot - it not only rolls results, it also informs
everyone of their results via `character.msg()` calls.
Here is an example of usage in a game command:
```python
from evennia import Command
from world import rules
class CmdAttack(Command):
"""
attack an opponent
Usage:
attack <target>
This will attack a target in the same room, dealing
damage with your bare hands.
"""
def func(self):
"Implementing combat"
caller = self.caller
if not self.args:
caller.msg("You need to pick a target to attack.")
return
target = caller.search(self.args)
if target:
rules.roll_challenge(caller, target, "combat")
```
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
number of Combat commands by just extending this functionality - you can easily roll challenges and
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
you don't have to change every command, but need only change the single `roll_hit` function inside
your `rules` module.

View file

@ -1,521 +0,0 @@
# Turn based Combat System
This tutorial gives an example of a full, if simplified, combat system for Evennia. It was inspired
by the discussions held on the [mailing
list](https://groups.google.com/forum/#!msg/evennia/wnJNM2sXSfs/-dbLRrgWnYMJ).
## Overview of combat system concepts
Most MUDs will use some sort of combat system. There are several main variations:
- _Freeform_ - the simplest form of combat to implement, common to MUSH-style roleplaying games.
This means the system only supplies dice rollers or maybe commands to compare skills and spit out
the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
the scene. A game master may be required to resolve rule disputes.
- _Twitch_ - This is the traditional MUD hack&slash style combat. In a twitch system there is often
no difference between your normal "move-around-and-explore mode" and the "combat mode". You enter an
attack command and the system will calculate if the attack hits and how much damage was caused.
Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
advantage of spamming or client scripting. Whereas the simplest systems just means entering `kill
<target>` over and over, more sophisticated twitch systems include anything from defensive stances
to tactical positioning.
- _Turn-based_ - a turn based system means that the system pauses to make sure all combatants can
choose their actions before continuing. In some systems, such entered actions happen immediately
(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
The disadvantage of a turn-based system is that the game must switch to a "combat mode" and one also
needs to take special care of how to handle new combatants and the passage of time. The advantage is
that success is not dependent on typing speed or of setting up quick client macros. This potentially
allows for emoting as part of combat which is an advantage for roleplay-heavy games.
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
example dice roller. To implement at twitch-based system you basically need a few combat
[commands](../../../Components/Commands.md), possibly ones with a [cooldown](../../Command-Cooldown.md). You also need a [game rule
module](./Implementing-a-game-rule-system.md) that makes use of it. We will focus on the turn-based
variety here.
## Tutorial overview
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
following properties:
- Combat is initiated with `attack <target>`, this initiates the combat mode.
- Characters may join an ongoing battle using `attack <target>` against a character already in
combat.
- Each turn every combating character will get to enter two commands, their internal order matters
and they are compared one-to-one in the order given by each combatant. Use of `say` and `pose` is
free.
- The commands are (in our example) simple; they can either `hit <target>`, `feint <target>` or
`parry <target>`. They can also `defend`, a generic passive defense. Finally they may choose to
`disengage/flee`.
- When attacking we use a classic [rock-paper-scissors](https://en.wikipedia.org/wiki/Rock-paper-
scissors) mechanic to determine success: `hit` defeats `feint`, which defeats `parry` which defeats
`hit`. `defend` is a general passive action that has a percentage chance to win against `hit`
(only).
- `disengage/flee` must be entered two times in a row and will only succeed if there is no `hit`
against them in that time. If so they will leave combat mode.
- Once every player has entered two commands, all commands are resolved in order and the result is
reported. A new turn then begins.
- If players are too slow the turn will time out and any unset commands will be set to `defend`.
For creating the combat system we will need the following components:
- A combat handler. This is the main mechanic of the system. This is a [Script](../../../Components/Scripts.md) object
created for each combat. It is not assigned to a specific object but is shared by the combating
characters and handles all the combat information. Since Scripts are database entities it also means
that the combat will not be affected by a server reload.
- A combat [command set](../../../Components/Command-Sets.md) with the relevant commands needed for combat, such as the
various attack/defend options and the `flee/disengage` command to leave the combat mode.
- A rule resolution system. The basics of making such a module is described in the [rule system
tutorial](./Implementing-a-game-rule-system.md). We will only sketch such a module here for our end-turn
combat resolution.
- An `attack` [command](../../../Components/Commands.md) for initiating the combat mode. This is added to the default
command set. It will create the combat handler and add the character(s) to it. It will also assign
the combat command set to the characters.
## The combat handler
The _combat handler_ is implemented as a stand-alone [Script](../../../Components/Scripts.md). This Script is created when
the first Character decides to attack another and is deleted when no one is fighting any more. Each
handler represents one instance of combat and one combat only. Each instance of combat can hold any
number of characters but each character can only be part of one combat at a time (a player would
need to disengage from the first combat before they could join another).
The reason we don't store this Script "on" any specific character is because any character may leave
the combat at any time. Instead the script holds references to all characters involved in the
combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
don't use this very much here this might allow the combat commands on the characters to access and
update the combat handler state directly.
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
time-keeping with the [TickerHandler](../../../Components/TickerHandler.md). This would require either adding custom hook
methods on the character or to implement a custom child of the TickerHandler class to track turns.
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
Here is a basic combat handler. Assuming our game folder is named `mygame`, we store it in
`mygame/typeclasses/combat_handler.py`:
```python
# mygame/typeclasses/combat_handler.py
import random
from evennia import DefaultScript
from world.rules import resolve_combat
class CombatHandler(DefaultScript):
"""
This implements the combat handler.
"""
# standard Script hooks
def at_script_creation(self):
"Called when script is first created"
self.key = f"combat_handler_{random.randint(1, 1000)}"
self.desc = "handles combat"
self.interval = 60 * 2 # two minute timeout
self.start_delay = True
self.persistent = True
# store all combatants
self.db.characters = {}
# store all actions for each turn
self.db.turn_actions = {}
# number of actions entered per combatant
self.db.action_count = {}
def _init_character(self, character):
"""
This initializes handler back-reference
and combat cmdset on a character
"""
character.ndb.combat_handler = self
character.cmdset.add("commands.combat.CombatCmdSet")
def _cleanup_character(self, character):
"""
Remove character from handler and clean
it of the back-reference and cmdset
"""
dbref = character.id
del self.db.characters[dbref]
del self.db.turn_actions[dbref]
del self.db.action_count[dbref]
del character.ndb.combat_handler
character.cmdset.delete("commands.combat.CombatCmdSet")
def at_start(self):
"""
This is called on first start but also when the script is restarted
after a server reboot. We need to re-assign this combat handler to
all characters as well as re-assign the cmdset.
"""
for character in self.db.characters.values():
self._init_character(character)
def at_stop(self):
"Called just before the script is stopped/destroyed."
for character in list(self.db.characters.values()):
# note: the list() call above disconnects list from database
self._cleanup_character(character)
def at_repeat(self):
"""
This is called every self.interval seconds (turn timeout) or
when force_repeat is called (because everyone has entered their
commands). We know this by checking the existence of the
`normal_turn_end` NAttribute, set just before calling
force_repeat.
"""
if self.ndb.normal_turn_end:
# we get here because the turn ended normally
# (force_repeat was called) - no msg output
del self.ndb.normal_turn_end
else:
# turn timeout
self.msg_all("Turn timer timed out. Continuing.")
self.end_turn()
# Combat-handler methods
def add_character(self, character):
"Add combatant to handler"
dbref = character.id
self.db.characters[dbref] = character
self.db.action_count[dbref] = 0
self.db.turn_actions[dbref] = [("defend", character, None),
("defend", character, None)]
# set up back-reference
self._init_character(character)
def remove_character(self, character):
"Remove combatant from handler"
if character.id in self.db.characters:
self._cleanup_character(character)
if not self.db.characters:
# if no more characters in battle, kill this handler
self.stop()
def msg_all(self, message):
"Send message to all combatants"
for character in self.db.characters.values():
character.msg(message)
def add_action(self, action, character, target):
"""
Called by combat commands to register an action with the handler.
action - string identifying the action, like "hit" or "parry"
character - the character performing the action
target - the target character or None
actions are stored in a dictionary keyed to each character, each
of which holds a list of max 2 actions. An action is stored as
a tuple (character, action, target).
"""
dbref = character.id
count = self.db.action_count[dbref]
if 0 <= count <= 1: # only allow 2 actions
self.db.turn_actions[dbref][count] = (action, character, target)
else:
# report if we already used too many actions
return False
self.db.action_count[dbref] += 1
return True
def check_end_turn(self):
"""
Called by the command to eventually trigger
the resolution of the turn. We check if everyone
has added all their actions; if so we call force the
script to repeat immediately (which will call
`self.at_repeat()` while resetting all timers).
"""
if all(count > 1 for count in self.db.action_count.values()):
self.ndb.normal_turn_end = True
self.force_repeat()
def end_turn(self):
"""
This resolves all actions by calling the rules module.
It then resets everything and starts the next turn. It
is called by at_repeat().
"""
resolve_combat(self, self.db.turn_actions)
if len(self.db.characters) < 2:
# less than 2 characters in battle, kill this handler
self.msg_all("Combat has ended")
self.stop()
else:
# reset counters before next turn
for character in self.db.characters.values():
self.db.characters[character.id] = character
self.db.action_count[character.id] = 0
self.db.turn_actions[character.id] = [("defend", character, None),
("defend", character, None)]
self.msg_all("Next turn begins ...")
```
This implements all the useful properties of our combat handler. This Script will survive a reboot
and will automatically re-assert itself when it comes back online. Even the current state of the
combat should be unaffected since it is saved in Attributes at every turn. An important part to note
is the use of the Script's standard `at_repeat` hook and the `force_repeat` method to end each turn.
This allows for everything to go through the same mechanisms with minimal repetition of code.
What is not present in this handler is a way for players to view the actions they set or to change
their actions once they have been added (but before the last one has added theirs). We leave this as
an exercise.
## Combat commands
Our combat commands - the commands that are to be available to us during the combat - are (in our
example) very simple. In a full implementation the commands available might be determined by the
weapon(s) held by the player or by which skills they know.
We create them in `mygame/commands/combat.py`.
```python
# mygame/commands/combat.py
from evennia import Command
class CmdHit(Command):
"""
hit an enemy
Usage:
hit <target>
Strikes the given enemy with your current weapon.
"""
key = "hit"
aliases = ["strike", "slash"]
help_category = "combat"
def func(self):
"Implements the command"
if not self.args:
self.caller.msg("Usage: hit <target>")
return
target = self.caller.search(self.args)
if not target:
return
ok = self.caller.ndb.combat_handler.add_action("hit",
self.caller,
target)
if ok:
self.caller.msg("You add 'hit' to the combat queue")
else:
self.caller.msg("You can only queue two actions per turn!")
# tell the handler to check if turn is over
self.caller.ndb.combat_handler.check_end_turn()
```
The other commands `CmdParry`, `CmdFeint`, `CmdDefend` and `CmdDisengage` look basically the same.
We should also add a custom `help` command to list all the available combat commands and what they
do.
We just need to put them all in a cmdset. We do this at the end of the same module:
```python
# mygame/commands/combat.py
from evennia import CmdSet
from evennia import default_cmds
class CombatCmdSet(CmdSet):
key = "combat_cmdset"
mergetype = "Replace"
priority = 10
no_exits = True
def at_cmdset_creation(self):
self.add(CmdHit())
self.add(CmdParry())
self.add(CmdFeint())
self.add(CmdDefend())
self.add(CmdDisengage())
self.add(CmdHelp())
self.add(default_cmds.CmdPose())
self.add(default_cmds.CmdSay())
```
## Rules module
A general way to implement a rule module is found in the [rule system tutorial](Implementing-a-game-
rule-system). Proper resolution would likely require us to change our Characters to store things
like strength, weapon skills and so on. So for this example we will settle for a very simplistic
rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
but just announce the results of each turn. In a real system the Character objects would hold stats
to affect their skills, their chosen weapon affect the choices, they would be able to lose health
etc.
Within each turn, there are "sub-turns", each consisting of one action per character. The actions
within each sub-turn happens simultaneously and only once they have all been resolved we move on to
the next sub-turn (or end the full turn).
*Note: In our simple example the sub-turns don't affect each other (except for `disengage/flee`),
nor do any effects carry over between turns. The real power of a turn-based system would be to add
real tactical possibilities here though; For example if your hit got parried you could be out of
balance and your next action would be at a disadvantage. A successful feint would open up for a
subsequent attack and so on ...*
Our rock-paper-scissor setup works like this:
- `hit` beats `feint` and `flee/disengage`. It has a random chance to fail against `defend`.
- `parry` beats `hit`.
- `feint` beats `parry` and is then counted as a `hit`.
- `defend` does nothing but has a chance to beat `hit`.
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the
turn). If so the character leaves combat.
```python
# mygame/world/rules.py
import random
# messages
def resolve_combat(combat_handler, actiondict):
"""
This is called by the combat handler
actiondict is a dictionary with a list of two actions
for each character:
{char.id:[(action1, char, target), (action2, char, target)], ...}
"""
flee = {} # track number of flee commands per character
for isub in range(2):
# loop over sub-turns
messages = []
for subturn in (sub[isub] for sub in actiondict.values()):
# for each character, resolve the sub-turn
action, char, target = subturn
if target:
taction, tchar, ttarget = actiondict[target.id][isub]
if action == "hit":
if taction == "parry" and ttarget == char:
messages.append(
f"{char} tries to hit {tchar}, but {tchar} parries the attack!"
)
elif taction == "defend" and random.random() < 0.5:
messages.append(
f"{tchar} defends against the attack by {char}."
)
elif taction == "flee":
flee[tchar] = -2
messages.append(
f"{char} stops {tchar} from disengaging, with a hit!"
)
else:
messages.append(
f"{char} hits {tchar}, bypassing their {taction}!"
)
elif action == "parry":
if taction == "hit":
messages.append(f"{char} parries the attack by {tchar}.")
elif taction == "feint":
messages.append(
f"{char} tries to parry, but {tchar} feints and hits!"
)
else:
messages.append(f"{char} parries to no avail.")
elif action == "feint":
if taction == "parry":
messages.append(
f"{char} feints past {tchar}'s parry, landing a hit!"
)
elif taction == "hit":
messages.append(f"{char} feints but is defeated by {tchar}'s hit!")
else:
messages.append(f"{char} feints to no avail.")
elif action == "defend":
messages.append(f"{char} defends.")
elif action == "flee":
if char in flee:
flee[char] += 1
else:
flee[char] = 1
messages.append(
f"{char} tries to disengage (two subsequent turns needed)"
)
# echo results of each subturn
combat_handler.msg_all("\n".join(messages))
# at the end of both sub-turns, test if anyone fled
for (char, fleevalue) in flee.items():
if fleevalue == 2:
combat_handler.msg_all(f"{char} withdraws from combat.")
combat_handler.remove_character(char)
```
To make it simple (and to save space), this example rule module actually resolves each interchange
twice - first when it gets to each character and then again when handling the target. Also, since we
use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up,
one could imagine tracking all the possible interactions to make sure each pair is only handled and
reported once.
## Combat initiator command
This is the last component we need, a command to initiate combat. This will tie everything together.
We store this with the other combat commands.
```python
# mygame/commands/combat.py
from evennia import create_script
class CmdAttack(Command):
"""
initiates combat
Usage:
attack <target>
This will initiate combat with <target>. If <target is
already in combat, you will join the combat.
"""
key = "attack"
help_category = "General"
def func(self):
"Handle command"
if not self.args:
self.caller.msg("Usage: attack <target>")
return
target = self.caller.search(self.args)
if not target:
return
# set up combat
if target.ndb.combat_handler:
# target is already in combat - join it
target.ndb.combat_handler.add_character(self.caller)
target.ndb.combat_handler.msg_all(f"{self.caller} joins combat!")
else:
# create a new combat handler
chandler = create_script("combat_handler.CombatHandler")
chandler.add_character(self.caller)
chandler.add_character(target)
self.caller.msg(f"You attack {target}! You are in combat.")
target.msg(f"{self.caller} attacks you! You are in combat.")
```
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
the [Adding Command Tutorial](../Part1/Adding-Commands.md) if you are unsure about how to do this.
## Expanding the example
At this point you should have a simple but flexible turn-based combat system. We have taken several
shortcuts and simplifications in this example. The output to the players is likely too verbose
during combat and too limited when it comes to informing about things surrounding it. Methods for
changing your commands or list them, view who is in combat etc is likely needed - this will require
play testing for each game and style. There is also currently no information displayed for other
people happening to be in the same room as the combat - some less detailed information should
probably be echoed to the room to
show others what's going on.