mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Start documenting evadventure
This commit is contained in:
parent
080db7092c
commit
2a62cc427a
27 changed files with 1164 additions and 77 deletions
|
|
@ -174,6 +174,24 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||
- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode
|
||||
files - this works better with black linting.
|
||||
- Added `move_type` str kwarg to `move_to()` calls, optionally identifying the type of
|
||||
move being done ('teleport', 'disembark', 'give' etc). (volund)
|
||||
- Made RPSystem contrib msg calls pass `pose` or `say` as msg-`type` for use in
|
||||
e.g. webclient pane filtering where desired. (volund)
|
||||
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
||||
finding if a user uses a screenreader (and adjust display accordingly).
|
||||
- Fixed bug in `cmdset.remove()` where a command could not be deleted by `key`,
|
||||
even though doc suggested one could (ChrisLR)
|
||||
- New contrib `name_generator` for building random real-world based or fantasy-names
|
||||
based on phonetic rules.
|
||||
- Enable proper serialization of dict subclasses in Attributes (aogier)
|
||||
- `object.search` fuzzy-matching now uses `icontains` instead of `istartswith`
|
||||
to better match how search works elsewhere (volund)
|
||||
- The `.at_traverse` hook now receives a `exit_obj` kwarg, linking back to the
|
||||
exit triggering the hook (volund)
|
||||
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
|
||||
- New `at_server_init()` hook called before all other startup hooks for all
|
||||
startup modes. Used for more generic overriding (volund)
|
||||
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ unexpected bug.
|
|||
If you have implemented your own tests for your game you can run them from your game dir
|
||||
with
|
||||
|
||||
evennia test .
|
||||
evennia test --settings settings.py .
|
||||
|
||||
The period (`.`) means to run all tests found in the current directory and all subdirectories. You
|
||||
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
|
||||
|
|
|
|||
371
docs/source/Contribs/Contrib-Buffs.md
Normal file
371
docs/source/Contribs/Contrib-Buffs.md
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
# Buffs
|
||||
|
||||
Contribution by Tegiminis 2022
|
||||
|
||||
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
||||
It is a common design pattern in RPGs, particularly action games.
|
||||
|
||||
Features:
|
||||
|
||||
- `BuffHandler`: A buff handler to apply to your objects.
|
||||
- `BaseBuff`: A buff class to extend from to create your own buffs.
|
||||
- `BuffableProperty`: A sample property class to show how to automatically check modifiers.
|
||||
- `CmdBuff`: A command which applies buffs.
|
||||
- `samplebuffs.py`: Some sample buffs to learn from.
|
||||
|
||||
## Quick Start
|
||||
Assign the handler to a property on the object, like so.
|
||||
|
||||
```python
|
||||
@lazy_property
|
||||
def buffs(self) -> BuffHandler:
|
||||
return BuffHandler(self)
|
||||
```
|
||||
|
||||
You may then call the handler to add or manipulate buffs like so: `object.buffs`. See **Using the Handler**.
|
||||
|
||||
### Customization
|
||||
|
||||
If you want to customize the handler, you can feed the constructor two arguments:
|
||||
- `dbkey`: The string you wish to use as the attribute key for the buff database. Defaults to "buffs". This allows you to keep separate buff pools - for example, "buffs" and "perks".
|
||||
- `autopause`: If you want this handler to automatically pause playtime buffs when its owning object is unpuppeted.
|
||||
|
||||
> **Note**: If you enable autopausing, you MUST initialize the property in your owning object's
|
||||
> `at_init` hook. Otherwise, a hot reload can cause playtime buffs to not update properly
|
||||
> on puppet/unpuppet. You have been warned!
|
||||
|
||||
Let's say you want another handler for an object, `perks`, which has a separate database and
|
||||
respects playtime buffs. You'd assign this new property as so:
|
||||
|
||||
```python
|
||||
class BuffableObject(Object):
|
||||
@lazy_property
|
||||
def perks(self) -> BuffHandler:
|
||||
return BuffHandler(self, dbkey='perks', autopause=True)
|
||||
|
||||
def at_init(self):
|
||||
self.perks
|
||||
```
|
||||
|
||||
## Using the Handler
|
||||
|
||||
Here's how to make use of your new handler.
|
||||
|
||||
### Apply a Buff
|
||||
|
||||
Call the handler's `add` method. This requires a class reference, and also contains a number of
|
||||
optional arguments to customize the buff's duration, stacks, and so on. You can also store any arbitrary value
|
||||
in the buff's cache by passing a dictionary through the `to_cache` optional argument. This will not overwrite the normal
|
||||
values on the cache.
|
||||
|
||||
```python
|
||||
self.buffs.add(StrengthBuff) # A single stack of StrengthBuff with normal duration
|
||||
self.buffs.add(DexBuff, stacks=3, duration=60) # Three stacks of DexBuff, with a duration of 60 seconds
|
||||
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of ReflectBuff, with an extra cache value
|
||||
```
|
||||
|
||||
Two important attributes on the buff are checked when the buff is applied: `refresh` and `unique`.
|
||||
- `refresh` (default: True) determines if a buff's timer is refreshed when it is reapplied.
|
||||
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
|
||||
|
||||
The combination of these two booleans creates one of three kinds of keys:
|
||||
- `Unique is True, Refresh is True/False`: The buff's default key.
|
||||
- `Unique is False, Refresh is True`: The default key mixed with the applier's dbref. This makes the buff "unique-per-player", so you can refresh through reapplication.
|
||||
- `Unique is False, Refresh is False`: The default key mixed with a randomized number.
|
||||
|
||||
### Get Buffs
|
||||
|
||||
The handler has several getter methods which return instanced buffs. You won't need to use these for basic functionality, but if you want to manipulate
|
||||
buffs after application, they are very useful. The handler's `check`/`trigger` methods utilize some of these getters, while others are just for developer convenience.
|
||||
|
||||
`get(key)` is the most basic getter. It returns a single buff instance, or `None` if the buff doesn't exist on the handler. It is also the only getter
|
||||
that returns a single buff instance, rather than a dictionary.
|
||||
|
||||
Group getters, listed below, return a dictionary of values in the format `{buffkey: instance}`. If you want to iterate over all of these buffs,
|
||||
you should do so via the `dict.values()` method.
|
||||
|
||||
- `get_all()` returns all buffs on this handler. You can also use the `handler.all` property.
|
||||
- `get_by_type(BuffClass)` returns buffs of the specified type.
|
||||
- `get_by_stat(stat)` returns buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `get_by_trigger(string)` returns buffs with the specified string in their `triggers` list.
|
||||
- `get_by_source(Object)` returns buffs applied by the specified `source` object.
|
||||
- `get_by_cachevalue(key, value)` returns buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
All group getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
|
||||
|
||||
```python
|
||||
dict1 = handler.get_by_type(Burned) # This finds all "Burned" buffs on the handler
|
||||
dict2 = handler.get_by_source(self, to_filter=dict1) # This filters dict1 to find buffs with the matching source
|
||||
```
|
||||
|
||||
> **Note**: Most of these getters also have an associated handler property. For example, `handler.effects` returns all buffs that can be triggered, which
|
||||
> is then iterated over by the `get_by_trigger` method.
|
||||
|
||||
### Remove Buffs
|
||||
|
||||
There are also a number of remover methods. Generally speaking, these follow the same format as the getters.
|
||||
|
||||
- `remove(key)` removes the buff with the specified key.
|
||||
- `clear()` removes all buffs.
|
||||
- `remove_by_type(BuffClass)` removes buffs of the specified type.
|
||||
- `remove_by_stat(stat)` removes buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `remove_by_trigger(string)` removes buffs with the specified string in their `triggers` list.
|
||||
- `remove_by_source(Object)` removes buffs applied by the specified source
|
||||
- `remove_by_cachevalue(key, value)` removes buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
You can also remove a buff by calling the instance's `remove` helper method. You can do this on the dictionaries returned by the
|
||||
getters listed above.
|
||||
|
||||
```python
|
||||
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
|
||||
for buff in to_remove.values(): # Removes all buffs in the to_remove dictionary via helper methods
|
||||
buff.remove()
|
||||
```
|
||||
|
||||
### Check Modifiers
|
||||
|
||||
Call the handler `check(value, stat)` method when you want to see the modified value.
|
||||
This will return the `value`, modified by any relevant buffs on the handler's owner (identified by
|
||||
the `stat` string).
|
||||
|
||||
For example, let's say you want to modify how much damage you take. That might look something like this:
|
||||
|
||||
```python
|
||||
# The method we call to damage ourselves
|
||||
def take_damage(self, source, damage):
|
||||
_damage = self.buffs.check(damage, 'taken_damage')
|
||||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
This method calls the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
|
||||
buffs that are reactive to being checked; for example, removing themselves, altering their values, or interacting with the game state.
|
||||
|
||||
> **Note**: You can also trigger relevant buffs at the same time as you check them by ensuring the optional argument `trigger` is True in the `check` method.
|
||||
|
||||
### Trigger Buffs
|
||||
|
||||
Call the handler's `trigger(string)` method when you want an event call. This will call the `at_trigger` hook method on all buffs with the relevant trigger `string`.
|
||||
|
||||
For example, let's say you want to trigger a buff to "detonate" when you hit your target with an attack.
|
||||
You'd write a buff that might look like this:
|
||||
|
||||
```python
|
||||
class Detonate(BaseBuff):
|
||||
...
|
||||
triggers = ['take_damage']
|
||||
def at_trigger(self, trigger, *args, **kwargs)
|
||||
self.owner.take_damage(100)
|
||||
self.remove()
|
||||
```
|
||||
|
||||
And then call `handler.trigger('take_damage')` in the method you use to take damage.
|
||||
|
||||
> **Note** You could also do this through mods and `at_post_check` if you like, depending on how to want to add the damage.
|
||||
|
||||
### Ticking
|
||||
|
||||
Ticking buffs are slightly special. They are similar to trigger buffs in that they run code, but instead of
|
||||
doing so on an event trigger, they do so on a periodic tick. A common use case for a buff like this is a poison,
|
||||
or a heal over time.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
tickrate = 5
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
_dmg = self.dmg * self.stacks
|
||||
if not initial:
|
||||
self.owner.location.msg_contents(
|
||||
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
|
||||
actor=self.owner.named, damage=_dmg
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
To make a buff ticking, ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
|
||||
method. Once you add it to the handler, it starts ticking!
|
||||
|
||||
> **Note**: Ticking buffs always tick on initial application, when `initial` is `True`. If you don't want your hook to fire at that time,
|
||||
> make sure to check the value of `initial` in your `at_tick` method.
|
||||
|
||||
### Context
|
||||
|
||||
Every important handler method optionally accepts a `context` dictionary.
|
||||
|
||||
Context is an important concept for this handler. Every method which checks, triggers, or ticks a buff passes this
|
||||
dictionary (default: empty) to the buff hook methods as keyword arguments (`**kwargs`). It is used for nothing else. This allows you to make those
|
||||
methods "event-aware" by storing relevant data in the dictionary you feed to the method.
|
||||
|
||||
For example, let's say you want a "thorns" buff which damages enemies that attack you. Let's take our `take_damage` method
|
||||
and add a context to the mix.
|
||||
|
||||
```python
|
||||
def take_damage(attacker, damage):
|
||||
context = {'attacker': attacker, 'damage': damage}
|
||||
_damage = self.buffs.check(damage, 'taken_damage', context=context)
|
||||
self.buffs.trigger('taken_damage', context=context)
|
||||
self.db.health -= _damage
|
||||
```
|
||||
Now we use the values that context passes to the buff kwargs to customize our logic.
|
||||
```python
|
||||
class ThornsBuff(BaseBuff):
|
||||
...
|
||||
triggers = ['taken_damage']
|
||||
# This is the hook method on our thorns buff
|
||||
def at_trigger(self, trigger, attacker=None, damage=0, **kwargs):
|
||||
if not attacker:
|
||||
return
|
||||
attacker.db.health -= damage * 0.2
|
||||
```
|
||||
Apply the buff, take damage, and watch the thorns buff do its work!
|
||||
|
||||
## Creating New Buffs
|
||||
|
||||
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
|
||||
However, there are a lot of individual moving parts to a buff. Here's a step-through of the important stuff.
|
||||
|
||||
### Basics
|
||||
|
||||
Regardless of any other functionality, all buffs have the following class attributes:
|
||||
|
||||
- They have customizable `key`, `name`, and `flavor` strings.
|
||||
- They have a `duration` (float), and automatically clean-up at the end. Use -1 for infinite duration, and 0 to clean-up immediately. (default: -1)
|
||||
- They can stack, if `maxstacks` (int) is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
|
||||
- They can be `unique` (bool), which determines if they have a unique namespace or not. (default: True)
|
||||
- They can `refresh` (bool), which resets the duration when stacked or reapplied. (default: True)
|
||||
- They can be `playtime` (bool) buffs, where duration only counts down during active play. (default: False)
|
||||
|
||||
They also always store some useful mutable information about themselves in the cache:
|
||||
|
||||
- `ref` (class): The buff class path we use to construct the buff.
|
||||
- `start` (float): The timestamp of when the buff was applied.
|
||||
- `source` (Object): If specified; this allows you to track who or what applied the buff.
|
||||
- `prevtick` (float): The timestamp of the previous tick.
|
||||
- `duration` (float): The cached duration. This can vary from the class duration, depending on if the duration has been modified (paused, extended, shortened, etc).
|
||||
- `stacks` (int): How many stacks they have.
|
||||
- `paused` (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.
|
||||
|
||||
You can always access the raw cache dictionary through the `cache` attribute on an instanced buff. This is grabbed when you get the buff through
|
||||
a handler method, so it may not always reflect recent changes you've made, depending on how you structure your buff calls. All of the above
|
||||
mutable information can be found in this cache, as well as any arbitrary information you pass through the handler `add` method (via `to_cache`).
|
||||
|
||||
### Modifiers
|
||||
|
||||
Mods are stored in the `mods` list attribute. Buffs which have one or more Mod objects in them can modify stats. You can use the handler method to check all
|
||||
mods of a specific stat string and apply their modifications to the value; however, you are encouraged to use `check` in a getter/setter, for easy access.
|
||||
|
||||
Mod objects consist of only four values, assigned by the constructor in this order:
|
||||
|
||||
- `stat`: The stat you want to modify. When `check` is called, this string is used to find all the mods that are to be collected.
|
||||
- `mod`: The modifier. Defaults are 'add' and 'mult'. Modifiers are calculated additively, and in standard arithmetic order (see `_calculate_mods` for more)
|
||||
- `value`: How much value the modifier gives regardless of stacks
|
||||
- `perstack`: How much value the modifier grants per stack, INCLUDING the first. (default: 0)
|
||||
|
||||
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
|
||||
|
||||
```python
|
||||
class DamageBuff(BaseBuff):
|
||||
mods = [Mod('damage', 'add', 10)]
|
||||
```
|
||||
|
||||
No mods applied to the value are permanent in any way. All calculations are done at runtime, and the mod values are never stored
|
||||
anywhere except on the buff in question. In other words: you don't need to track the origin of particular stat mods, and you will
|
||||
never permanently change a stat modified by a buff. To remove the modification, simply remove the buff from the object.
|
||||
|
||||
> **Note**: You can add your own modifier types by overloading the `_calculate_mods` method, which contains the basic modifier application logic.
|
||||
|
||||
#### Generating Mods (Advanced)
|
||||
|
||||
An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.
|
||||
|
||||
```python
|
||||
class GeneratedStatBuff(BaseBuff):
|
||||
...
|
||||
def __init__(self, handler, buffkey, cache={}) -> None:
|
||||
super().__init__(handler, buffkey, cache)
|
||||
# Finds our "modgen" cache value, and generates a mod from it
|
||||
modgen = list(self.cache.get("modgen"))
|
||||
if modgen:
|
||||
self.mods = [Mod(*modgen)]
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
|
||||
|
||||
When the handler's `trigger` method is called, it searches all buffs on the handler for any with a matchingtrigger,
|
||||
then calls their `at_trigger` hooks. Buffs can have multiple triggers, and you can tell which trigger was used by
|
||||
the `trigger` argument in the hook.
|
||||
|
||||
```python
|
||||
class AmplifyBuff(BaseBuff):
|
||||
triggers = ['damage', 'heal']
|
||||
|
||||
def at_trigger(self, trigger, **kwargs):
|
||||
if trigger == 'damage': print('Damage trigger called!')
|
||||
if trigger == 'heal': print('Heal trigger called!')
|
||||
```
|
||||
|
||||
### Ticking
|
||||
|
||||
A buff which ticks isn't much different than one which triggers. You're still executing arbitrary hooks on
|
||||
the buff class. To tick, the buff must have a `tickrate` of 1 or higher.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
# this buff will tick 6 times between application and cleanup.
|
||||
duration = 30
|
||||
tickrate = 5
|
||||
def at_tick(self, initial, **kwargs):
|
||||
self.owner.take_damage(10)
|
||||
```
|
||||
> **Note**: The buff always ticks once when applied. For this **first tick only**, `initial` will be True in the `at_tick` hook method. `initial` will be False on subsequent ticks.
|
||||
|
||||
Ticks utilize a persistent delay, so they should be pickleable. As long as you are not adding new properties to your buff class, this shouldn't be a concern.
|
||||
If you **are** adding new properties, try to ensure they do not end up with a circular code path to their object or handler, as this will cause pickling errors.
|
||||
|
||||
### Extras
|
||||
|
||||
Buffs have a grab-bag of extra functionality to let you add complexity to your designs.
|
||||
|
||||
#### Conditionals
|
||||
|
||||
You can restrict whether or not the buff will `check`, `trigger`, or `tick` through defining the `conditional` hook. As long
|
||||
as it returns a "truthy" value, the buff will apply itself. This is useful for making buffs dependent on game state - for
|
||||
example, if you want a buff that makes the player take more damage when they are on fire:
|
||||
|
||||
```python
|
||||
class FireSick(BaseBuff):
|
||||
...
|
||||
def conditional(self, *args, **kwargs):
|
||||
if self.owner.buffs.get_by_type(FireBuff):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
Conditionals for `check`/`trigger` are checked when the buffs are gathered by the handler methods for the respective operations. `Tick`
|
||||
conditionals are checked each tick.
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
Buff instances have a number of helper methods.
|
||||
|
||||
- `remove`/`dispel`: Allows you to remove or dispel the buff. Calls `at_remove`/`at_dispel`, depending on optional arguments.
|
||||
- `pause`/`unpause`: Pauses and unpauses the buff. Calls `at_pause`/`at_unpause`.
|
||||
- `reset`: Resets the buff's start to the current time; same as "refreshing" it.
|
||||
|
||||
#### Playtime Duration
|
||||
|
||||
If your handler has `autopause` enabled, any buffs with truthy `playtime` value will automatically pause
|
||||
and unpause when the object the handler is attached to is puppetted or unpuppetted. This even works with ticking buffs,
|
||||
although if you have less than 1 second of tick duration remaining, it will round up to 1s.
|
||||
|
||||
> **Note**: If you want more control over this process, you can comment out the signal subscriptions on the handler and move the autopause logic
|
||||
> to your object's `at_pre/post_puppet/unpuppet` hooks.
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/rpg/buffs/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
282
docs/source/Contribs/Contrib-Name-Generator.md
Normal file
282
docs/source/Contribs/Contrib-Name-Generator.md
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# Random Name Generator
|
||||
|
||||
Contribution by InspectorCaracal (2022)
|
||||
|
||||
A module for generating random names, both real-world and fantasy. Real-world
|
||||
names can be generated either as first (personal) names, family (last) names, or
|
||||
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
Fantasy names are generated from basic phonetic rules, using CVC syllable syntax.
|
||||
|
||||
Both real-world and fantasy name generation can be extended to include additional
|
||||
information via your game's `settings.py`
|
||||
|
||||
## Installation
|
||||
|
||||
This is a stand-alone utility. Just import this module (`from evennia.contrib.utils import name_generator`) and use its functions wherever you like.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the module where you need it with the following:
|
||||
```py
|
||||
from evennia.contrib.utils.name_generator import namegen
|
||||
```
|
||||
|
||||
By default, all of the functions will return a string with one generated name.
|
||||
If you specify more than one, or pass `return_list=True` as a keyword argument, the returned value will be a list of strings.
|
||||
|
||||
The module is especially useful for naming newly-created NPCs, like so:
|
||||
```py
|
||||
npc_name = namegen.full_name()
|
||||
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
|
||||
```
|
||||
|
||||
## Available Settings
|
||||
|
||||
These settings can all be defined in your game's `server/conf/settings.py` file.
|
||||
|
||||
- `NAMEGEN_FIRST_NAMES` adds a new list of first (personal) names.
|
||||
- `NAMEGEN_LAST_NAMES` adds a new list of last (family) names.
|
||||
- `NAMEGEN_REPLACE_LISTS` - set to `True` if you want to use only the names defined in your settings.
|
||||
- `NAMEGEN_FANTASY_RULES` lets you add new phonetic rules for generating entirely made-up names. See the section "Custom Fantasy Name style rules" for details on how this should look.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
NAMEGEN_FIRST_NAMES = [
|
||||
("Evennia", 'mf'),
|
||||
("Green Tea", 'f'),
|
||||
]
|
||||
|
||||
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||
|
||||
NAMEGEN_FANTASY_RULES = {
|
||||
"example_style": {
|
||||
"syllable": "(C)VC",
|
||||
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||
"start": ['m'],
|
||||
"end": ['x','n'],
|
||||
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||
"length": (2,4),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Generating Real Names
|
||||
|
||||
The contrib offers three functions for generating random real-world names:
|
||||
`first_name()`, `last_name()`, and `full_name()`. If you want more than one name
|
||||
generated at once, you can use the `num` keyword argument to specify how many.
|
||||
|
||||
Example:
|
||||
```
|
||||
>>> namegen.first_name(num=5)
|
||||
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']
|
||||
>>> namegen.first_name(gender='m')
|
||||
'Blanchard'
|
||||
```
|
||||
|
||||
The `first_name` function also takes a `gender` keyword argument to filter names
|
||||
by gender association. 'f' for feminine, 'm' for masculine, 'mf' for feminine
|
||||
_and_ masculine, or the default `None` to match any gendering.
|
||||
|
||||
The `full_name` function also takes the `gender` keyword, as well as `parts` which
|
||||
defines how many names make up the full name. The minimum is two: a first name and
|
||||
a last name. You can also generate names with the family name first by setting
|
||||
the keyword arg `surname_first` to `True`
|
||||
|
||||
Example:
|
||||
```
|
||||
>>> namegen.full_name()
|
||||
'Keeva Bernat'
|
||||
>>> namegen.full_name(parts=4)
|
||||
'Suzu Shabnam Kafka Baier'
|
||||
>>> namegen.full_name(parts=3, surname_first=True)
|
||||
'Ó Muircheartach Torunn Dyson'
|
||||
>>> namegen.full_name(gender='f')
|
||||
'Wikolia Ó Deasmhumhnaigh'
|
||||
```
|
||||
|
||||
### Adding your own names
|
||||
|
||||
You can add additional names with the settings `NAMEGEN_FIRST_NAMES` and
|
||||
`NAMEGEN_LAST_NAMES`
|
||||
|
||||
`NAMEGEN_FIRST_NAMES` should be a list of tuples, where the first value is the name
|
||||
and then second value is the gender flag - 'm' for masculine-only, 'f' for feminine-
|
||||
only, and 'mf' for either one.
|
||||
|
||||
`NAMEGEN_LAST_NAMES` should be a list of strings, where each item is an available
|
||||
surname.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
NAMEGEN_FIRST_NAMES = [
|
||||
("Evennia", 'mf'),
|
||||
("Green Tea", 'f'),
|
||||
]
|
||||
|
||||
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||
```
|
||||
|
||||
Set `NAMEGEN_REPLACE_LISTS = True` if you want your custom lists above to entirely replace the built-in lists rather than extend them.
|
||||
|
||||
## Generating Fantasy Names
|
||||
|
||||
Generating completely made-up names is done with the `fantasy_name` function. The
|
||||
contrib comes with three built-in styles of names which you can use, or you can
|
||||
put a dictionary of custom name rules into `settings.py`
|
||||
|
||||
Generating a fantasy name takes the ruleset key as the "style" keyword, and can
|
||||
return either a single name or multiple names. By default, it will return a
|
||||
single name in the built-in "harsh" style. The contrib also comes with "fluid" and "alien" styles.
|
||||
|
||||
```py
|
||||
>>> namegen.fantasy_name()
|
||||
'Vhon'
|
||||
>>> namegen.fantasy_name(num=3, style="harsh")
|
||||
['Kha', 'Kizdhu', 'Godögäk']
|
||||
>>> namegen.fantasy_name(num=3, style="fluid")
|
||||
['Aewalisash', 'Ayi', 'Iaa']
|
||||
>>> namegen.fantasy_name(num=5, style="alien")
|
||||
["Qz'vko'", "Xv'w'hk'hxyxyz", "Wxqv'hv'k", "Wh'k", "Xbx'qk'vz"]
|
||||
```
|
||||
|
||||
### Multi-Word Fantasy Names
|
||||
|
||||
The `fantasy_name` function will only generate one name-word at a time, so for multi-word names
|
||||
you'll need to combine pieces together. Depending on what kind of end result you want, there are
|
||||
several approaches.
|
||||
|
||||
|
||||
#### The simple approach
|
||||
|
||||
If all you need is for it to have multiple parts, you can generate multiple names at once and `join` them.
|
||||
|
||||
```py
|
||||
>>> name = " ".join(namegen.fantasy_name(num=2))
|
||||
>>> name
|
||||
'Dezhvözh Khäk'
|
||||
```
|
||||
|
||||
If you want a little more variation between first/last names, you can also generate names for
|
||||
different styles and then combine them.
|
||||
|
||||
```py
|
||||
>>> first = namegen.fantasy_name(style="fluid")
|
||||
>>> last = namegen.fantasy_name(style="harsh")
|
||||
>>> name = f"{first} {last}"
|
||||
>>> name
|
||||
'Ofasa Käkudhu'
|
||||
```
|
||||
|
||||
#### "Nakku Silversmith"
|
||||
|
||||
One common fantasy name practice is profession- or title-based surnames. To achieve this effect,
|
||||
you can use the `last_name` function with a custom list of last names and combine it with your generated
|
||||
fantasy name.
|
||||
|
||||
Example:
|
||||
```py
|
||||
NAMEGEN_LAST_NAMES = [ "Silversmith", "the Traveller", "Destroyer of Worlds" ]
|
||||
NAMEGEN_REPLACE_LISTS = True
|
||||
|
||||
>>> first = namegen.fantasy_name()
|
||||
>>> last = namegen.last_name()
|
||||
>>> name = f"{first} {last}"
|
||||
>>> name
|
||||
'Tözhkheko the Traveller'
|
||||
```
|
||||
|
||||
#### Elarion d'Yrinea, Thror Obinson
|
||||
|
||||
Another common flavor of fantasy names is to use a surname suffix or prefix. For that, you'll
|
||||
need to add in the extra bit yourself.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
>>> names = namegen.fantasy_name(num=2)
|
||||
>>> name = f"{names[0]} za'{names[1]}"
|
||||
>>> name
|
||||
"Tithe za'Dhudozkok"
|
||||
|
||||
>>> names = namegen.fantasy_name(num=2)
|
||||
>>> name = f"{names[0]} {names[1]}son"
|
||||
>>> name
|
||||
'Kön Ködhöddoson'
|
||||
```
|
||||
|
||||
|
||||
### Custom Fantasy Name style rules
|
||||
|
||||
The style rules are contained in a dictionary of dictionaries, where the style name
|
||||
is the key and the style rules are the dictionary value.
|
||||
|
||||
The following is how you would add a custom style to `settings.py`:
|
||||
```py
|
||||
NAMEGEN_FANTASY_RULES = {
|
||||
"example_style": {
|
||||
"syllable": "(C)VC",
|
||||
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||
"start": ['m'],
|
||||
"end": ['x','n'],
|
||||
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||
"length": (2,4),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then you could generate names following that ruleset with `namegen.fantasy_name(style="example_style")`.
|
||||
|
||||
The keys `syllable`, `consonants`, `vowels`, and `length` must be present, and `length` must be the minimum and maximum syllable counts. `start` and `end` are optional.
|
||||
|
||||
|
||||
#### syllable
|
||||
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel,
|
||||
and parentheses mean it's optional. So, the example `(C)VC` means that every syllable
|
||||
will always have a vowel followed by a consonant, and will *sometimes* have another
|
||||
consonant at the beginning. e.g. `en`, `bak`
|
||||
|
||||
*Note:* While it's not standard, the contrib lets you nest parentheses, with each layer
|
||||
being less likely to show up. Additionally, any other characters put into the syllable
|
||||
structure - e.g. an apostrophe - will be read and inserted as written. The
|
||||
"alien" style rules in the module gives an example of both: the syllable structure is `C(C(V))(')(C)`
|
||||
which results in syllables such as `khq`, `xho'q`, and `q'` with a much lower frequency of vowels than
|
||||
`C(C)(V)(')(C)` would have given.
|
||||
|
||||
#### consonants
|
||||
A simple list of consonant phonemes that can be chosen from. Multi-character strings are
|
||||
perfectly acceptable, such as "th", but each one will be treated as a single consonant.
|
||||
|
||||
The function uses a naive form of weighting, where you make a phoneme more likely to
|
||||
occur by putting more copies of it into the list.
|
||||
|
||||
#### start and end
|
||||
These are **optional** lists for the first and last letters of a syllable, if they're
|
||||
a consonant. You can add on additional consonants which can only occur at the beginning
|
||||
or end of a syllable, or you can add extra copies of already-defined consonants to
|
||||
increase the frequency of them at the start/end of syllables.
|
||||
|
||||
For example, in the `example_style` above, we have a `start` of m, and `end` of x and n.
|
||||
Taken with the rest of the consonants/vowels, this means you can have the syllables of `mez`
|
||||
but not `zem`, and you can have `phex` or `phen` but not `xeph` or `neph`.
|
||||
|
||||
They can be left out of custom rulesets entirely.
|
||||
|
||||
#### vowels
|
||||
Vowels is a simple list of vowel phonemes - exactly like consonants, but instead used for the
|
||||
vowel selection. Single-or multi-character strings are equally fine. It uses the same naive weighting system
|
||||
as consonants - you can increase the frequency of any given vowel by putting it into the list multiple times.
|
||||
|
||||
#### length
|
||||
A tuple with the minimum and maximum number of syllables a name can have.
|
||||
|
||||
When setting this, keep in mind how long your syllables can get! 4 syllables might
|
||||
not seem like very many, but if you have a (C)(V)VC structure with one- and
|
||||
two-letter phonemes, you can get up to eight characters per syllable.
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/utils/name_generator/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -458,6 +458,7 @@ and rule implementation like character traits, dice rolling and emoting._
|
|||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Contrib-Buffs.md
|
||||
Contrib-Dice.md
|
||||
Contrib-Health-Bar.md
|
||||
Contrib-RPSystem.md
|
||||
|
|
@ -465,6 +466,17 @@ Contrib-Traits.md
|
|||
```
|
||||
|
||||
|
||||
### Contrib: `buffs`
|
||||
|
||||
_Contribution by Tegiminis 2022_
|
||||
|
||||
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
||||
It is a common design pattern in RPGs, particularly action games.
|
||||
|
||||
[Read the documentation](./Contrib-Buffs.md) - [Browse the Code](evennia.contrib.rpg.buffs)
|
||||
|
||||
|
||||
|
||||
### Contrib: `dice`
|
||||
|
||||
_Contribution by Griatch, 2012_
|
||||
|
|
@ -643,6 +655,7 @@ and more._
|
|||
|
||||
Contrib-Auditing.md
|
||||
Contrib-Fieldfill.md
|
||||
Contrib-Name-Generator.md
|
||||
Contrib-Random-String-Generator.md
|
||||
Contrib-Tree-Select.md
|
||||
```
|
||||
|
|
@ -676,6 +689,19 @@ to any callable of your choice.
|
|||
|
||||
|
||||
|
||||
### Contrib: `name_generator`
|
||||
|
||||
_Contribution by InspectorCaracal (2022)_
|
||||
|
||||
A module for generating random names, both real-world and fantasy. Real-world
|
||||
names can be generated either as first (personal) names, family (last) names, or
|
||||
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
[Read the documentation](./Contrib-Name-Generator.md) - [Browse the Code](evennia.contrib.utils.name_generator)
|
||||
|
||||
|
||||
|
||||
### Contrib: `random_string_generator`
|
||||
|
||||
_Contribution by Vincent Le Goff (vlgeoff), 2017_
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
[prev lesson](../Unimplemented.md) | [next lesson](../Unimplemented.md)
|
||||
|
||||
# Making a sittable object
|
||||
|
||||
|
|
@ -524,7 +524,7 @@ class CmdStand(Command):
|
|||
# ...
|
||||
```
|
||||
|
||||
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
|
||||
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.
|
||||
|
||||
|
|
@ -753,7 +753,7 @@ class CmdStand2(Command):
|
|||
```
|
||||
|
||||
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
|
||||
more complex we would likely need to break out a [Django query](Beginner-Tutorial/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.
|
||||
|
|
@ -799,4 +799,4 @@ Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (
|
|||
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)
|
||||
[prev lesson](../Unimplemented.md) | [next lesson](../Unimplemented.md)
|
||||
|
|
@ -17,47 +17,52 @@
|
|||
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 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 _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
|
||||
|
||||
```
|
||||
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)
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rule-System
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tuturial-Objects
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-Based-Combat-System
|
||||
A-Sittable-Object
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rule-System
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tuturial-Objects
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
# 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"
|
||||
HP = "hp"
|
||||
LEVEL = "level"
|
||||
XP = "xp"
|
||||
|
||||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
ALLEGIANCE_HOSTILE = "hostile"
|
||||
ALLEGIANCE_NEUTRAL = "neutral"
|
||||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
class WieldLocation(Enum):
|
||||
"""
|
||||
Wield (or wear) locations.
|
||||
|
||||
"""
|
||||
|
||||
# wield/wear location
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
|
||||
class ObjType(Enum):
|
||||
"""
|
||||
Object types.
|
||||
|
||||
"""
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Here the `Ability` class holds basic properties of a character sheet, while `WieldLocation` tracks
|
||||
equipment and where a character would wield and wear things - since _Knave_ has these, it makes sense
|
||||
to track it. Finally we have a set of different `ObjType`s, for differentiate game items. These are
|
||||
extracted by reading the _Knave_ object lists and figuring out how they should be categorized.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
> 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/evadventure/tests/test_utils.py](evennia.contrib.evadventure.tests.test_utils)
|
||||
is the final test 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!
|
||||
|
|
@ -45,12 +45,12 @@ makes it easier to change and update things in one place later.
|
|||
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
|
||||
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
|
||||
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
|
||||
|
|
@ -413,4 +413,4 @@ easily new game defining features can be added to Evennia.
|
|||
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
|
||||
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
|
||||
[implement a combat system](Beginner-Tutorial/Part3/Turn-based-Combat-System.md).
|
||||
[implement a combat system](./Turn-based-Combat-System.md).
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ allows for emoting as part of combat which is an advantage for roleplay-heavy ga
|
|||
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
|
||||
[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.
|
||||
|
||||
|
|
@ -61,22 +61,22 @@ reported. A new turn then begins.
|
|||
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
- 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 _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
|
||||
|
|
@ -89,7 +89,7 @@ don't use this very much here this might allow the combat commands on the charac
|
|||
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
|
||||
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._
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ class CmdAttack(Command):
|
|||
```
|
||||
|
||||
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.
|
||||
the [Adding Command Tutorial](Beginner-Tutorial/Part1/Adding-Commands.md) if you are unsure about how to do this.
|
||||
|
||||
## Expanding the example
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ class QuestHandler:
|
|||
|
||||
```
|
||||
|
||||
The handler is just a normal Python class and has no database-storage on its own. But it has a link to `.obj`, which is assumed to be a full typeclased entity, on which we can create persistent [Attributes](Attributes) to store things however we like!
|
||||
The handler is just a normal Python class and has no database-storage on its own. But it has a link to `.obj`, which is assumed to be a full typeclased entity, on which we can create persistent [Attributes](Components/Attributes.md) to store things however we like!
|
||||
|
||||
We make two helper methods `_load` and
|
||||
`_save` that handles local fetches and saves `storage` to an Attribute on the object. To avoid saving more than necessary, we have a property `do_save`. This we will set in `Quest` below.
|
||||
|
|
@ -160,7 +160,7 @@ class Quest:
|
|||
|
||||
The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in `QuestHandler.add`. We want to monitor the changing of `current_step`, so we make it into a `property`. When we edit that value, we set the `do_save` flag on the handler, which means it will save the status to database once it has checked progress on all its quests.
|
||||
|
||||
The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed because `Attributes` can't store 'hidden' database objects (the `Quest.obj` property. The methods help Evennia serialize/deserialize `Quest` propertly when the handler saves it. For more information, see [Storing Single objects](Attributes#storing-single-objects) in the Attributes documentation.
|
||||
The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed because `Attributes` can't store 'hidden' database objects (the `Quest.obj` property. The methods help Evennia serialize/deserialize `Quest` propertly when the handler saves it. For more information, see [Storing Single objects](Components/Attributes.md#storing-single-objects) in the Attributes documentation.
|
||||
|
||||
### Tying it all together
|
||||
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ value - which may change as Evennia is developed. This way you can
|
|||
always be sure of what you have changed and what is default behaviour.
|
||||
|
||||
"""
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
######################################################################
|
||||
# Evennia base server config
|
||||
######################################################################
|
||||
|
|
@ -405,9 +405,11 @@ INITIAL_SETUP_MODULE = "evennia.server.initial_setup"
|
|||
# the server's initial setup sequence (the very first startup of the system).
|
||||
# The check will fail quietly if module doesn't exist or fails to load.
|
||||
AT_INITIAL_SETUP_HOOK_MODULE = "server.conf.at_initial_setup"
|
||||
# Module containing your custom at_server_start(), at_server_reload() and
|
||||
# at_server_stop() methods. These methods will be called every time
|
||||
# the server starts, reloads and resets/stops respectively.
|
||||
# Module(s) containing custom at_server_init(), at_server_start(),
|
||||
# at_server_reload() and at_server_stop() methods. These methods will be called
|
||||
# every time the server starts, reloads and resets/stops
|
||||
# respectively. Can be given as a single path or a list of paths. If a list,
|
||||
# each module's hooks will be called in list order.
|
||||
AT_SERVER_STARTSTOP_MODULE = "server.conf.at_server_startstop"
|
||||
# List of one or more module paths to modules containing a function start_
|
||||
# plugin_services(application). This module will be called with the main
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
evennia.contrib.tutorials.evadventure.tests.test\_utils module
|
||||
==============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.tests.test_utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -30,17 +30,8 @@ class Ability(Enum):
|
|||
WIS = "wisdom"
|
||||
CHA = "charisma"
|
||||
|
||||
STR_DEFENSE = "strength_defense"
|
||||
DEX_DEFENSE = "dexterity_defense"
|
||||
CON_DEFENSE = "constitution_defense"
|
||||
INT_DEFENSE = "intelligence_defense"
|
||||
WIS_DEFENSE = "wisdom_defense"
|
||||
CHA_DEFENSE = "charisma_defense"
|
||||
|
||||
ARMOR = "armor"
|
||||
HP = "hp"
|
||||
EXPLORATION_SPEED = "exploration_speed"
|
||||
COMBAT_SPEED = "combat_speed"
|
||||
LEVEL = "level"
|
||||
XP = "xp"
|
||||
|
||||
|
|
@ -66,10 +57,6 @@ class WieldLocation(Enum):
|
|||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
# combat-related
|
||||
OPTIMAL_DISTANCE = "optimal_distance"
|
||||
SUBOPTIMAL_DISTANCE = "suboptimal_distance"
|
||||
|
||||
|
||||
class ObjType(Enum):
|
||||
"""
|
||||
|
|
|
|||
34
evennia/contrib/tutorials/evadventure/tests/test_utils.py
Normal file
34
evennia/contrib/tutorials/evadventure/tests/test_utils.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""
|
||||
Tests of the utils module.
|
||||
|
||||
"""
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from .. import utils
|
||||
from ..objects import EvAdventureObject
|
||||
|
||||
|
||||
class TestUtils(BaseEvenniaTest):
|
||||
def test_get_obj_stats(self):
|
||||
|
||||
obj = create.create_object(
|
||||
EvAdventureObject, key="testobj", attributes=(("desc", "A test object"),)
|
||||
)
|
||||
result = utils.get_obj_stats(obj)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
"""
|
||||
|ctestobj|n
|
||||
Value: ~|y0|n coins
|
||||
|
||||
A test object
|
||||
|
||||
Slots: |w1|n, Used from: |wbackpack|n
|
||||
Quality: |wN/A|n, Uses: |wuses|n
|
||||
Attacks using |wNo attack|n against |wNo defense|n
|
||||
Damage roll: |wNone|n
|
||||
""".strip(),
|
||||
)
|
||||
|
|
@ -4,14 +4,15 @@ Various utilities.
|
|||
"""
|
||||
|
||||
_OBJ_STATS = """
|
||||
|c{key}|n Value: approx. |y{value}|n coins {carried}
|
||||
|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_value}|n
|
||||
Damage roll: |w{damage_roll}""".strip()
|
||||
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):
|
||||
|
|
@ -31,7 +32,10 @@ def get_obj_stats(obj, owner=None):
|
|||
if owner:
|
||||
objmap = dict(owner.equipment.all())
|
||||
carried = objmap.get(obj)
|
||||
carried = f"Worn: [{carried.value}]" if carried else ""
|
||||
carried = f", Worn: [{carried.value}]" if carried else ""
|
||||
|
||||
attack_type = getattr(obj, "attack_type", None)
|
||||
defense_type = getattr(obj, "attack_type", None)
|
||||
|
||||
return _OBJ_STATS.format(
|
||||
key=obj.key,
|
||||
|
|
@ -39,10 +43,10 @@ def get_obj_stats(obj, owner=None):
|
|||
carried=carried,
|
||||
desc=obj.db.desc,
|
||||
size=obj.size,
|
||||
use_slot=obj.use_slot.value,
|
||||
quality=obj.quality,
|
||||
uses=obj.uses,
|
||||
attack_type=obj.attack_type.value,
|
||||
defense_type=obj.defense_type.value,
|
||||
damage_roll=obj.damage_roll,
|
||||
use_slot_name=obj.inventory_use_slot.value,
|
||||
quality=getattr(obj, "quality", "N/A"),
|
||||
uses=getattr(obj, "uses", "N/A"),
|
||||
attack_type_name=attack_type.value if attack_type else "No attack",
|
||||
defense_type_name=defense_type.value if defense_type else "No defense",
|
||||
damage_roll=getattr(obj, "damage_roll", "None"),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue