Add documentation for new crafting contrib

This commit is contained in:
Griatch 2020-11-28 22:58:07 +01:00
parent e890bd9040
commit 87c43ccce0
32 changed files with 765 additions and 257 deletions

View file

@ -24,10 +24,12 @@
- Make IP throttle use Django-based cache system for optional persistence (PR by strikaco)
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
"TutorialWeaponRack" to prevent collisions with classes in mygame
- New `crafting` contrib, adding a full crafting subsystem (Griatch 2020)
### Evennia 0.9.5
### Evennia 0.9.5 (2019-2020)
A transitional release, including new doc system
Released 2020-11-14.
A transitional release, including new doc system.
- `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `py` command now reroutes stdout to output results in-game client. `py`

View file

@ -46,7 +46,7 @@ URL_REMAPS = {
"Starting/Adding-Command-Tutorial": "Adding-Commands",
"Adding-Command-Tutorial": "Adding-Commands",
"CmdSet": "Command-Sets",
"Spawner": "Spawner-and-Prototypes",
"Spawner": "Prototypes",
"issue": "github:issue",
"issues": "github:issue",
"bug": "github:issue",

View file

@ -15,7 +15,7 @@ than, the doc-strings of each component in the [API](../Evennia-API).
- [Attributes](./Attributes)
- [Nicks](./Nicks)
- [Tags](./Tags)
- [Spawner and prototypes](./Spawner-and-Prototypes)
- [Spawner and prototypes](./Prototypes)
- [Help entries](./Help-System)
## Commands

View file

@ -53,7 +53,7 @@ like [Scripts](./Scripts)).
This particular Rose class doesn't really do much, all it does it make sure the attribute
`desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you
will usually want to change this at build time (using the `@desc` command or using the
[Spawner](./Spawner-and-Prototypes)). The `Object` typeclass offers many more hooks that is available
[Spawner](./Prototypes)). The `Object` typeclass offers many more hooks that is available
to use though - see next section.
## Properties and functions on Objects

View file

@ -119,7 +119,7 @@ Deprecated as of Evennia 0.8:
- `ndb_<name>` - sets the value of a non-persistent attribute (`"ndb_"` is stripped from the name).
This is simply not useful in a prototype and is deprecated.
- `exec` - This accepts a code snippet or a list of code snippets to run. This should not be used -
use callables or [$protfuncs](./Spawner-and-Prototypes#protfuncs) instead (see below).
use callables or [$protfuncs](./Prototypes#protfuncs) instead (see below).
### Prototype values

View file

@ -3,20 +3,52 @@
The [evennia/contrib/](api:evennia.contrib) folder holds Game-specific tools, systems and utilities created by the community. This gathers
longer-form documentation associated with particular contribs.
## Crafting
A full, extendable crafting system.
- [Crafting overview](./Crafting)
- [Crafting API documentation](api:evennia.contrib.crafting.crafting)
- [Example of a sword crafting tree](api:evennia.contrib.crafting.example_recipes)
## In-Game-Python
Allow Builders to add Python-scripted events to their objects (OBS-not for untrusted users!)
- [A voice-operated elevator using events](./A-voice-operated-elevator-using-events)
- [Dialogues using events](./Dialogues-in-events)
## Maps
Solutions for generating and displaying maps in-game.
- [Dynamic in-game map](./Dynamic-In-Game-Map)
- [Static in-game map](./Static-In-Game-Map)
## The tutorial-world
The Evennia single-player sole quest. Made to be analyzed to learn.
- [The tutorial world introduction](../Howto/Starting/Part1/Tutorial-World-Introduction)
## Menu-builder
A tool for building using an in-game menu instead of the normal build commands. Meant to
be expanded for the needs of your game.
- [Building Menus](./Building-menus)
```toctree::
:hidden:
./Crafting
../api/evennia.contrib.crafting.crafting
../api/evennia.contrib.crafting.example_recipes
./A-voice-operated-elevator-using-events
./Dialogues-in-events
./Dynamic-In-Game-Map
./Static-In-Game-Map
../Howto/Starting/Part1/Tutorial-World-Introduction
./Building-menus
```

View file

@ -0,0 +1,214 @@
# Crafting system contrib
_Contrib by Griatch 2020_
```versionadded:: 1.0
```
This contrib implements a full Crafting system that can be expanded and modified to fit your game.
- See the [evennia/contrib/crafting/crafting.py API](api:evennia.contrib.crafting.crafting) for installation
instructrions.
- See the [sword example](api:evennia.contrib.crafting.example_recipes) for an example of how to design
a crafting tree for crafting a sword from base elements.
From in-game it uses the new `craft` command:
```bash
> craft bread from flour, eggs, salt, water, yeast using oven, roller
> craft bandage from cloth using scissors
```
The syntax is `craft <recipe> [from <ingredient>,...][ using <tool>,...]`.
The above example uses the `bread` *recipe* and requires `flour`, `eggs`, `salt`, `water` and `yeast` objects
to be in your inventory. These will be consumed as part of crafting (baking) the bread.
The `oven` and `roller` are "tools" that can be either in your inventory or in your current location (you are not carrying an oven
around with you after all). Tools are *not* consumed in the crafting. If the added ingredients/tools matches
the requirements of the recipe, a new `bread` object will appear in the crafter's inventory.
If you wanted, you could also picture recipes without any consumables:
```
> craft fireball using wand, spellbook
```
With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or
magic systems.
In code, you can craft using the `evennia.contrib.crafting.crafting.craft` function:
```python
from evennia.contrib.crafting.crafting import craft
result = craft(caller, *inputs)
```
Here, `caller` is the one doing the crafting and `*inputs` is any combination of consumables and/or tool
Objects. The system will identify which is which by the [Tags](../Components/Tags) on them (see below)
The `result` is always a list.
## Adding new recipes
A *recipe* is a class inheriting from `evennia.contrib.crafting.crafting.CraftingRecipe`. This class
implements the most common form of crafting - that using in-game objects. Each recipe is a separate class
which gets initialized with the consumables/tools you provide.
For the `craft` command to find your custom recipes, you need to tell Evennia where they are. Add a new
line to your `mygame/server/conf/settings.py` file, with a list to any new modules with recipe classes.
```python
CRAFT_RECIPE_MODULES = ["world.myrecipes"]
```
(You need to reload after adding this). All global-level classes in these modules (whose names don't start
with underscore) are considered by the system as viable recipes.
Here we assume you created `mygame/world/myrecipes.py` to match the above example setting:
```python
# in mygame/world/myrecipes.py
from evennia.contrib.crafting.crafting import CraftingRecipe
class WoodenPuppetRecipe(CraftingRecipe):
"""A puppet""""
name = "wooden puppet" # name to refer to this recipe as
tool_tags = ["knife"]
consumable_tags = ["wood"]
output_prototypes = [
{"key": "A carved wooden doll",
"typeclass": "typeclasses.objects.decorations.Toys",
"desc": "A small carved doll",
]
```
This specifies what tags to look for in the inputs and a [Prototype](../Components/Prototypes) to spawn
the result on the fly (a recipe could spawn more than one result if needed). Instead of specifying
the full prototype-dict, you could also just provide a list of `prototype_key`s to existing
prototypes you have.
After reloading the server, this recipe would now be available to use. To try it we should
create materials and tools the recipe understands.
The recipe looks only for the [Tag](../Components/Tags) of the ingredients. The tag-category used
can be set per-recipe using the (`.consumable_tag_category` and
`.tool_tag_category` respectively). The defaults are `crafting_material` and `crafting_tool`. For
the puppet we need one object with the `wood` tag and another with the `knife` tag:
```python
from evennia import create_object
knife = create_object(key="Hobby knife", tags=[("knife", "crafting_tool")])
wood = create_object(key="Piece of wood", tags[("wood", "crafting_material")])
```
Note that the objects can have any name, all that matters is the tag/tag-category. This means if a
"bayonet" also had the "knife" crafting tag, it could also be used to carve a puppet. This is also
potentially interesting for use in puzzles and to allow users to experiment and find alternatives to
know ingredients.
Assuming these objects were put in our inventory, we could now craft using the in-game command:
```bash
> craft wooden puppet from wood using hobby knife
```
In code we would do
```python
from evennia.contrub.crafting.crafting import craft
puppet = craft(crafter, "wooden puppet", knife, wood)
```
In the call to `craft`, the order of `knife` and `wood` doesn't matter - the recipe will sort out which
is which based on their tags.
## Deeper customization of recipes
To understand how to customize recipes further, it helps to understand how they are used directly:
```python
class MyRecipe(CraftingRecipe):
...
# convenient helper to get dummy objects with the right tags
tools, consumables = MyRecipe.seed()
recipe = MyRecipe(crafter, *(tools + consumables))
result = recipe.craft()
```
This is useful for testing and allows you to use the class directly without adding it to a module
in `settings.CRAFTING_RECIPE_MODULES`. The `seed` class method is useful e.g. for making unit tests.
Even without modifying more than the class properties, there are a lot of options to set on
the `CraftingRecipe` class. Easiest is to refer to the
[CraftingRecipe api documentation](evennia.contrib.crafting.crafting.html#evennia.contrib.crafting.crafting.CraftingRecipe).
For example, you can customize the validation-error messages, decide if the ingredients have
to be exactly right, if a failure still consumes the ingredients or not, and much more.
For even more control you can override hooks in your own class:
- `pre_craft` - this should handle input validation and store its data in `.validated_consumables` and
`validated_tools` respectively. On error, this reports the error to the crafter and raises the
`CraftingValidationError`.
- `do_craft` - this will only be called if `pre_craft` finished without an exception. This should
return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting
fails for some reason. This is the place to add skill-checks or random chance if you need it
for your game.
- `post_craft` - this receives the result from `do_craft` and handles error messages and also deletes
any consumables as needed. It may also modify the result before returning it.
- `msg` - this is a wrapper for `self.crafter.msg` and should be used to send messages to the
crafter. Centralizing this means you can also easily modify the sending style in one place later.
The class constructor (and the `craft` access function) takes optional `**kwargs`. These are passed
into each crafting hook. These are unused by default but could be used to customize things per-call.
### Skilled crafters
What the crafting system does not have out of the box is a 'skill' system - the notion of being able
to fail the craft if you are not skilled enough. Just how skills work is game-dependent, so to add
this you need to make your own recipe parent class and have your recipes inherit from this.
```python
from random import randint
from evennia.contrib.crafting.crafting import CraftingRecipe
class SkillRecipe(CraftingRecipe):
"""A recipe that considers skill"""
difficulty = 20
def do_craft(self, **kwargs):
"""The input is ok. Determine if crafting succeeds"""
# this is set at initialization
crafter = self.crafte
# let's assume the skill is stored directly on the crafter
# - the skill is 0..100.
crafting_skill = crafter.db.skill_crafting
# roll for success:
if randint(1, 100) <= (crafting_skill - self.difficulty):
# all is good, craft away
return super().do_craft()
else:
self.msg("You are not good enough to craft this. Better luck next time!")
return []
```
In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see
if we succed. We would of course make this a lot more immersive and detailed in a full game. In
principle you could customize each recipe just the way you want it, but you could also inherit from
a central parent like this to cut down on work.
The [sword recipe example module](api:evennia.contrib.crafting.example_recipes) also shows an example
of a random skill-check being implemented in a parent and then inherited for multiple use.
## Even more customization
The base class `evennia.contrib.crafting.crafting.CraftingRecipeBase` implements just the minimum
needed to be a recipe. It doesn't know about Objects or tags. If you want to adopt the crafting system
for something entirely different (maybe using different input or validation logic), starting from this
may be cleaner than overriding things in the more opinionated `CraftingRecipe`.

View file

@ -70,7 +70,7 @@ The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__
- [evennia.gametime](api:evennia.utils.gametime) - server run- and game time ([docs](Components/Coding-Utils#gametime))
- [evennia.logger](api:evennia.utils.logger) - logging tools
- [evennia.ansi](api:evennia.utils.ansi) - ansi coloring tools
- [evennia.spawn](api:evennia.prototypes.spawner#evennia.prototypes.spawner.Spawn) - spawn/prototype system ([docs](Components/Spawner-and-Prototypes))
- [evennia.spawn](api:evennia.prototypes.spawner#evennia.prototypes.spawner.Spawn) - spawn/prototype system ([docs](Components/Prototypes))
- [evennia.lockfuncs](api:evennia.locks.lockfuncs) - default lock functions for access control ([docs](Components/Locks))
- [evennia.EvMenu](api:evennia.utils.evmenu#evennia.utils.evmenu.EvMenu) - menu system ([docs](Components/EvMenu))
- [evennia.EvTable](api:evennia.utils.evtable#evennia.utils.evtable.EvTable) - text table creater

View file

@ -62,7 +62,7 @@ from here to `mygame/server/settings.py` file.
- `locale/` - Language files ([i18n](../../../Concepts/Internationalization)).
- [`locks/`](../../../Components/Locks) - Lock system for restricting access to in-game entities.
- [`objects/`](../../../Components/Objects) - In-game entities (all types of items and Characters).
- [`prototypes/`](../../../Components/Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
- [`prototypes/`](../../../Components/Prototypes) - Object Prototype/spawning system and OLC menu
- [`accounts/`](../../../Components/Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](../../../Components/Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](../../../Components/Portal-And-Server) - Core server code and Session handling.

View file

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

View file

@ -0,0 +1,7 @@
evennia.contrib.crafting.crafting
========================================
.. automodule:: evennia.contrib.crafting.crafting
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.crafting.example\_recipes
================================================
.. automodule:: evennia.contrib.crafting.example_recipes
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,16 @@
evennia.contrib.crafting
================================
.. automodule:: evennia.contrib.crafting
:members:
:undoc-members:
:show-inheritance:
.. toctree::
:maxdepth: 6
evennia.contrib.crafting.crafting
evennia.contrib.crafting.example_recipes
evennia.contrib.crafting.tests

View file

@ -0,0 +1,7 @@
evennia.contrib.crafting.tests
=====================================
.. automodule:: evennia.contrib.crafting.tests
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.commands
===========================================
.. automodule:: evennia.contrib.evscaperoom.commands
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.menu
=======================================
.. automodule:: evennia.contrib.evscaperoom.menu
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.objects
==========================================
.. automodule:: evennia.contrib.evscaperoom.objects
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.room
=======================================
.. automodule:: evennia.contrib.evscaperoom.room
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,21 @@
evennia.contrib.evscaperoom
===================================
.. automodule:: evennia.contrib.evscaperoom
:members:
:undoc-members:
:show-inheritance:
.. toctree::
:maxdepth: 6
evennia.contrib.evscaperoom.commands
evennia.contrib.evscaperoom.menu
evennia.contrib.evscaperoom.objects
evennia.contrib.evscaperoom.room
evennia.contrib.evscaperoom.scripts
evennia.contrib.evscaperoom.state
evennia.contrib.evscaperoom.tests
evennia.contrib.evscaperoom.utils

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.scripts
==========================================
.. automodule:: evennia.contrib.evscaperoom.scripts
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.state
========================================
.. automodule:: evennia.contrib.evscaperoom.state
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.tests
========================================
.. automodule:: evennia.contrib.evscaperoom.tests
:members:
:undoc-members:
:show-inheritance:

View file

@ -0,0 +1,7 @@
evennia.contrib.evscaperoom.utils
========================================
.. automodule:: evennia.contrib.evscaperoom.utils
:members:
:undoc-members:
:show-inheritance:

View file

@ -45,6 +45,8 @@ evennia.contrib
:maxdepth: 6
evennia.contrib.awsstorage
evennia.contrib.crafting
evennia.contrib.evscaperoom
evennia.contrib.ingame_python
evennia.contrib.security
evennia.contrib.turnbattle

View file

@ -37,12 +37,12 @@
- [Components/Objects](Components/Objects)
- [Components/Outputfuncs](Components/Outputfuncs)
- [Components/Portal And Server](Components/Portal-And-Server)
- [Components/Prototypes](Components/Prototypes)
- [Components/Scripts](Components/Scripts)
- [Components/Server](Components/Server)
- [Components/Server Conf](Components/Server-Conf)
- [Components/Sessions](Components/Sessions)
- [Components/Signals](Components/Signals)
- [Components/Spawner and Prototypes](Components/Spawner-and-Prototypes)
- [Components/Tags](Components/Tags)
- [Components/TickerHandler](Components/TickerHandler)
- [Components/Typeclasses](Components/Typeclasses)
@ -70,6 +70,7 @@
- [Contribs/Arxcode installing help](Contribs/Arxcode-installing-help)
- [Contribs/Building menus](Contribs/Building-menus)
- [Contribs/Contrib Overview](Contribs/Contrib-Overview)
- [Contribs/Crafting](Contribs/Crafting)
- [Contribs/Dialogues in events](Contribs/Dialogues-in-events)
- [Contribs/Dynamic In Game Map](Contribs/Dynamic-In-Game-Map)
- [Contribs/Static In Game Map](Contribs/Static-In-Game-Map)

View file

@ -1 +1 @@
0.9.0
1.0-dev

View file

View file

@ -2,69 +2,88 @@
Crafting - Griatch 2020
This is a general crafting engine. The basic functionality of crafting is to
combine any number of of items in a 'recipe' to produce a new result. This is
useful not only for traditional crafting but also for puzzle-solving or
similar.
combine any number of of items or tools in a 'recipe' to produce a new result.
item + item + item + tool + tool -> recipe -> new result
This is useful not only for traditional crafting but the engine is flexible
enough to also be useful for puzzles or similar.
## Installation
- Add the `CmdCraft` Command from this module to your default cmdset. This
allows for crafting from in-game using a simple syntax.
- Create a new module and add it to a new list in your settings file
(`server/conf/settings.py`) named `CRAFT_MODULE_RECIPES`.
- In the new module, create one or more classes, each a child of
(`server/conf/settings.py`) named `CRAFT_RECIPES_MODULES`, such as
`CRAFT_RECIPE_MODULES = ["world.recipes_weapons"]`.
- In the new module(s), create one or more classes, each a child of
`CraftingRecipe` from this module. Each such class must have a unique `.name`
property. It also defines what inputs are required and what is created using
this recipe.
- Objects to use for crafting should (by default) be tagged with tags using the
tag-category `crafting_material`. The name of the object doesn't matter, only
its tag.
- Add the `CmdCraft` command from this module to your default cmdset. This is a
very simple example-command (your real command will most likely need to do
skill-checks etc!).
tag-category `crafting_material` or `crafting_tool`. The name of the object
doesn't matter, only its tag.
## Usage
## Crafting in game
By default the crafter needs to specify which components
should be used for the recipe:
The default `craft` command handles all crafting needs.
::
craft spiked club from club, nails
> craft spiked club from club, nails
Here, `spiked club` specifies the recipe while `club` and `nails` are objects
the crafter must have in their inventory. These will be consumed during
crafting (by default only if crafting was successful).
A recipe can also require _tools_. These must be either in inventory or in
the current location. Tools are not consumed during the crafting.
A recipe can also require *tools* (like the `hammer` above). These must be
either in inventory *or* be in the current location. Tools are *not* consumed
during the crafting process.
::
craft wooden doll from wood with knife
> craft wooden doll from wood with knife
## Crafting in code
In code, you should use the helper function `craft` from this module. This
specifies the name of the recipe to use and expects all suitable
ingredients/tools as arguments (consumables and tools should be added together,
tools will be identified before consumables).
spiked_club = craft(crafter, "spiked club", club, nails)
```python
A fail leads to an empty return. The crafter should already have been notified
of any error in this case (this should be handle by the recipe itself).
from evennia.contrib.crafting import crafting
spiked_club = crafting.craft(crafter, "spiked club", club, nails)
```
The result is always a list with zero or more objects. A fail leads to an empty
list. The crafter should already have been notified of any error in this case
(this should be handle by the recipe itself).
## Recipes
A _recipe_ works like an input/output blackbox: you put consumables (and/or
tools) into it and if they match the recipe, a new result is spit out.
Consumables are consumed in the process while tools are not.
A *recipe* is a class that works like an input/output blackbox: you initialize
it with consumables (and/or tools) if they match the recipe, a new
result is spit out. Consumables are consumed in the process while tools are not.
This module contains a base class for making new ingredient types
(`CraftingRecipeBase`) and an implementation of the most common form of
crafting (`CraftingRecipe`) using objects and prototypes.
Recipes are put in one or more modules added as a list to the
`CRAFT_MODULE_RECIPES` setting, for example:
`CRAFT_RECIPE_MODULES` setting, for example:
CRAFT_MODULE_RECIPES = ['world.recipes_weapons', 'world.recipes_potions']
```python
Below is an example of a crafting recipe. See the `CraftingRecipe` class for
details of which properties and methods are available to override - the craft
behavior can be modified substantially this way.
CRAFT_RECIPE_MODULES = ['world.recipes_weapons', 'world.recipes_potions']
```
Below is an example of a crafting recipe and how `craft` calls it under the
hood. See the `CraftingRecipe` class for details of which properties and
methods are available to override - the craft behavior can be modified
substantially this way.
```python
@ -73,7 +92,7 @@ behavior can be modified substantially this way.
class PigIronRecipe(CraftingRecipe):
# Pig iron is a high-carbon result of melting iron in a blast furnace.
name = "pig iron"
name = "pig iron" # this is what crafting.craft and CmdCraft uses
tool_tags = ["blast furnace"]
consumable_tags = ["iron ore", "coal", "coal"]
output_prototypes = [
@ -82,18 +101,26 @@ behavior can be modified substantially this way.
"tags": [("pig iron", "crafting_material")]}
]
# for testing, conveniently spawn all we need based on the tags on the class
tools, consumables = PigIronRecipe.seed()
recipe = PigIronRecipe(caller, *(tools + consumables))
result = recipe.craft()
```
The `evennia/contrib/crafting/example_recipes.py` module has more examples of
recipes.
If the above class was added to a module in `CRAFT_RECIPE_MODULES`, it could be
called using its `.name` property, as "pig iron".
The [example_recipies](api:evennia.contrib.crafting.example_recipes) module has
a full example of the components for creating a sword from base components.
----
"""
from copy import copy
from evennia.utils.utils import (
iter_to_str, callables_from_module, inherits_from, make_iter)
from evennia.utils.utils import iter_to_str, callables_from_module, inherits_from, make_iter
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.prototypes.spawner import spawn
@ -109,6 +136,7 @@ def _load_recipes():
"""
from django.conf import settings
global _RECIPE_CLASSES
if not _RECIPE_CLASSES:
paths = ["evennia.contrib.crafting.example_recipes"]
@ -126,12 +154,14 @@ class CraftingError(RuntimeError):
"""
class CraftingValidationError(CraftingError):
"""
Error if crafting validation failed.
"""
class CraftingRecipeBase:
"""
The recipe handles all aspects of performing a 'craft' operation. This is
@ -164,6 +194,7 @@ class CraftingRecipeBase:
"""
name = "recipe base"
# if set, allow running `.craft` more than once on the same instance.
@ -436,6 +467,7 @@ class CraftingRecipe(CraftingRecipeBase):
shown to the crafter automatically
"""
name = "crafting recipe"
# this define the overall category all material tags must have
@ -458,11 +490,13 @@ class CraftingRecipe(CraftingRecipeBase):
error_tool_missing_message = "Could not craft {outputs} without {missing}."
# error to show if tool-order matters and it was wrong. Missing is the first
# tool out of order
error_tool_order_message = \
error_tool_order_message = (
"Could not craft {outputs} since {missing} was added in the wrong order."
)
# if .exact_tools is set and there are more than needed
error_tool_excess_message = \
error_tool_excess_message = (
"Could not craft {outputs} without the exact tools (extra {excess})."
)
# a list of tag-keys (of the `tag_category`). If more than one of each type
# is needed, there should be multiple same-named entries in this list.
@ -483,11 +517,13 @@ class CraftingRecipe(CraftingRecipeBase):
error_consumable_missing_message = "Could not craft {outputs} without {missing}."
# error to show if consumable order matters and it was wrong. Missing is the first
# consumable out of order
error_consumable_order_message = \
error_consumable_order_message = (
"Could not craft {outputs} since {missing} was added in the wrong order."
)
# if .exact_consumables is set and there are more than needed
error_consumable_excess_message = \
error_consumable_excess_message = (
"Could not craft {outputs} without the exact ingredients (extra {excess})."
)
# this is a list of one or more prototypes (prototype_keys to existing
# prototypes or full prototype-dicts) to use to build the result. All of
@ -503,7 +539,7 @@ class CraftingRecipe(CraftingRecipeBase):
# show after a successful craft
success_message = "You successfully craft {outputs}!"
def __init__(self, crafter, *inputs, **kwargs):
def __init__(self, crafter, *inputs, **kwargs):
"""
Args:
crafter (Object): The one doing the crafting.
@ -528,32 +564,37 @@ class CraftingRecipe(CraftingRecipeBase):
# validate class properties
if self.consumable_names:
assert len(self.consumable_names) == len(self.consumable_tags), \
f"Crafting {self.__class__}.consumable_names list must " \
assert len(self.consumable_names) == len(self.consumable_tags), (
f"Crafting {self.__class__}.consumable_names list must "
"have the same length as .consumable_tags."
)
else:
self.consumable_names = self.consumable_tags
if self.tool_names:
assert len(self.tool_names) == len(self.tool_tags), \
f"Crafting {self.__class__}.tool_names list must " \
assert len(self.tool_names) == len(self.tool_tags), (
f"Crafting {self.__class__}.tool_names list must "
"have the same length as .tool_tags."
)
else:
self.tool_names = self.tool_tags
if self.output_names:
assert len(self.consumable_names) == len(self.consumable_tags), \
f"Crafting {self.__class__}.output_names list must " \
assert len(self.consumable_names) == len(self.consumable_tags), (
f"Crafting {self.__class__}.output_names list must "
"have the same length as .output_prototypes."
)
else:
self.output_names = [
prot.get("key", prot.get("typeclass", "unnamed"))
if isinstance(prot, dict) else str(prot)
if isinstance(prot, dict)
else str(prot)
for prot in self.output_prototypes
]
assert isinstance(self.output_prototypes, (list, tuple)), \
"Crafting {self.__class__}.output_prototypes must be a list or tuple."
assert isinstance(
self.output_prototypes, (list, tuple)
), "Crafting {self.__class__}.output_prototypes must be a list or tuple."
# don't allow reuse if we have consumables. If only tools we can reuse
# over and over since nothing changes.
@ -568,14 +609,15 @@ class CraftingRecipe(CraftingRecipeBase):
# build template context
mapping = {"missing": missing, "excess": excess}
mapping.update({
f"i{ind}": self.consumable_names[ind]
for ind, name in enumerate(self.consumable_names or self.consumable_tags)
})
mapping.update({
f"o{ind}": self.output_names[ind]
for ind, name in enumerate(self.output_names)
})
mapping.update(
{
f"i{ind}": self.consumable_names[ind]
for ind, name in enumerate(self.consumable_names or self.consumable_tags)
}
)
mapping.update(
{f"o{ind}": self.output_names[ind] for ind, name in enumerate(self.output_names)}
)
mapping["tools"] = involved_tools
mapping["consumables"] = involved_cons
@ -633,18 +675,17 @@ class CraftingRecipe(CraftingRecipeBase):
create_object(
key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
tags=[(tag, cls.tool_tag_category), *tool_tags],
**tool_kwargs
**tool_kwargs,
)
)
consumables = []
for itag, tag in enumerate(cls.consumable_tags):
consumables.append(
create_object(
key=cons_key or (cls.consumable_names[itag] if
cls.consumable_names else
tag.capitalize()),
key=cons_key
or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
tags=[(tag, cls.consumable_tag_category), *cons_tags],
**consumable_kwargs
**consumable_kwargs,
)
)
return tools, consumables
@ -669,8 +710,15 @@ class CraftingRecipe(CraftingRecipeBase):
"""
def _check_completeness(
tagmap, taglist, namelist, exact_match, exact_order,
error_missing_message, error_order_message, error_excess_message):
tagmap,
taglist,
namelist,
exact_match,
exact_order,
error_missing_message,
error_order_message,
error_excess_message,
):
"""Compare tagmap (inputs) to taglist (required)"""
valids = []
for itag, tagkey in enumerate(taglist):
@ -682,8 +730,8 @@ class CraftingRecipe(CraftingRecipeBase):
if exact_order:
# if we get here order is wrong
err = self._format_message(
error_order_message,
missing=obj.get_display_name(looker=self.crafter))
error_order_message, missing=obj.get_display_name(looker=self.crafter)
)
self.msg(err)
raise CraftingValidationError(err)
@ -694,7 +742,8 @@ class CraftingRecipe(CraftingRecipeBase):
elif exact_match:
err = self._format_message(
error_missing_message,
missing=namelist[itag] if namelist else tagkey.capitalize())
missing=namelist[itag] if namelist else tagkey.capitalize(),
)
self.msg(err)
raise CraftingValidationError(err)
@ -703,21 +752,30 @@ class CraftingRecipe(CraftingRecipeBase):
# thus this is not an exact match
err = self._format_message(
error_excess_message,
excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap])
excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap],
)
self.msg(err)
raise CraftingValidationError(err)
return valids
# get tools and consumables from self.inputs
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
for obj in self.inputs if obj and hasattr(obj, "tags") and
inherits_from(obj, "evennia.objects.models.ObjectDB")}
tool_map = {
obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
for obj in self.inputs
if obj
and hasattr(obj, "tags")
and inherits_from(obj, "evennia.objects.models.ObjectDB")
}
tool_map = {obj: tags for obj, tags in tool_map.items() if tags}
consumable_map = {obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
for obj in self.inputs
if obj and hasattr(obj, "tags") and obj not in tool_map and
inherits_from(obj, "evennia.objects.models.ObjectDB")}
consumable_map = {
obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
for obj in self.inputs
if obj
and hasattr(obj, "tags")
and obj not in tool_map
and inherits_from(obj, "evennia.objects.models.ObjectDB")
}
consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags}
# we set these so they are available for error management at all times,
@ -750,11 +808,13 @@ class CraftingRecipe(CraftingRecipeBase):
# all the recipe needs now.
if len(tools) != len(self.tool_tags):
raise CraftingValidationError(
f"Tools {tools}'s tags do not match expected tags {self.tool_tags}")
f"Tools {tools}'s tags do not match expected tags {self.tool_tags}"
)
if len(consumables) != len(self.consumable_tags):
raise CraftingValidationError(
f"Consumables {consumables}'s tags do not match "
f"expected tags {self.consumable_tags}")
f"expected tags {self.consumable_tags}"
)
self.validated_tools = tools
self.validated_consumables = consumables
@ -816,25 +876,29 @@ class CraftingRecipe(CraftingRecipeBase):
def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
"""
Craft a given recipe from a source recipe module. A recipe module is a
Python module containing recipe classes. Note that this requires
`settings.CRAFT_RECIPE_MODULES` to be added to a list of one or more
python-paths to modules holding Recipe-classes.
Access function. Craft a given recipe from a source recipe module. A
recipe module is a Python module containing recipe classes. Note that this
requires `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or
more python-paths to modules holding Recipe-classes.
Args:
crafter (Object): The one doing the crafting.
recipe_name (str): The `CraftRecipe.name` to use.
*inputs: Suitable ingredients (Objects) to use in the crafting.
recipe_name (str): The `CraftRecipe.name` to use. This uses fuzzy-matching
if the result is unique.
*inputs: Suitable ingredients and/or tools (Objects) to use in the crafting.
raise_exception (bool, optional): If crafting failed for whatever
reason, raise `CraftingError`. The user will still be informed by the recipe.
**kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft).
reason, raise `CraftingError`. The user will still be informed by the
recipe.
**kwargs: Optional kwargs to pass into the recipe (will passed into
recipe.craft).
Returns:
list: Crafted objects, if any.
Raises:
CraftingError: If `raise_exception` is True and crafting failed to produce an output.
KeyError: If `recipe_name` failed to find a matching recipe class.
CraftingError: If `raise_exception` is True and crafting failed to
produce an output. KeyError: If `recipe_name` failed to find a
matching recipe class (or the hit was not precise enough.)
Notes:
If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and
@ -846,18 +910,30 @@ def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
RecipeClass = _RECIPE_CLASSES.get(recipe_name, None)
if not RecipeClass:
raise KeyError("No recipe in settings.CRAFT_RECIPE_MODULES "
f"has a name matching {recipe_name}")
# try a startswith fuzzy match
matches = [key for key in _RECIPE_CLASSES if key.startswith(recipe_name)]
if not matches:
# try in-match
matches = [key for key in _RECIPE_CLASSES if recipe_name in key]
if len(matches) == 1:
RecipeClass = matches[0]
if not RecipeClass:
raise KeyError(
f"No recipe in settings.CRAFT_RECIPE_MODULES has a name matching {recipe_name}"
)
recipe = RecipeClass(crafter, *inputs, **kwargs)
return recipe.craft(raise_exception=raise_exception)
# craft command/cmdset
class CraftingCmdSet(CmdSet):
"""
Store crafting command.
"""
key = "Crafting cmdset"
def at_cmdset_creation(self):
@ -883,22 +959,35 @@ class CmdCraft(Command):
"""
key = "craft"
locks = "cmd:all()"
help_category = "General"
arg_regex = r"\s|$"
def parse(self):
"""
Handle parsing of
Handle parsing of:
::
<recipe> [FROM <ingredients>] [USING <tools>]
Examples:
::
craft snowball from snow
craft puppet from piece of wood using knife
craft bread from flour, butter, water, yeast using owen, bowl, roller
craft fireball using wand, spellbook
"""
self.args = args = self.args.strip().lower()
recipe, ingredients, tools = "", "", ""
if 'from' in args:
if "from" in args:
recipe, *rest = args.split(" from ", 1)
rest = rest[0] if rest else ""
ingredients, *tools = rest.split(" using ", 1)
elif 'using' in args:
elif "using" in args:
recipe, *tools = args.split(" using ", 1)
tools = tools[0] if tools else ""
@ -931,13 +1020,19 @@ class CmdCraft(Command):
# try to include characters or accounts etc.
if not obj:
return
if (not inherits_from(obj, "evennia.objects.models.ObjectDB")
or obj.sessions.all() or not obj.access(caller, "craft", default=True)):
if (
not inherits_from(obj, "evennia.objects.models.ObjectDB")
or obj.sessions.all()
or not obj.access(caller, "craft", default=True)
):
# We don't allow to include puppeted objects nor those with the
# 'negative' permission 'nocraft'.
caller.msg(obj.attributes.get(
"crafting_consumable_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this."))
caller.msg(
obj.attributes.get(
"crafting_consumable_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
)
)
return
ingredients.append(obj)
@ -950,9 +1045,12 @@ class CmdCraft(Command):
if not obj:
return None
if not obj.access(caller, "craft", default=True):
caller.msg(obj.attributes.get(
"crafting_tool_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this."))
caller.msg(
obj.attributes.get(
"crafting_tool_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
)
)
return
tools.append(obj)

View file

@ -1,18 +1,19 @@
"""
Example recipes for the crafting system - how to make a sword.
How to make a sword - example crafting tree for the crafting system.
See the _SwordSmithingBaseRecipe for an example of extendng the recipe with a
mocked 'skill' system (just random chance in our case). The skill system used
is game-specific but likely to be needed for most 'real' crafting systems.
See the `SwordSmithingBaseRecipe` in this module for an example of extendng the
recipe with a mocked 'skill' system (just random chance in our case). The skill
system used is game-specific but likely to be needed for most 'real' crafting
systems.
Note that 'tools' are references to the tools used - they don't need to be in
the inventory of the crafter. So when 'blast furnace' is given below, it is a
reference to a blast furnace used, not suggesting the crafter is carrying it
around with them.
::
## Sword crafting tree
Sword crafting tree
::
# base materials (consumables)
@ -40,6 +41,7 @@ around with them.
sword = sword blade + sword guard + sword pommel
+ sword handle + leather + knife[T] + hammer[T] + furnace[T]
----
"""
@ -52,13 +54,16 @@ class PigIronRecipe(CraftingRecipe):
Pig iron is a high-carbon result of melting iron in a blast furnace.
"""
name = "pig iron"
tool_tags = ["blast furnace"]
consumable_tags = ["iron ore", "coal", "coal"]
output_prototypes = [
{"key": "Pig Iron ingot",
"desc": "An ingot of crude pig iron.",
"tags": [("pig iron", "crafting_material")]}
{
"key": "Pig Iron ingot",
"desc": "An ingot of crude pig iron.",
"tags": [("pig iron", "crafting_material")],
}
]
@ -68,13 +73,16 @@ class CrucibleSteelRecipe(CraftingRecipe):
crucible produces a medieval level of steel (like damascus steel).
"""
name = "crucible steel"
tool_tags = ["crucible"]
consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"]
output_prototypes = [
{"key": "Crucible steel ingot",
"desc": "An ingot of multi-colored crucible steel.",
"tags": [("crucible steel", "crafting_material")]}
{
"key": "Crucible steel ingot",
"desc": "An ingot of multi-colored crucible steel.",
"tags": [("crucible steel", "crafting_material")],
}
]
@ -87,8 +95,9 @@ class _SwordSmithingBaseRecipe(CraftingRecipe):
"""
success_message = "Your smithing work bears fruit and you craft {outputs}!"
failed_message = ("You work and work but you are not happy with the result. "
"You need to start over.")
failed_message = (
"You work and work but you are not happy with the result. You need to start over."
)
def do_craft(self, **kwargs):
"""
@ -130,28 +139,35 @@ class SwordBladeRecipe(_SwordSmithingBaseRecipe):
part of the sword you hold on to).
"""
name = "sword blade"
tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"]
output_prototypes = [
{"key": "Sword blade",
"desc": "A long blade that may one day become a sword.",
"tags": [("sword blade", "crafting_material")]}
{
"key": "Sword blade",
"desc": "A long blade that may one day become a sword.",
"tags": [("sword blade", "crafting_material")],
}
]
class SwordPommelRecipe(_SwordSmithingBaseRecipe):
"""
The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding
it together.
"""
name = "sword pommel"
tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"]
output_prototypes = [
{"key": "Sword pommel",
"desc": "The pommel for a future sword.",
"tags": [("sword pommel", "crafting_material")]}
{
"key": "Sword pommel",
"desc": "The pommel for a future sword.",
"tags": [("sword pommel", "crafting_material")],
}
]
@ -161,13 +177,16 @@ class SwordGuardRecipe(_SwordSmithingBaseRecipe):
sword's blade and also protects the hand when parrying.
"""
name = "sword guard"
tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"]
output_prototypes = [
{"key": "Sword guard",
"desc": "The cross-guard for a future sword.",
"tags": [("sword guard", "crafting_material")]}
{
"key": "Sword guard",
"desc": "The cross-guard for a future sword.",
"tags": [("sword guard", "crafting_material")],
}
]
@ -176,13 +195,16 @@ class RawhideRecipe(CraftingRecipe):
Rawhide is animal skin cleaned and stripped of hair.
"""
name = "rawhide"
tool_tags = ["knife"]
consumable_tags = ["fur"]
output_prototypes = [
{"key": "Rawhide",
"desc": "Animal skin, cleaned and with hair removed.",
"tags": [("rawhide", "crafting_material")]}
{
"key": "Rawhide",
"desc": "Animal skin, cleaned and with hair removed.",
"tags": [("rawhide", "crafting_material")],
}
]
@ -193,17 +215,21 @@ class OakBarkRecipe(CraftingRecipe):
This produces two outputs - the bark and the cleaned wood.
"""
name = "oak bark"
tool_tags = ["knife"]
consumable_tags = ["oak wood"]
output_prototypes = [
{"key": "Oak bark",
"desc": "Bark of oak, stripped from the core wood.",
"tags": [("oak bark", "crafting_material")]},
{"key": "Oak Wood (cleaned)",
"desc": "Oakwood core, stripped of bark.",
"tags": [("cleaned oak wood", "crafting_material")]},
{
"key": "Oak bark",
"desc": "Bark of oak, stripped from the core wood.",
"tags": [("oak bark", "crafting_material")],
},
{
"key": "Oak Wood (cleaned)",
"desc": "Oakwood core, stripped of bark.",
"tags": [("cleaned oak wood", "crafting_material")],
},
]
@ -214,13 +240,16 @@ class LeatherRecipe(CraftingRecipe):
'tanning rack' tool should be required too ...
"""
name = "leather"
tool_tags = ["cauldron"]
consumable_tags = ["rawhide", "oak bark", "water"]
output_prototypes = [
{"key": "Piece of Leather",
"desc": "A piece of leather.",
"tags": [("leather", "crafting_material")]}
{
"key": "Piece of Leather",
"desc": "A piece of leather.",
"tags": [("leather", "crafting_material")],
}
]
@ -231,13 +260,16 @@ class SwordHandleRecipe(CraftingRecipe):
is wrapped in leather, but that will be added at the end.
"""
name = "sword handle"
tool_tags = ["knife"]
consumable_tags = ["cleaned oak wood"]
output_prototypes = [
{"key": "Sword handle",
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
"tags": [("sword handle", "crafting_material")]}
{
"key": "Sword handle",
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
"tags": [("sword handle", "crafting_material")],
}
]
@ -252,17 +284,19 @@ class SwordRecipe(_SwordSmithingBaseRecipe):
This covers only a single 'sword' type.
"""
name = "sword"
tool_tags = ["hammer", "furnace", "knife"]
consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle",
"leather"]
consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", "leather"]
output_prototypes = [
{"key": "Sword",
"desc": "A bladed weapon.",
# setting the tag as well - who knows if one can make something from this too!
"tags": [("sword", "crafting_material")]}
# obviously there would be other properties of a 'sword' added here
# too, depending on how combat works in the your game!
{
"key": "Sword",
"desc": "A bladed weapon.",
# setting the tag as well - who knows if one can make something from this too!
"tags": [("sword", "crafting_material")],
}
# obviously there would be other properties of a 'sword' added here
# too, depending on how combat works in the your game!
]
# this requires more precision
exact_consumable_order = True

View file

@ -18,6 +18,7 @@ class TestCraftUtils(TestCase):
Test helper utils for crafting.
"""
maxDiff = None
@override_settings(CRAFT_RECIPE_MODULES=[])
@ -28,17 +29,17 @@ class TestCraftUtils(TestCase):
self.assertEqual(
crafting._RECIPE_CLASSES,
{
'crucible steel': example_recipes.CrucibleSteelRecipe,
'leather': example_recipes.LeatherRecipe,
'oak bark': example_recipes.OakBarkRecipe,
'pig iron': example_recipes.PigIronRecipe,
'rawhide': example_recipes.RawhideRecipe,
'sword': example_recipes.SwordRecipe,
'sword blade': example_recipes.SwordBladeRecipe,
'sword guard': example_recipes.SwordGuardRecipe,
'sword handle': example_recipes.SwordHandleRecipe,
'sword pommel': example_recipes.SwordPommelRecipe,
}
"crucible steel": example_recipes.CrucibleSteelRecipe,
"leather": example_recipes.LeatherRecipe,
"oak bark": example_recipes.OakBarkRecipe,
"pig iron": example_recipes.PigIronRecipe,
"rawhide": example_recipes.RawhideRecipe,
"sword": example_recipes.SwordRecipe,
"sword blade": example_recipes.SwordBladeRecipe,
"sword guard": example_recipes.SwordGuardRecipe,
"sword handle": example_recipes.SwordHandleRecipe,
"sword pommel": example_recipes.SwordPommelRecipe,
},
)
@ -54,6 +55,7 @@ class TestCraftingRecipeBase(TestCase):
"""
Test the parent recipe class.
"""
def setUp(self):
self.crafter = mock.MagicMock()
self.crafter.msg = mock.MagicMock()
@ -65,7 +67,8 @@ class TestCraftingRecipeBase(TestCase):
self.kwargs = {"kw1": 1, "kw2": 2}
self.recipe = crafting.CraftingRecipeBase(
self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs)
self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs
)
def test_msg(self):
"""Test messaging to crafter"""
@ -76,9 +79,7 @@ class TestCraftingRecipeBase(TestCase):
def test_pre_craft(self):
"""Test validating hook"""
self.recipe.pre_craft()
self.assertEqual(
self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3)
)
self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))
def test_pre_craft_fail(self):
"""Should rase error if validation fails"""
@ -126,9 +127,11 @@ class _MockRecipe(crafting.CraftingRecipe):
tool_tags = ["tool1", "tool2"]
consumable_tags = ["cons1", "cons2", "cons3"]
output_prototypes = [
{"key": "Result1",
"prototype_key": "resultprot",
"tags": [("result1", "crafting_material")]}
{
"key": "Result1",
"prototype_key": "resultprot",
"tags": [("result1", "crafting_material")],
}
]
@ -137,6 +140,7 @@ class TestCraftingRecipe(TestCase):
"""
Test the CraftingRecipe class with one recipe
"""
maxDiff = None
def setUp(self):
@ -162,19 +166,27 @@ class TestCraftingRecipe(TestCase):
def test_error_format(self):
"""Test the automatic error formatter """
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
)
msg = ("{missing},{tools},{consumables},{inputs},{outputs}"
"{i0},{i1},{o0}")
kwargs = {"missing": "foo", "tools": ["bar", "bar2", "bar3"],
"consumables": ["cons1", "cons2"]}
msg = "{missing},{tools},{consumables},{inputs},{outputs}" "{i0},{i1},{o0}"
kwargs = {
"missing": "foo",
"tools": ["bar", "bar2", "bar3"],
"consumables": ["cons1", "cons2"],
}
expected = {
'missing': 'foo', 'i0': 'cons1', 'i1': 'cons2', 'i2': 'cons3', 'o0':
'Result1', 'tools': 'bar, bar2 and bar3', 'consumables': 'cons1 and cons2',
'inputs': 'cons1, cons2 and cons3', 'outputs': 'Result1'}
"missing": "foo",
"i0": "cons1",
"i1": "cons2",
"i2": "cons3",
"o0": "Result1",
"tools": "bar, bar2 and bar3",
"consumables": "cons1 and cons2",
"inputs": "cons1, cons2 and cons3",
"outputs": "Result1",
}
result = recipe._format_message(msg, **kwargs)
self.assertEqual(result, msg.format(**expected))
@ -182,16 +194,16 @@ class TestCraftingRecipe(TestCase):
def test_craft__success(self):
"""Test to create a result from the recipe"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
)
result = recipe.craft()
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ['result1', 'resultprot'])
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
)
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
@ -208,17 +220,15 @@ class TestCraftingRecipe(TestCase):
tools, consumables = _MockRecipe.seed()
# this should be a normal successful crafting
recipe = _MockRecipe(
self.crafter,
*(tools + consumables)
)
recipe = _MockRecipe(self.crafter, *(tools + consumables))
result = recipe.craft()
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ['result1', 'resultprot'])
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
)
# make sure consumables are gone
for cons in consumables:
@ -229,15 +239,13 @@ class TestCraftingRecipe(TestCase):
def test_craft_missing_tool__fail(self):
"""Fail craft by missing tool2"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.cons1, self.cons2, self.cons3
)
recipe = _MockRecipe(self.crafter, self.tool1, self.cons1, self.cons2, self.cons3)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_missing_message.format(outputs="Result1", missing='tool2'),
{"type": "crafting"})
recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -249,16 +257,13 @@ class TestCraftingRecipe(TestCase):
def test_craft_missing_cons__fail(self):
"""Fail craft by missing cons3"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2
)
recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(
outputs="Result1", missing='cons3'),
{"type": "crafting"})
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -273,19 +278,16 @@ class TestCraftingRecipe(TestCase):
cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, cons4
)
recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, cons4)
recipe.consume_on_fail = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(
outputs="Result1", missing='cons3'),
{"type": "crafting"})
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
{"type": "crafting"},
)
# make sure consumables are deleted even though we failed
self.assertIsNone(self.cons1.pk)
@ -303,16 +305,15 @@ class TestCraftingRecipe(TestCase):
wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, wrong
)
recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, wrong)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)),
{"type": "crafting"})
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
@ -328,15 +329,16 @@ class TestCraftingRecipe(TestCase):
tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)),
{"type": "crafting"})
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -354,15 +356,16 @@ class TestCraftingRecipe(TestCase):
cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_excess_message.format(
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)),
{"type": "crafting"})
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -379,14 +382,14 @@ class TestCraftingRecipe(TestCase):
tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
)
recipe.exact_tools = False
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
)
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
@ -402,14 +405,14 @@ class TestCraftingRecipe(TestCase):
cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
)
recipe.exact_consumables = False
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
)
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
@ -422,16 +425,17 @@ class TestCraftingRecipe(TestCase):
def test_craft_tool_order__fail(self):
"""Strict tool-order recipe fail """
recipe = _MockRecipe(
self.crafter,
self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
)
recipe.exact_tool_order = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_order_message.format(
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)),
{"type": "crafting"})
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -444,16 +448,17 @@ class TestCraftingRecipe(TestCase):
def test_craft_cons_order__fail(self):
"""Strict tool-order recipe fail """
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
)
recipe.exact_consumable_order = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_order_message.format(
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)),
{"type": "crafting"})
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
),
{"type": "crafting"},
)
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
@ -469,6 +474,7 @@ class TestCraftSword(TestCase):
Test the `craft` function by crafting the example sword.
"""
def setUp(self):
self.crafter = mock.MagicMock()
self.crafter.msg = mock.MagicMock()
@ -578,8 +584,16 @@ class TestCraftSword(TestCase):
sword_handle = _craft("sword handle", *inputs)
# sword (order matters)
inputs = [sword_blade, sword_guard, sword_pommel, sword_handle,
leather, knife, hammer, furnace]
inputs = [
sword_blade,
sword_guard,
sword_pommel,
sword_handle,
leather,
knife,
hammer,
furnace,
]
sword = _craft("sword", *inputs)
self.assertEqual(sword.key, "Sword")
@ -633,10 +647,8 @@ class TestCraftSword(TestCase):
self.assertIsNotNone(cauldron)
@mock.patch("evennia.contrib.crafting.crafting._load_recipes",
new=mock.MagicMock())
@mock.patch("evennia.contrib.crafting.crafting._RECIPE_CLASSES",
new={"testrecipe": _MockRecipe})
@mock.patch("evennia.contrib.crafting.crafting._load_recipes", new=mock.MagicMock())
@mock.patch("evennia.contrib.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe})
@override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
class TestCraftCommand(CommandTest):
"""Test the crafting command"""
@ -645,15 +657,15 @@ class TestCraftCommand(CommandTest):
super().setUp()
tools, consumables = _MockRecipe.seed(
tool_kwargs={"location": self.char1},
consumable_kwargs={"location": self.char1})
tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1}
)
def test_craft__success(self):
"Successfully craft using command"
self.call(
crafting.CmdCraft(),
"testrecipe from cons1, cons2, cons3 using tool1, tool2",
_MockRecipe.success_message.format(outputs="Result1")
_MockRecipe.success_message.format(outputs="Result1"),
)
def test_craft__notools__failure(self):
@ -661,12 +673,12 @@ class TestCraftCommand(CommandTest):
self.call(
crafting.CmdCraft(),
"testrecipe from cons1, cons2, cons3",
_MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1")
_MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1"),
)
def test_craft__nocons__failure(self):
self.call(
crafting.CmdCraft(),
"testrecipe using tool1, tool2",
_MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1")
_MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"),
)

