diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md
index 6feaed180f..103053b325 100644
--- a/docs/source/Coding/Changelog.md
+++ b/docs/source/Coding/Changelog.md
@@ -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
diff --git a/docs/source/Coding/Unit-Testing.md b/docs/source/Coding/Unit-Testing.md
index 8f35930bf3..d4b29f0efb 100644
--- a/docs/source/Coding/Unit-Testing.md
+++ b/docs/source/Coding/Unit-Testing.md
@@ -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.
diff --git a/docs/source/Contribs/Contrib-Buffs.md b/docs/source/Contribs/Contrib-Buffs.md
new file mode 100644
index 0000000000..1b09d19dd5
--- /dev/null
+++ b/docs/source/Contribs/Contrib-Buffs.md
@@ -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.
+
+
+----
+
+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.
diff --git a/docs/source/Contribs/Contrib-Name-Generator.md b/docs/source/Contribs/Contrib-Name-Generator.md
new file mode 100644
index 0000000000..8772cdd537
--- /dev/null
+++ b/docs/source/Contribs/Contrib-Name-Generator.md
@@ -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.
+
+----
+
+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.
diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md
index 5b004972c3..6ff630393b 100644
--- a/docs/source/Contribs/Contribs-Overview.md
+++ b/docs/source/Contribs/Contribs-Overview.md
@@ -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_
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md b/docs/source/Howtos/A-Sittable-Object.md
similarity index 98%
rename from docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
rename to docs/source/Howtos/A-Sittable-Object.md
index b92e6e75ba..5dfc9d081c 100644
--- a/docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
+++ b/docs/source/Howtos/A-Sittable-Object.md
@@ -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)
diff --git a/evennia/contrib/tutorials/evadventure/combat_twitch.py b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md
similarity index 100%
rename from evennia/contrib/tutorials/evadventure/combat_twitch.py
rename to docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Characters.md
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Commands.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Commands.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-NPCs.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-NPCs.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Objects.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Intro.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Intro.md
index 9e22e70e2d..1b9f63acd1 100644
--- a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Intro.md
+++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Intro.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
```
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rooms.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rule-System.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Rule-System.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Shops.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Shops.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Turnbased-Combat b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Turnbased-Combat
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md
new file mode 100644
index 0000000000..bab946e25e
--- /dev/null
+++ b/docs/source/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Utilities.md
@@ -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: `.
+
+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!
\ No newline at end of file
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Implementing-a-game-rule-system.md b/docs/source/Howtos/Implementing-a-game-rule-system.md
similarity index 98%
rename from docs/source/Howtos/Beginner-Tutorial/Part3/Implementing-a-game-rule-system.md
rename to docs/source/Howtos/Implementing-a-game-rule-system.md
index 85e752d974..cc197b9be4 100644
--- a/docs/source/Howtos/Beginner-Tutorial/Part3/Implementing-a-game-rule-system.md
+++ b/docs/source/Howtos/Implementing-a-game-rule-system.md
@@ -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
diff --git a/docs/source/Howtos/Static-In-Game-Map.md b/docs/source/Howtos/Static-In-Game-Map.md
index ce229b4699..bdf5b77d42 100644
--- a/docs/source/Howtos/Static-In-Game-Map.md
+++ b/docs/source/Howtos/Static-In-Game-Map.md
@@ -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).
diff --git a/docs/source/Howtos/Beginner-Tutorial/Part3/Turn-based-Combat-System.md b/docs/source/Howtos/Turn-based-Combat-System.md
similarity index 96%
rename from docs/source/Howtos/Beginner-Tutorial/Part3/Turn-based-Combat-System.md
rename to docs/source/Howtos/Turn-based-Combat-System.md
index b00459a784..08a20ddcac 100644
--- a/docs/source/Howtos/Beginner-Tutorial/Part3/Turn-based-Combat-System.md
+++ b/docs/source/Howtos/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
diff --git a/docs/source/Persistent-Handler.md b/docs/source/Persistent-Handler.md
index d4838b0881..e95deabad0 100644
--- a/docs/source/Persistent-Handler.md
+++ b/docs/source/Persistent-Handler.md
@@ -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
diff --git a/docs/source/Setup/Settings-Default.md b/docs/source/Setup/Settings-Default.md
index 5567ce2052..baa2ee3c9e 100644
--- a/docs/source/Setup/Settings-Default.md
+++ b/docs/source/Setup/Settings-Default.md
@@ -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
diff --git a/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_utils.rst b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_utils.rst
new file mode 100644
index 0000000000..39d3aba676
--- /dev/null
+++ b/docs/source/api/evennia.contrib.tutorials.evadventure.tests.test_utils.rst
@@ -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:
diff --git a/evennia/contrib/tutorials/evadventure/enums.py b/evennia/contrib/tutorials/evadventure/enums.py
index 9253c79976..59c24163a0 100644
--- a/evennia/contrib/tutorials/evadventure/enums.py
+++ b/evennia/contrib/tutorials/evadventure/enums.py
@@ -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):
"""
diff --git a/evennia/contrib/tutorials/evadventure/tests/test_utils.py b/evennia/contrib/tutorials/evadventure/tests/test_utils.py
new file mode 100644
index 0000000000..192e6b44f6
--- /dev/null
+++ b/evennia/contrib/tutorials/evadventure/tests/test_utils.py
@@ -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(),
+ )
diff --git a/evennia/contrib/tutorials/evadventure/utils.py b/evennia/contrib/tutorials/evadventure/utils.py
index 1e9b6d9127..d39e786e7d 100644
--- a/evennia/contrib/tutorials/evadventure/utils.py
+++ b/evennia/contrib/tutorials/evadventure/utils.py
@@ -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"),
)