Doc refactor/renaming
This commit is contained in:
parent
9d8e8d7693
commit
b5b265ec3b
115 changed files with 518 additions and 434 deletions
|
|
@ -0,0 +1,265 @@
|
|||
# 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), 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). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](http://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("You are now level %i!" % 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 = "You are hit by %s for %i damage!"
|
||||
wintext = "You hit %s for %i damage!"
|
||||
xp_gain = randint(1, 3)
|
||||
if char1.db.combat >= roll1 > roll2:
|
||||
# char 1 hits
|
||||
dmg = roll_dmg() + char1.db.STR
|
||||
char1.msg(wintext % (char2, dmg))
|
||||
add_XP(char1, xp_gain)
|
||||
char2.msg(failtext % (char1, 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 % (char2, dmg))
|
||||
char1.db.HP -= dmg
|
||||
check_defeat(char1)
|
||||
char2.msg(wintext % (char1, 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("Skillname %s not found." % skillname)
|
||||
```
|
||||
|
||||
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.
|
||||
520
docs/source/Howto/Starting/Part3/Turn-based-Combat-System.md
Normal file
520
docs/source/Howto/Starting/Part3/Turn-based-Combat-System.md
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
# 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), possibly ones with a [cooldown](../../Command-Cooldown). You also need a [game rule
|
||||
module](Implementing-a-game-rule-system) 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) 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) 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). We will only sketch such a module here for our end-turn
|
||||
combat resolution.
|
||||
- An `attack` [command](../../../Components/Commands) 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). 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). 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 = "combat_handler_%i" % 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:
|
||||
msg = "%s tries to hit %s, but %s parries the attack!"
|
||||
messages.append(msg % (char, tchar, tchar))
|
||||
elif taction == "defend" and random.random() < 0.5:
|
||||
msg = "%s defends against the attack by %s."
|
||||
messages.append(msg % (tchar, char))
|
||||
elif taction == "flee":
|
||||
msg = "%s stops %s from disengaging, with a hit!"
|
||||
flee[tchar] = -2
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s hits %s, bypassing their %s!"
|
||||
messages.append(msg % (char, tchar, taction))
|
||||
elif action == "parry":
|
||||
if taction == "hit":
|
||||
msg = "%s parries the attack by %s."
|
||||
messages.append(msg % (char, tchar))
|
||||
elif taction == "feint":
|
||||
msg = "%s tries to parry, but %s feints and hits!"
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s parries to no avail."
|
||||
messages.append(msg % char)
|
||||
elif action == "feint":
|
||||
if taction == "parry":
|
||||
msg = "%s feints past %s's parry, landing a hit!"
|
||||
messages.append(msg % (char, tchar))
|
||||
elif taction == "hit":
|
||||
msg = "%s feints but is defeated by %s hit!"
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s feints to no avail."
|
||||
messages.append(msg % char)
|
||||
elif action == "defend":
|
||||
msg = "%s defends."
|
||||
messages.append(msg % char)
|
||||
elif action == "flee":
|
||||
if char in flee:
|
||||
flee[char] += 1
|
||||
else:
|
||||
flee[char] = 1
|
||||
msg = "%s tries to disengage (two subsequent turns needed)"
|
||||
messages.append(msg % char)
|
||||
|
||||
# echo results of each subturn
|
||||
combat_handler.msg_all("\n".join(messages))
|
||||
|
||||
# at the end of both sub-turns, test if anyone fled
|
||||
msg = "%s withdraws from combat."
|
||||
for (char, fleevalue) in flee.items():
|
||||
if fleevalue == 2:
|
||||
combat_handler.msg_all(msg % char)
|
||||
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("%s joins combat!" % self.caller)
|
||||
else:
|
||||
# create a new combat handler
|
||||
chandler = create_script("combat_handler.CombatHandler")
|
||||
chandler.add_character(self.caller)
|
||||
chandler.add_character(target)
|
||||
self.caller.msg("You attack %s! You are in combat." % target)
|
||||
target.msg("%s attacks you! You are in combat." % self.caller)
|
||||
```
|
||||
|
||||
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) 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.
|
||||
|
|
@ -0,0 +1,654 @@
|
|||
# Tutorial for basic MUSH like game
|
||||
|
||||
|
||||
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
|
||||
[MUSH](http://en.wikipedia.org/wiki/MUSH) is, for our purposes, a class of roleplay-centric games
|
||||
focused on free form storytelling. Even if you are not interested in MUSH:es, this is still a good
|
||||
first game-type to try since it's not so code heavy. You will be able to use the same principles for
|
||||
building other types of games.
|
||||
|
||||
The tutorial starts from scratch. If you did the [First Steps Coding](../Starting-Part1) tutorial
|
||||
already you should have some ideas about how to do some of the steps already.
|
||||
|
||||
The following are the (very simplistic and cut-down) features we will implement (this was taken from
|
||||
a feature request from a MUSH user new to Evennia). A Character in this system should:
|
||||
|
||||
- Have a “Power” score from 1 to 10 that measures how strong they are (stand-in for the stat
|
||||
system).
|
||||
- Have a command (e.g. `+setpower 4`) that sets their power (stand-in for character generation
|
||||
code).
|
||||
- Have a command (e.g. `+attack`) that lets them roll their power and produce a "Combat Score"
|
||||
between `1` and `10*Power`, displaying the result and editing their object to record this number
|
||||
(stand-in for `+actions` in the command code).
|
||||
- Have a command that displays everyone in the room and what their most recent "Combat Score" roll
|
||||
was (stand-in for the combat code).
|
||||
- Have a command (e.g. `+createNPC Jenkins`) that creates an NPC with full abilities.
|
||||
- Have a command to control NPCs, such as `+npc/cmd (name)=(command)` (stand-in for the NPC
|
||||
controlling code).
|
||||
|
||||
In this tutorial we will assume you are starting from an empty database without any previous
|
||||
modifications.
|
||||
|
||||
## Server Settings
|
||||
|
||||
To emulate a MUSH, the default `MULTISESSION_MODE=0` is enough (one unique session per
|
||||
account/character). This is the default so you don't need to change anything. You will still be able
|
||||
to puppet/unpuppet objects you have permission to, but there is no character selection out of the
|
||||
box in this mode.
|
||||
|
||||
We will assume our game folder is called `mygame` henceforth. You should be fine with the default
|
||||
SQLite3 database.
|
||||
|
||||
## Creating the Character
|
||||
|
||||
First thing is to choose how our Character class works. We don't need to define a special NPC object
|
||||
-- an NPC is after all just a Character without an Account currently controlling them.
|
||||
|
||||
Make your changes in the `mygame/typeclasses/characters.py` file:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
[...]
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when object is first created, only."
|
||||
self.db.power = 1
|
||||
self.db.combat_score = 1
|
||||
```
|
||||
|
||||
We defined two new [Attributes](../../../Components/Attributes) `power` and `combat_score` and set them to default
|
||||
values. Make sure to `@reload` the server if you had it already running (you need to reload every
|
||||
time you update your python code, don't worry, no accounts will be disconnected by the reload).
|
||||
|
||||
Note that only *new* characters will see your new Attributes (since the `at_object_creation` hook is
|
||||
called when the object is first created, existing Characters won't have it). To update yourself,
|
||||
run
|
||||
|
||||
@typeclass/force self
|
||||
|
||||
This resets your own typeclass (the `/force` switch is a safety measure to not do this
|
||||
accidentally), this means that `at_object_creation` is re-run.
|
||||
|
||||
examine self
|
||||
|
||||
Under the "Persistent attributes" heading you should now find the new Attributes `power` and `score`
|
||||
set on yourself by `at_object_creation`. If you don't, first make sure you `@reload`ed into the new
|
||||
code, next look at your server log (in the terminal/console) to see if there were any syntax errors
|
||||
in your code that may have stopped your new code from loading correctly.
|
||||
|
||||
## Character Generation
|
||||
|
||||
We assume in this example that Accounts first connect into a "character generation area". Evennia
|
||||
also supports full OOC menu-driven character generation, but for this example, a simple start room
|
||||
is enough. When in this room (or rooms) we allow character generation commands. In fact, character
|
||||
generation commands will *only* be available in such rooms.
|
||||
|
||||
Note that this again is made so as to be easy to expand to a full-fledged game. With our simple
|
||||
example, we could simply set an `is_in_chargen` flag on the account and have the `+setpower` command
|
||||
check it. Using this method however will make it easy to add more functionality later.
|
||||
|
||||
What we need are the following:
|
||||
|
||||
- One character generation [Command](../../../Components/Commands) to set the "Power" on the `Character`.
|
||||
- A chargen [CmdSet](../../../Components/Command-Sets) to hold this command. Lets call it `ChargenCmdset`.
|
||||
- A custom `ChargenRoom` type that makes this set of commands available to players in such rooms.
|
||||
- One such room to test things in.
|
||||
|
||||
### The +setpower command
|
||||
|
||||
For this tutorial we will add all our new commands to `mygame/commands/command.py` but you could
|
||||
split your commands into multiple module if you prefered.
|
||||
|
||||
For this tutorial character generation will only consist of one [Command](../../../Components/Commands) to set the
|
||||
Character s "power" stat. It will be called on the following MUSH-like form:
|
||||
|
||||
+setpower 4
|
||||
|
||||
Open `command.py` file. It contains documented empty templates for the base command and the
|
||||
"MuxCommand" type used by default in Evennia. We will use the plain `Command` type here, the
|
||||
`MuxCommand` class offers some extra features like stripping whitespace that may be useful - if so,
|
||||
just import from that instead.
|
||||
|
||||
Add the following to the end of the `command.py` file:
|
||||
|
||||
```python
|
||||
# end of command.py
|
||||
from evennia import Command # just for clarity; already imported above
|
||||
|
||||
class CmdSetPower(Command):
|
||||
"""
|
||||
set the power of a character
|
||||
|
||||
Usage:
|
||||
+setpower <1-10>
|
||||
|
||||
This sets the power of the current character. This can only be
|
||||
used during character generation.
|
||||
"""
|
||||
|
||||
key = "+setpower"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"This performs the actual command"
|
||||
errmsg = "You must supply a number between 1 and 10."
|
||||
if not self.args:
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
try:
|
||||
power = int(self.args)
|
||||
except ValueError:
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
if not (1 <= power <= 10):
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
# at this point the argument is tested as valid. Let's set it.
|
||||
self.caller.db.power = power
|
||||
self.caller.msg("Your Power was set to %i." % power)
|
||||
```
|
||||
This is a pretty straightforward command. We do some error checking, then set the power on ourself.
|
||||
We use a `help_category` of "mush" for all our commands, just so they are easy to find and separate
|
||||
in the help list.
|
||||
|
||||
Save the file. We will now add it to a new [CmdSet](../../../Components/Command-Sets) so it can be accessed (in a full
|
||||
chargen system you would of course have more than one command here).
|
||||
|
||||
Open `mygame/commands/default_cmdsets.py` and import your `command.py` module at the top. We also
|
||||
import the default `CmdSet` class for the next step:
|
||||
|
||||
```python
|
||||
from evennia import CmdSet
|
||||
from commands import command
|
||||
```
|
||||
|
||||
Next scroll down and define a new command set (based on the base `CmdSet` class we just imported at
|
||||
the end of this file, to hold only our chargen-specific command(s):
|
||||
|
||||
```python
|
||||
# end of default_cmdsets.py
|
||||
|
||||
class ChargenCmdset(CmdSet):
|
||||
"""
|
||||
This cmdset it used in character generation areas.
|
||||
"""
|
||||
key = "Chargen"
|
||||
def at_cmdset_creation(self):
|
||||
"This is called at initialization"
|
||||
self.add(command.CmdSetPower())
|
||||
```
|
||||
|
||||
In the future you can add any number of commands to this cmdset, to expand your character generation
|
||||
system as you desire. Now we need to actually put that cmdset on something so it's made available to
|
||||
users. We could put it directly on the Character, but that would make it available all the time.
|
||||
It's cleaner to put it on a room, so it's only available when players are in that room.
|
||||
|
||||
### Chargen areas
|
||||
|
||||
We will create a simple Room typeclass to act as a template for all our Chargen areas. Edit
|
||||
`mygame/typeclasses/rooms.py` next:
|
||||
|
||||
```python
|
||||
from commands.default_cmdsets import ChargenCmdset
|
||||
|
||||
# ...
|
||||
# down at the end of rooms.py
|
||||
|
||||
class ChargenRoom(Room):
|
||||
"""
|
||||
This room class is used by character-generation rooms. It makes
|
||||
the ChargenCmdset available.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"this is called only at first creation"
|
||||
self.cmdset.add(ChargenCmdset, permanent=True)
|
||||
```
|
||||
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
|
||||
Don't forget the `permanent=True` keyword or you will lose the cmdset after a server reload. For
|
||||
more information about [Command Sets](../../../Components/Command-Sets) and [Commands](../../../Components/Commands), see the respective
|
||||
links.
|
||||
|
||||
### Testing chargen
|
||||
|
||||
First, make sure you have `@reload`ed the server (or use `evennia reload` from the terminal) to have
|
||||
your new python code added to the game. Check your terminal and fix any errors you see - the error
|
||||
traceback lists exactly where the error is found - look line numbers in files you have changed.
|
||||
|
||||
We can't test things unless we have some chargen areas to test. Log into the game (you should at
|
||||
this point be using the new, custom Character class). Let's dig a chargen area to test.
|
||||
|
||||
@dig chargen:rooms.ChargenRoom = chargen,finish
|
||||
|
||||
If you read the help for `@dig` you will find that this will create a new room named `chargen`. The
|
||||
part after the `:` is the python-path to the Typeclass you want to use. Since Evennia will
|
||||
automatically try the `typeclasses` folder of our game directory, we just specify
|
||||
`rooms.ChargenRoom`, meaning it will look inside the module `rooms.py` for a class named
|
||||
`ChargenRoom` (which is what we created above). The names given after `=` are the names of exits to
|
||||
and from the room from your current location. You could also append aliases to each one name, such
|
||||
as `chargen;character generation`.
|
||||
|
||||
So in summary, this will create a new room of type ChargenRoom and open an exit `chargen` to it and
|
||||
an exit back here named `finish`. If you see errors at this stage, you must fix them in your code.
|
||||
`@reload`
|
||||
between fixes. Don't continue until the creation seems to have worked okay.
|
||||
|
||||
chargen
|
||||
|
||||
This should bring you to the chargen room. Being in there you should now have the `+setpower`
|
||||
command available, so test it out. When you leave (via the `finish` exit), the command will go away
|
||||
and trying `+setpower` should now give you a command-not-found error. Use `ex me` (as a privileged
|
||||
user) to check so the `Power` [Attribute](../../../Components/Attributes) has been set correctly.
|
||||
|
||||
If things are not working, make sure your typeclasses and commands are free of bugs and that you
|
||||
have entered the paths to the various command sets and commands correctly. Check the logs or command
|
||||
line for tracebacks and errors.
|
||||
|
||||
## Combat System
|
||||
|
||||
We will add our combat command to the default command set, meaning it will be available to everyone
|
||||
at all times. The combat system consists of a `+attack` command to get how successful our attack is.
|
||||
We also change the default `look` command to display the current combat score.
|
||||
|
||||
|
||||
### Attacking with the +attack command
|
||||
|
||||
Attacking in this simple system means rolling a random "combat score" influenced by the `power` stat
|
||||
set during Character generation:
|
||||
|
||||
> +attack
|
||||
You +attack with a combat score of 12!
|
||||
|
||||
Go back to `mygame/commands/command.py` and add the command to the end like this:
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
# ...
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
issues an attack
|
||||
|
||||
Usage:
|
||||
+attack
|
||||
|
||||
This will calculate a new combat score based on your Power.
|
||||
Your combat score is visible to everyone in the same location.
|
||||
"""
|
||||
key = "+attack"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"Calculate the random score between 1-10*Power"
|
||||
caller = self.caller
|
||||
power = caller.db.power
|
||||
if not power:
|
||||
# this can happen if caller is not of
|
||||
# our custom Character typeclass
|
||||
power = 1
|
||||
combat_score = random.randint(1, 10 * power)
|
||||
caller.db.combat_score = combat_score
|
||||
|
||||
# announce
|
||||
message = "%s +attack%s with a combat score of %s!"
|
||||
caller.msg(message % ("You", "", combat_score))
|
||||
caller.location.msg_contents(message %
|
||||
(caller.key, "s", combat_score),
|
||||
exclude=caller)
|
||||
```
|
||||
|
||||
What we do here is simply to generate a "combat score" using Python's inbuilt `random.randint()`
|
||||
function. We then store that and echo the result to everyone involved.
|
||||
|
||||
To make the `+attack` command available to you in game, go back to
|
||||
`mygame/commands/default_cmdsets.py` and scroll down to the `CharacterCmdSet` class. At the correct
|
||||
place add this line:
|
||||
|
||||
```python
|
||||
self.add(command.CmdAttack())
|
||||
```
|
||||
|
||||
`@reload` Evennia and the `+attack` command should be available to you. Run it and use e.g. `@ex` to
|
||||
make sure the `combat_score` attribute is saved correctly.
|
||||
|
||||
### Have "look" show combat scores
|
||||
|
||||
Players should be able to view all current combat scores in the room. We could do this by simply
|
||||
adding a second command named something like `+combatscores`, but we will instead let the default
|
||||
`look` command do the heavy lifting for us and display our scores as part of its normal output, like
|
||||
this:
|
||||
|
||||
> look Tom
|
||||
Tom (combat score: 3)
|
||||
This is a great warrior.
|
||||
|
||||
We don't actually have to modify the `look` command itself however. To understand why, take a look
|
||||
at how the default `look` is actually defined. It sits in `evennia/commands/default/general.py` (or
|
||||
browse it online
|
||||
[here](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L44)).
|
||||
You will find that the actual return text is done by the `look` command calling a *hook method*
|
||||
named `return_appearance` on the object looked at. All the `look` does is to echo whatever this hook
|
||||
returns. So what we need to do is to edit our custom Character typeclass and overload its
|
||||
`return_appearance` to return what we want (this is where the advantage of having a custom typeclass
|
||||
comes into play for real).
|
||||
|
||||
Go back to your custom Character typeclass in `mygame/typeclasses/characters.py`. The default
|
||||
implementation of `return appearance` is found in `evennia.DefaultCharacter` (or online
|
||||
[here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1438)). If you
|
||||
want to make bigger changes you could copy & paste the whole default thing into our overloading
|
||||
method. In our case the change is small though:
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
[...]
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when object is first created, only."
|
||||
self.db.power = 1
|
||||
self.db.combat_score = 1
|
||||
|
||||
def return_appearance(self, looker):
|
||||
"""
|
||||
The return from this method is what
|
||||
looker sees when looking at this object.
|
||||
"""
|
||||
text = super().return_appearance(looker)
|
||||
cscore = " (combat score: %s)" % self.db.combat_score
|
||||
if "\n" in text:
|
||||
# text is multi-line, add score after first line
|
||||
first_line, rest = text.split("\n", 1)
|
||||
text = first_line + cscore + "\n" + rest
|
||||
else:
|
||||
# text is only one line; add score to end
|
||||
text += cscore
|
||||
return text
|
||||
```
|
||||
|
||||
What we do is to simply let the default `return_appearance` do its thing (`super` will call the
|
||||
parent's version of the same method). We then split out the first line of this text, append our
|
||||
`combat_score` and put it back together again.
|
||||
|
||||
`@reload` the server and you should be able to look at other Characters and see their current combat
|
||||
scores.
|
||||
|
||||
> Note: A potentially more useful way to do this would be to overload the entire `return_appearance`
|
||||
of the `Room`s of your mush and change how they list their contents; in that way one could see all
|
||||
combat scores of all present Characters at the same time as looking at the room. We leave this as an
|
||||
exercise.
|
||||
|
||||
## NPC system
|
||||
|
||||
Here we will re-use the Character class by introducing a command that can create NPC objects. We
|
||||
should also be able to set its Power and order it around.
|
||||
|
||||
There are a few ways to define the NPC class. We could in theory create a custom typeclass for it
|
||||
and put a custom NPC-specific cmdset on all NPCs. This cmdset could hold all manipulation commands.
|
||||
Since we expect NPC manipulation to be a common occurrence among the user base however, we will
|
||||
instead put all relevant NPC commands in the default command set and limit eventual access with
|
||||
[Permissions and Locks](../../../Components/Locks#Permissions).
|
||||
|
||||
### Creating an NPC with +createNPC
|
||||
|
||||
We need a command for creating the NPC, this is a very straightforward command:
|
||||
|
||||
> +createnpc Anna
|
||||
You created the NPC 'Anna'.
|
||||
|
||||
At the end of `command.py`, create our new command:
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
|
||||
class CmdCreateNPC(Command):
|
||||
"""
|
||||
create a new npc
|
||||
|
||||
Usage:
|
||||
+createNPC <name>
|
||||
|
||||
Creates a new, named NPC. The NPC will start with a Power of 1.
|
||||
"""
|
||||
key = "+createnpc"
|
||||
aliases = ["+createNPC"]
|
||||
locks = "call:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"creates the object and names it"
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: +createNPC <name>")
|
||||
return
|
||||
if not caller.location:
|
||||
# may not create npc when OOC
|
||||
caller.msg("You must have a location to create an npc.")
|
||||
return
|
||||
# make name always start with capital letter
|
||||
name = self.args.strip().capitalize()
|
||||
# create npc in caller's location
|
||||
npc = create_object("characters.Character",
|
||||
key=name,
|
||||
location=caller.location,
|
||||
locks="edit:id(%i) and perm(Builders);call:false()" % caller.id)
|
||||
# announce
|
||||
message = "%s created the NPC '%s'."
|
||||
caller.msg(message % ("You", name))
|
||||
caller.location.msg_contents(message % (caller.key, name),
|
||||
exclude=caller)
|
||||
```
|
||||
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
|
||||
`nonpcs` "[permission](../../../Components/Locks#Permissions)" (in Evennia, a "permission" can just as well be used to
|
||||
block access, it depends on the lock we define). We create the NPC object in the caller's current
|
||||
location, using our custom `Character` typeclass to do so.
|
||||
|
||||
We set an extra lock condition on the NPC, which we will use to check who may edit the NPC later --
|
||||
we allow the creator to do so, and anyone with the Builders permission (or higher). See
|
||||
[Locks](../../../Components/Locks) for more information about the lock system.
|
||||
|
||||
Note that we just give the object default permissions (by not specifying the `permissions` keyword
|
||||
to the `create_object()` call). In some games one might want to give the NPC the same permissions
|
||||
as the Character creating them, this might be a security risk though.
|
||||
|
||||
Add this command to your default cmdset the same way you did the `+attack` command earlier.
|
||||
`@reload` and it will be available to test.
|
||||
|
||||
### Editing the NPC with +editNPC
|
||||
|
||||
Since we re-used our custom character typeclass, our new NPC already has a *Power* value - it
|
||||
defaults to 1. How do we change this?
|
||||
|
||||
There are a few ways we can do this. The easiest is to remember that the `power` attribute is just a
|
||||
simple [Attribute](../../../Components/Attributes) stored on the NPC object. So as a Builder or Admin we could set this
|
||||
right away with the default `@set` command:
|
||||
|
||||
@set mynpc/power = 6
|
||||
|
||||
The `@set` command is too generally powerful though, and thus only available to staff. We will add a
|
||||
custom command that only changes the things we want players to be allowed to change. We could in
|
||||
principle re-work our old `+setpower` command, but let's try something more useful. Let's make a
|
||||
`+editNPC` command.
|
||||
|
||||
> +editNPC Anna/power = 10
|
||||
Set Anna's property 'power' to 10.
|
||||
|
||||
This is a slightly more complex command. It goes at the end of your `command.py` file as before.
|
||||
|
||||
```python
|
||||
class CmdEditNPC(Command):
|
||||
"""
|
||||
edit an existing NPC
|
||||
|
||||
Usage:
|
||||
+editnpc <name>[/<attribute> [= value]]
|
||||
|
||||
Examples:
|
||||
+editnpc mynpc/power = 5
|
||||
+editnpc mynpc/power - displays power value
|
||||
+editnpc mynpc - shows all editable
|
||||
attributes and values
|
||||
|
||||
This command edits an existing NPC. You must have
|
||||
permission to edit the NPC to use this.
|
||||
"""
|
||||
key = "+editnpc"
|
||||
aliases = ["+editNPC"]
|
||||
locks = "cmd:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def parse(self):
|
||||
"We need to do some parsing here"
|
||||
args = self.args
|
||||
propname, propval = None, None
|
||||
if "=" in args:
|
||||
args, propval = [part.strip() for part in args.rsplit("=", 1)]
|
||||
if "/" in args:
|
||||
args, propname = [part.strip() for part in args.rsplit("/", 1)]
|
||||
# store, so we can access it below in func()
|
||||
self.name = args
|
||||
self.propname = propname
|
||||
# a propval without a propname is meaningless
|
||||
self.propval = propval if propname else None
|
||||
|
||||
def func(self):
|
||||
"do the editing"
|
||||
|
||||
allowed_propnames = ("power", "attribute1", "attribute2")
|
||||
|
||||
caller = self.caller
|
||||
if not self.args or not self.name:
|
||||
caller.msg("Usage: +editnpc name[/propname][=propval]")
|
||||
return
|
||||
npc = caller.search(self.name)
|
||||
if not npc:
|
||||
return
|
||||
if not npc.access(caller, "edit"):
|
||||
caller.msg("You cannot change this NPC.")
|
||||
return
|
||||
if not self.propname:
|
||||
# this means we just list the values
|
||||
output = "Properties of %s:" % npc.key
|
||||
for propname in allowed_propnames:
|
||||
propvalue = npc.attributes.get(propname, default="N/A")
|
||||
output += "\n %s = %s" % (propname, propvalue)
|
||||
caller.msg(output)
|
||||
elif self.propname not in allowed_propnames:
|
||||
caller.msg("You may only change %s." %
|
||||
", ".join(allowed_propnames))
|
||||
elif self.propval:
|
||||
# assigning a new propvalue
|
||||
# in this example, the properties are all integers...
|
||||
intpropval = int(self.propval)
|
||||
npc.attributes.add(self.propname, intpropval)
|
||||
caller.msg("Set %s's property '%s' to %s" %
|
||||
(npc.key, self.propname, self.propval))
|
||||
else:
|
||||
# propname set, but not propval - show current value
|
||||
caller.msg("%s has property %s = %s" %
|
||||
(npc.key, self.propname,
|
||||
npc.attributes.get(self.propname, default="N/A")))
|
||||
```
|
||||
|
||||
This command example shows off the use of more advanced parsing but otherwise it's mostly error
|
||||
checking. It searches for the given npc in the same room, and checks so the caller actually has
|
||||
permission to "edit" it before continuing. An account without the proper permission won't even be
|
||||
able to view the properties on the given NPC. It's up to each game if this is the way it should be.
|
||||
|
||||
Add this to the default command set like before and you should be able to try it out.
|
||||
|
||||
_Note: If you wanted a player to use this command to change an on-object property like the NPC's
|
||||
name (the `key` property), you'd need to modify the command since "key" is not an Attribute (it is
|
||||
not retrievable via `npc.attributes.get` but directly via `npc.key`). We leave this as an optional
|
||||
exercise._
|
||||
|
||||
### Making the NPC do stuff - the +npc command
|
||||
|
||||
Finally, we will make a command to order our NPC around. For now, we will limit this command to only
|
||||
be usable by those having the "edit" permission on the NPC. This can be changed if it's possible for
|
||||
anyone to use the NPC.
|
||||
|
||||
The NPC, since it inherited our Character typeclass has access to most commands a player does. What
|
||||
it doesn't have access to are Session and Player-based cmdsets (which means, among other things that
|
||||
they cannot chat on channels, but they could do that if you just added those commands). This makes
|
||||
the `+npc` command simple:
|
||||
|
||||
+npc Anna = say Hello!
|
||||
Anna says, 'Hello!'
|
||||
|
||||
Again, add to the end of your `command.py` module:
|
||||
|
||||
```python
|
||||
class CmdNPC(Command):
|
||||
"""
|
||||
controls an NPC
|
||||
|
||||
Usage:
|
||||
+npc <name> = <command>
|
||||
|
||||
This causes the npc to perform a command as itself. It will do so
|
||||
with its own permissions and accesses.
|
||||
"""
|
||||
key = "+npc"
|
||||
locks = "call:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def parse(self):
|
||||
"Simple split of the = sign"
|
||||
name, cmdname = None, None
|
||||
if "=" in self.args:
|
||||
name, cmdname = [part.strip()
|
||||
for part in self.args.rsplit("=", 1)]
|
||||
self.name, self.cmdname = name, cmdname
|
||||
|
||||
def func(self):
|
||||
"Run the command"
|
||||
caller = self.caller
|
||||
if not self.cmdname:
|
||||
caller.msg("Usage: +npc <name> = <command>")
|
||||
return
|
||||
npc = caller.search(self.name)
|
||||
if not npc:
|
||||
return
|
||||
if not npc.access(caller, "edit"):
|
||||
caller.msg("You may not order this NPC to do anything.")
|
||||
return
|
||||
# send the command order
|
||||
npc.execute_cmd(self.cmdname)
|
||||
caller.msg("You told %s to do '%s'." % (npc.key, self.cmdname))
|
||||
```
|
||||
|
||||
Note that if you give an erroneous command, you will not see any error message, since that error
|
||||
will be returned to the npc object, not to you. If you want players to see this, you can give the
|
||||
caller's session ID to the `execute_cmd` call, like this:
|
||||
|
||||
```python
|
||||
npc.execute_cmd(self.cmdname, sessid=self.caller.sessid)
|
||||
```
|
||||
|
||||
Another thing to remember is however that this is a very simplistic way to control NPCs. Evennia
|
||||
supports full puppeting very easily. An Account (assuming the "puppet" permission was set correctly)
|
||||
could simply do `@ic mynpc` and be able to play the game "as" that NPC. This is in fact just what
|
||||
happens when an Account takes control of their normal Character as well.
|
||||
|
||||
## Concluding remarks
|
||||
|
||||
This ends the tutorial. It looks like a lot of text but the amount of code you have to write is
|
||||
actually relatively short. At this point you should have a basic skeleton of a game and a feel for
|
||||
what is involved in coding your game.
|
||||
|
||||
From here on you could build a few more ChargenRooms and link that to a bigger grid. The `+setpower`
|
||||
command can either be built upon or accompanied by many more to get a more elaborate character
|
||||
generation.
|
||||
|
||||
The simple "Power" game mechanic should be easily expandable to something more full-fledged and
|
||||
useful, same is true for the combat score principle. The `+attack` could be made to target a
|
||||
specific player (or npc) and automatically compare their relevant attributes to determine a result.
|
||||
|
||||
To continue from here, you can take a look at the [Tutorial World](../Part1/Tutorial-World-Introduction). For
|
||||
more specific ideas, see the [other tutorials and hints](../../Howto-Overview) as well
|
||||
as the [Evennia Component overview](../../../Components/Components-Overview).
|
||||
Loading…
Add table
Add a link
Reference in a new issue