View file

View file

@ -174,12 +174,11 @@ class TestEvscaperoomCommands(CommandTest):
self.call(commands.CmdSpeak(), "", "What do you want to say?", cmdstring="")
self.call(commands.CmdSpeak(), "Hello!", "You say: Hello!", cmdstring="")
self.call(commands.CmdSpeak(), "", "What do you want to whisper?", cmdstring="whisper")
self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper")
self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper")
self.call(commands.CmdSpeak(), "Hi.", "You whisper: (Hi.)", cmdstring="whisper")
self.call(commands.CmdSpeak(), "HELLO!", "You shout: HELLO!", cmdstring="shout")
self.call(commands.CmdSpeak(), "Hello to obj", "You say: Hello", cmdstring="say")
self.call(commands.CmdSpeak(), "Hello to obj", "You shout: Hello", cmdstring="shout")
self.call(commands.CmdSpeak(), "Hello", "You say: Hello", cmdstring="say")
self.call(commands.CmdSpeak(), "Hello", "You shout: HELLO", cmdstring="shout")
def test_emote(self):
self.call(
@ -272,7 +271,7 @@ class TestStates(EvenniaTest):
dirname = path.join(path.dirname(__file__), "states")
states = []
for imp, module, ispackage in pkgutil.walk_packages(
path=[dirname], prefix="evscaperoom.states."
path=[dirname], prefix="evennia.contrib.evscaperoom.states."
):
mod = mod_import(module)
states.append(mod)