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) - Make IP throttle use Django-based cache system for optional persistence (PR by strikaco)
- Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and - Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and
"TutorialWeaponRack" to prevent collisions with classes in mygame "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 - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False
- `py` command now reroutes stdout to output results in-game client. `py` - `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", "Starting/Adding-Command-Tutorial": "Adding-Commands",
"Adding-Command-Tutorial": "Adding-Commands", "Adding-Command-Tutorial": "Adding-Commands",
"CmdSet": "Command-Sets", "CmdSet": "Command-Sets",
"Spawner": "Spawner-and-Prototypes", "Spawner": "Prototypes",
"issue": "github:issue", "issue": "github:issue",
"issues": "github:issue", "issues": "github:issue",
"bug": "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) - [Attributes](./Attributes)
- [Nicks](./Nicks) - [Nicks](./Nicks)
- [Tags](./Tags) - [Tags](./Tags)
- [Spawner and prototypes](./Spawner-and-Prototypes) - [Spawner and prototypes](./Prototypes)
- [Help entries](./Help-System) - [Help entries](./Help-System)
## Commands ## 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 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 `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 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. to use though - see next section.
## Properties and functions on Objects ## 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). - `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. 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 - - `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 ### 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 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. 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 ## 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) - [A voice-operated elevator using events](./A-voice-operated-elevator-using-events)
- [Dialogues using events](./Dialogues-in-events) - [Dialogues using events](./Dialogues-in-events)
## Maps ## Maps
Solutions for generating and displaying maps in-game.
- [Dynamic in-game map](./Dynamic-In-Game-Map) - [Dynamic in-game map](./Dynamic-In-Game-Map)
- [Static in-game map](./Static-In-Game-Map) - [Static in-game map](./Static-In-Game-Map)
## The tutorial-world ## 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) - [The tutorial world introduction](../Howto/Starting/Part1/Tutorial-World-Introduction)
## Menu-builder ## 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) - [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.gametime](api:evennia.utils.gametime) - server run- and game time ([docs](Components/Coding-Utils#gametime))
- [evennia.logger](api:evennia.utils.logger) - logging tools - [evennia.logger](api:evennia.utils.logger) - logging tools
- [evennia.ansi](api:evennia.utils.ansi) - ansi coloring 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.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.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 - [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)). - `locale/` - Language files ([i18n](../../../Concepts/Internationalization)).
- [`locks/`](../../../Components/Locks) - Lock system for restricting access to in-game entities. - [`locks/`](../../../Components/Locks) - Lock system for restricting access to in-game entities.
- [`objects/`](../../../Components/Objects) - In-game entities (all types of items and Characters). - [`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) - [`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. - [`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. - [`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 - [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 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. [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 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 tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
equipment, stats and looks. 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 :maxdepth: 6
evennia.contrib.awsstorage evennia.contrib.awsstorage
evennia.contrib.crafting
evennia.contrib.evscaperoom
evennia.contrib.ingame_python evennia.contrib.ingame_python
evennia.contrib.security evennia.contrib.security
evennia.contrib.turnbattle evennia.contrib.turnbattle

View file

@ -37,12 +37,12 @@
- [Components/Objects](Components/Objects) - [Components/Objects](Components/Objects)
- [Components/Outputfuncs](Components/Outputfuncs) - [Components/Outputfuncs](Components/Outputfuncs)
- [Components/Portal And Server](Components/Portal-And-Server) - [Components/Portal And Server](Components/Portal-And-Server)
- [Components/Prototypes](Components/Prototypes)
- [Components/Scripts](Components/Scripts) - [Components/Scripts](Components/Scripts)
- [Components/Server](Components/Server) - [Components/Server](Components/Server)
- [Components/Server Conf](Components/Server-Conf) - [Components/Server Conf](Components/Server-Conf)
- [Components/Sessions](Components/Sessions) - [Components/Sessions](Components/Sessions)
- [Components/Signals](Components/Signals) - [Components/Signals](Components/Signals)
- [Components/Spawner and Prototypes](Components/Spawner-and-Prototypes)
- [Components/Tags](Components/Tags) - [Components/Tags](Components/Tags)
- [Components/TickerHandler](Components/TickerHandler) - [Components/TickerHandler](Components/TickerHandler)
- [Components/Typeclasses](Components/Typeclasses) - [Components/Typeclasses](Components/Typeclasses)
@ -70,6 +70,7 @@
- [Contribs/Arxcode installing help](Contribs/Arxcode-installing-help) - [Contribs/Arxcode installing help](Contribs/Arxcode-installing-help)
- [Contribs/Building menus](Contribs/Building-menus) - [Contribs/Building menus](Contribs/Building-menus)
- [Contribs/Contrib Overview](Contribs/Contrib-Overview) - [Contribs/Contrib Overview](Contribs/Contrib-Overview)
- [Contribs/Crafting](Contribs/Crafting)
- [Contribs/Dialogues in events](Contribs/Dialogues-in-events) - [Contribs/Dialogues in events](Contribs/Dialogues-in-events)
- [Contribs/Dynamic In Game Map](Contribs/Dynamic-In-Game-Map) - [Contribs/Dynamic In Game Map](Contribs/Dynamic-In-Game-Map)
- [Contribs/Static In Game Map](Contribs/Static-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 Crafting - Griatch 2020
This is a general crafting engine. The basic functionality of crafting is to 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 combine any number of of items or tools in a 'recipe' to produce a new result.
useful not only for traditional crafting but also for puzzle-solving or
similar. 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 ## 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 - Create a new module and add it to a new list in your settings file
(`server/conf/settings.py`) named `CRAFT_MODULE_RECIPES`. (`server/conf/settings.py`) named `CRAFT_RECIPES_MODULES`, such as
- In the new module, create one or more classes, each a child of `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` `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 property. It also defines what inputs are required and what is created using
this recipe. this recipe.
- Objects to use for crafting should (by default) be tagged with tags using the - 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 tag-category `crafting_material` or `crafting_tool`. The name of the object
its tag. 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!).
## Usage ## Crafting in game
By default the crafter needs to specify which components The default `craft` command handles all crafting needs.
should be used for the recipe: ::
craft spiked club from club, nails > craft spiked club from club, nails
Here, `spiked club` specifies the recipe while `club` and `nails` are objects Here, `spiked club` specifies the recipe while `club` and `nails` are objects
the crafter must have in their inventory. These will be consumed during the crafter must have in their inventory. These will be consumed during
crafting (by default only if crafting was successful). crafting (by default only if crafting was successful).
A recipe can also require _tools_. These must be either in inventory or in A recipe can also require *tools* (like the `hammer` above). These must be
the current location. Tools are not consumed during the crafting. 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 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 specifies the name of the recipe to use and expects all suitable
ingredients/tools as arguments (consumables and tools should be added together, ingredients/tools as arguments (consumables and tools should be added together,
tools will be identified before consumables). 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 from evennia.contrib.crafting import crafting
of any error in this case (this should be handle by the recipe itself).
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 ## Recipes
A _recipe_ works like an input/output blackbox: you put consumables (and/or A *recipe* is a class that works like an input/output blackbox: you initialize
tools) into it and if they match the recipe, a new result is spit out. it with consumables (and/or tools) if they match the recipe, a new
Consumables are consumed in the process while tools are not. 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 This module contains a base class for making new ingredient types
(`CraftingRecipeBase`) and an implementation of the most common form of (`CraftingRecipeBase`) and an implementation of the most common form of
crafting (`CraftingRecipe`) using objects and prototypes. crafting (`CraftingRecipe`) using objects and prototypes.
Recipes are put in one or more modules added as a list to the 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 CRAFT_RECIPE_MODULES = ['world.recipes_weapons', 'world.recipes_potions']
details of which properties and methods are available to override - the craft
behavior can be modified substantially this way. ```
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 ```python
@ -73,7 +92,7 @@ behavior can be modified substantially this way.
class PigIronRecipe(CraftingRecipe): class PigIronRecipe(CraftingRecipe):
# Pig iron is a high-carbon result of melting iron in a blast furnace. # 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"] tool_tags = ["blast furnace"]
consumable_tags = ["iron ore", "coal", "coal"] consumable_tags = ["iron ore", "coal", "coal"]
output_prototypes = [ output_prototypes = [
@ -82,18 +101,26 @@ behavior can be modified substantially this way.
"tags": [("pig iron", "crafting_material")]} "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 If the above class was added to a module in `CRAFT_RECIPE_MODULES`, it could be
recipes. 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 copy import copy
from evennia.utils.utils import ( from evennia.utils.utils import iter_to_str, callables_from_module, inherits_from, make_iter
iter_to_str, callables_from_module, inherits_from, make_iter)
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command from evennia.commands.command import Command
from evennia.prototypes.spawner import spawn from evennia.prototypes.spawner import spawn
@ -109,6 +136,7 @@ def _load_recipes():
""" """
from django.conf import settings from django.conf import settings
global _RECIPE_CLASSES global _RECIPE_CLASSES
if not _RECIPE_CLASSES: if not _RECIPE_CLASSES:
paths = ["evennia.contrib.crafting.example_recipes"] paths = ["evennia.contrib.crafting.example_recipes"]
@ -126,12 +154,14 @@ class CraftingError(RuntimeError):
""" """
class CraftingValidationError(CraftingError): class CraftingValidationError(CraftingError):
""" """
Error if crafting validation failed. Error if crafting validation failed.
""" """
class CraftingRecipeBase: class CraftingRecipeBase:
""" """
The recipe handles all aspects of performing a 'craft' operation. This is The recipe handles all aspects of performing a 'craft' operation. This is
@ -164,6 +194,7 @@ class CraftingRecipeBase:
""" """
name = "recipe base" name = "recipe base"
# if set, allow running `.craft` more than once on the same instance. # if set, allow running `.craft` more than once on the same instance.
@ -436,6 +467,7 @@ class CraftingRecipe(CraftingRecipeBase):
shown to the crafter automatically shown to the crafter automatically
""" """
name = "crafting recipe" name = "crafting recipe"
# this define the overall category all material tags must have # 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_tool_missing_message = "Could not craft {outputs} without {missing}."
# error to show if tool-order matters and it was wrong. Missing is the first # error to show if tool-order matters and it was wrong. Missing is the first
# tool out of order # tool out of order
error_tool_order_message = \ error_tool_order_message = (
"Could not craft {outputs} since {missing} was added in the wrong order." "Could not craft {outputs} since {missing} was added in the wrong order."
)
# if .exact_tools is set and there are more than needed # 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})." "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 # 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. # 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_consumable_missing_message = "Could not craft {outputs} without {missing}."
# error to show if consumable order matters and it was wrong. Missing is the first # error to show if consumable order matters and it was wrong. Missing is the first
# consumable out of order # consumable out of order
error_consumable_order_message = \ error_consumable_order_message = (
"Could not craft {outputs} since {missing} was added in the wrong order." "Could not craft {outputs} since {missing} was added in the wrong order."
)
# if .exact_consumables is set and there are more than needed # 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})." "Could not craft {outputs} without the exact ingredients (extra {excess})."
)
# this is a list of one or more prototypes (prototype_keys to existing # 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 # 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 # show after a successful craft
success_message = "You successfully craft {outputs}!" success_message = "You successfully craft {outputs}!"
def __init__(self, crafter, *inputs, **kwargs): def __init__(self, crafter, *inputs, **kwargs):
""" """
Args: Args:
crafter (Object): The one doing the crafting. crafter (Object): The one doing the crafting.
@ -528,32 +564,37 @@ class CraftingRecipe(CraftingRecipeBase):
# validate class properties # validate class properties
if self.consumable_names: if self.consumable_names:
assert len(self.consumable_names) == len(self.consumable_tags), \ assert len(self.consumable_names) == len(self.consumable_tags), (
f"Crafting {self.__class__}.consumable_names list must " \ f"Crafting {self.__class__}.consumable_names list must "
"have the same length as .consumable_tags." "have the same length as .consumable_tags."
)
else: else:
self.consumable_names = self.consumable_tags self.consumable_names = self.consumable_tags
if self.tool_names: if self.tool_names:
assert len(self.tool_names) == len(self.tool_tags), \ assert len(self.tool_names) == len(self.tool_tags), (
f"Crafting {self.__class__}.tool_names list must " \ f"Crafting {self.__class__}.tool_names list must "
"have the same length as .tool_tags." "have the same length as .tool_tags."
)
else: else:
self.tool_names = self.tool_tags self.tool_names = self.tool_tags
if self.output_names: if self.output_names:
assert len(self.consumable_names) == len(self.consumable_tags), \ assert len(self.consumable_names) == len(self.consumable_tags), (
f"Crafting {self.__class__}.output_names list must " \ f"Crafting {self.__class__}.output_names list must "
"have the same length as .output_prototypes." "have the same length as .output_prototypes."
)
else: else:
self.output_names = [ self.output_names = [
prot.get("key", prot.get("typeclass", "unnamed")) 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 for prot in self.output_prototypes
] ]
assert isinstance(self.output_prototypes, (list, tuple)), \ assert isinstance(
"Crafting {self.__class__}.output_prototypes must be a list or tuple." 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 # don't allow reuse if we have consumables. If only tools we can reuse
# over and over since nothing changes. # over and over since nothing changes.
@ -568,14 +609,15 @@ class CraftingRecipe(CraftingRecipeBase):
# build template context # build template context
mapping = {"missing": missing, "excess": excess} mapping = {"missing": missing, "excess": excess}
mapping.update({ mapping.update(
f"i{ind}": self.consumable_names[ind] {
for ind, name in enumerate(self.consumable_names or self.consumable_tags) 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"o{ind}": self.output_names[ind] for ind, name in enumerate(self.output_names)}
)
mapping["tools"] = involved_tools mapping["tools"] = involved_tools
mapping["consumables"] = involved_cons mapping["consumables"] = involved_cons
@ -633,18 +675,17 @@ class CraftingRecipe(CraftingRecipeBase):
create_object( create_object(
key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()), key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
tags=[(tag, cls.tool_tag_category), *tool_tags], tags=[(tag, cls.tool_tag_category), *tool_tags],
**tool_kwargs **tool_kwargs,
) )
) )
consumables = [] consumables = []
for itag, tag in enumerate(cls.consumable_tags): for itag, tag in enumerate(cls.consumable_tags):
consumables.append( consumables.append(
create_object( create_object(
key=cons_key or (cls.consumable_names[itag] if key=cons_key
cls.consumable_names else or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
tag.capitalize()),
tags=[(tag, cls.consumable_tag_category), *cons_tags], tags=[(tag, cls.consumable_tag_category), *cons_tags],
**consumable_kwargs **consumable_kwargs,
) )
) )
return tools, consumables return tools, consumables
@ -669,8 +710,15 @@ class CraftingRecipe(CraftingRecipeBase):
""" """
def _check_completeness( def _check_completeness(
tagmap, taglist, namelist, exact_match, exact_order, tagmap,
error_missing_message, error_order_message, error_excess_message): taglist,
namelist,
exact_match,
exact_order,
error_missing_message,
error_order_message,
error_excess_message,
):
"""Compare tagmap (inputs) to taglist (required)""" """Compare tagmap (inputs) to taglist (required)"""
valids = [] valids = []
for itag, tagkey in enumerate(taglist): for itag, tagkey in enumerate(taglist):
@ -682,8 +730,8 @@ class CraftingRecipe(CraftingRecipeBase):
if exact_order: if exact_order:
# if we get here order is wrong # if we get here order is wrong
err = self._format_message( err = self._format_message(
error_order_message, error_order_message, missing=obj.get_display_name(looker=self.crafter)
missing=obj.get_display_name(looker=self.crafter)) )
self.msg(err) self.msg(err)
raise CraftingValidationError(err) raise CraftingValidationError(err)
@ -694,7 +742,8 @@ class CraftingRecipe(CraftingRecipeBase):
elif exact_match: elif exact_match:
err = self._format_message( err = self._format_message(
error_missing_message, error_missing_message,
missing=namelist[itag] if namelist else tagkey.capitalize()) missing=namelist[itag] if namelist else tagkey.capitalize(),
)
self.msg(err) self.msg(err)
raise CraftingValidationError(err) raise CraftingValidationError(err)
@ -703,21 +752,30 @@ class CraftingRecipe(CraftingRecipeBase):
# thus this is not an exact match # thus this is not an exact match
err = self._format_message( err = self._format_message(
error_excess_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) self.msg(err)
raise CraftingValidationError(err) raise CraftingValidationError(err)
return valids return valids
# get tools and consumables from self.inputs # get tools and consumables from self.inputs
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True) tool_map = {
for obj in self.inputs if obj and hasattr(obj, "tags") and obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
inherits_from(obj, "evennia.objects.models.ObjectDB")} 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} 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) consumable_map = {
for obj in self.inputs obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
if obj and hasattr(obj, "tags") and obj not in tool_map and for obj in self.inputs
inherits_from(obj, "evennia.objects.models.ObjectDB")} 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} 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, # 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. # all the recipe needs now.
if len(tools) != len(self.tool_tags): if len(tools) != len(self.tool_tags):
raise CraftingValidationError( 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): if len(consumables) != len(self.consumable_tags):
raise CraftingValidationError( raise CraftingValidationError(
f"Consumables {consumables}'s tags do not match " 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_tools = tools
self.validated_consumables = consumables self.validated_consumables = consumables
@ -816,25 +876,29 @@ class CraftingRecipe(CraftingRecipeBase):
def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs): def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
""" """
Craft a given recipe from a source recipe module. A recipe module is a Access function. Craft a given recipe from a source recipe module. A
Python module containing recipe classes. Note that this requires recipe module is a Python module containing recipe classes. Note that this
`settings.CRAFT_RECIPE_MODULES` to be added to a list of one or more requires `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or
python-paths to modules holding Recipe-classes. more python-paths to modules holding Recipe-classes.
Args: Args:
crafter (Object): The one doing the crafting. crafter (Object): The one doing the crafting.
recipe_name (str): The `CraftRecipe.name` to use. recipe_name (str): The `CraftRecipe.name` to use. This uses fuzzy-matching
*inputs: Suitable ingredients (Objects) to use in the crafting. 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 raise_exception (bool, optional): If crafting failed for whatever
reason, raise `CraftingError`. The user will still be informed by the recipe. reason, raise `CraftingError`. The user will still be informed by the
**kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft). recipe.
**kwargs: Optional kwargs to pass into the recipe (will passed into
recipe.craft).
Returns: Returns:
list: Crafted objects, if any. list: Crafted objects, if any.
Raises: Raises:
CraftingError: If `raise_exception` is True and crafting failed to produce an output. CraftingError: If `raise_exception` is True and crafting failed to
KeyError: If `recipe_name` failed to find a matching recipe class. produce an output. KeyError: If `recipe_name` failed to find a
matching recipe class (or the hit was not precise enough.)
Notes: Notes:
If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and 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) RecipeClass = _RECIPE_CLASSES.get(recipe_name, None)
if not RecipeClass: if not RecipeClass:
raise KeyError("No recipe in settings.CRAFT_RECIPE_MODULES " # try a startswith fuzzy match
f"has a name matching {recipe_name}") 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) recipe = RecipeClass(crafter, *inputs, **kwargs)
return recipe.craft(raise_exception=raise_exception) return recipe.craft(raise_exception=raise_exception)
# craft command/cmdset # craft command/cmdset
class CraftingCmdSet(CmdSet): class CraftingCmdSet(CmdSet):
""" """
Store crafting command. Store crafting command.
""" """
key = "Crafting cmdset" key = "Crafting cmdset"
def at_cmdset_creation(self): 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): def parse(self):
""" """
Handle parsing of Handle parsing of:
:: ::
<recipe> [FROM <ingredients>] [USING <tools>] <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() self.args = args = self.args.strip().lower()
recipe, ingredients, tools = "", "", "" recipe, ingredients, tools = "", "", ""
if 'from' in args: if "from" in args:
recipe, *rest = args.split(" from ", 1) recipe, *rest = args.split(" from ", 1)
rest = rest[0] if rest else "" rest = rest[0] if rest else ""
ingredients, *tools = rest.split(" using ", 1) ingredients, *tools = rest.split(" using ", 1)
elif 'using' in args: elif "using" in args:
recipe, *tools = args.split(" using ", 1) recipe, *tools = args.split(" using ", 1)
tools = tools[0] if tools else "" tools = tools[0] if tools else ""
@ -931,13 +1020,19 @@ class CmdCraft(Command):
# try to include characters or accounts etc. # try to include characters or accounts etc.
if not obj: if not obj:
return return
if (not inherits_from(obj, "evennia.objects.models.ObjectDB") if (
or obj.sessions.all() or not obj.access(caller, "craft", default=True)): 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 # We don't allow to include puppeted objects nor those with the
# 'negative' permission 'nocraft'. # 'negative' permission 'nocraft'.
caller.msg(obj.attributes.get( caller.msg(
"crafting_consumable_err_msg", obj.attributes.get(
default=f"{obj.get_display_name(looker=caller)} can't be used for this.")) "crafting_consumable_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
)
)
return return
ingredients.append(obj) ingredients.append(obj)
@ -950,9 +1045,12 @@ class CmdCraft(Command):
if not obj: if not obj:
return None return None
if not obj.access(caller, "craft", default=True): if not obj.access(caller, "craft", default=True):
caller.msg(obj.attributes.get( caller.msg(
"crafting_tool_err_msg", obj.attributes.get(
default=f"{obj.get_display_name(looker=caller)} can't be used for this.")) "crafting_tool_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
)
)
return return
tools.append(obj) 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 See the `SwordSmithingBaseRecipe` in this module for an example of extendng the
mocked 'skill' system (just random chance in our case). The skill system used recipe with a mocked 'skill' system (just random chance in our case). The skill
is game-specific but likely to be needed for most 'real' crafting systems. 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 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 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 reference to a blast furnace used, not suggesting the crafter is carrying it
around with them. around with them.
:: ## Sword crafting tree
Sword crafting tree ::
# base materials (consumables) # base materials (consumables)
@ -40,6 +41,7 @@ around with them.
sword = sword blade + sword guard + sword pommel sword = sword blade + sword guard + sword pommel
+ sword handle + leather + knife[T] + hammer[T] + furnace[T] + 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. Pig iron is a high-carbon result of melting iron in a blast furnace.
""" """
name = "pig iron" name = "pig iron"
tool_tags = ["blast furnace"] tool_tags = ["blast furnace"]
consumable_tags = ["iron ore", "coal", "coal"] consumable_tags = ["iron ore", "coal", "coal"]
output_prototypes = [ output_prototypes = [
{"key": "Pig Iron ingot", {
"desc": "An ingot of crude pig iron.", "key": "Pig Iron ingot",
"tags": [("pig iron", "crafting_material")]} "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). crucible produces a medieval level of steel (like damascus steel).
""" """
name = "crucible steel" name = "crucible steel"
tool_tags = ["crucible"] tool_tags = ["crucible"]
consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"] consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"]
output_prototypes = [ output_prototypes = [
{"key": "Crucible steel ingot", {
"desc": "An ingot of multi-colored crucible steel.", "key": "Crucible steel ingot",
"tags": [("crucible steel", "crafting_material")]} "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}!" 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. " failed_message = (
"You need to start over.") "You work and work but you are not happy with the result. You need to start over."
)
def do_craft(self, **kwargs): def do_craft(self, **kwargs):
""" """
@ -130,28 +139,35 @@ class SwordBladeRecipe(_SwordSmithingBaseRecipe):
part of the sword you hold on to). part of the sword you hold on to).
""" """
name = "sword blade" name = "sword blade"
tool_tags = ["hammer", "anvil", "furnace"] tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"] consumable_tags = ["crucible steel"]
output_prototypes = [ output_prototypes = [
{"key": "Sword blade", {
"desc": "A long blade that may one day become a sword.", "key": "Sword blade",
"tags": [("sword blade", "crafting_material")]} "desc": "A long blade that may one day become a sword.",
"tags": [("sword blade", "crafting_material")],
}
] ]
class SwordPommelRecipe(_SwordSmithingBaseRecipe): class SwordPommelRecipe(_SwordSmithingBaseRecipe):
""" """
The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding
it together. it together.
""" """
name = "sword pommel" name = "sword pommel"
tool_tags = ["hammer", "anvil", "furnace"] tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"] consumable_tags = ["crucible steel"]
output_prototypes = [ output_prototypes = [
{"key": "Sword pommel", {
"desc": "The pommel for a future sword.", "key": "Sword pommel",
"tags": [("sword pommel", "crafting_material")]} "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. sword's blade and also protects the hand when parrying.
""" """
name = "sword guard" name = "sword guard"
tool_tags = ["hammer", "anvil", "furnace"] tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"] consumable_tags = ["crucible steel"]
output_prototypes = [ output_prototypes = [
{"key": "Sword guard", {
"desc": "The cross-guard for a future sword.", "key": "Sword guard",
"tags": [("sword guard", "crafting_material")]} "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. Rawhide is animal skin cleaned and stripped of hair.
""" """
name = "rawhide" name = "rawhide"
tool_tags = ["knife"] tool_tags = ["knife"]
consumable_tags = ["fur"] consumable_tags = ["fur"]
output_prototypes = [ output_prototypes = [
{"key": "Rawhide", {
"desc": "Animal skin, cleaned and with hair removed.", "key": "Rawhide",
"tags": [("rawhide", "crafting_material")]} "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. This produces two outputs - the bark and the cleaned wood.
""" """
name = "oak bark" name = "oak bark"
tool_tags = ["knife"] tool_tags = ["knife"]
consumable_tags = ["oak wood"] consumable_tags = ["oak wood"]
output_prototypes = [ output_prototypes = [
{"key": "Oak bark", {
"desc": "Bark of oak, stripped from the core wood.", "key": "Oak bark",
"tags": [("oak bark", "crafting_material")]}, "desc": "Bark of oak, stripped from the core wood.",
{"key": "Oak Wood (cleaned)", "tags": [("oak bark", "crafting_material")],
"desc": "Oakwood core, stripped of bark.", },
"tags": [("cleaned oak wood", "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 ... 'tanning rack' tool should be required too ...
""" """
name = "leather" name = "leather"
tool_tags = ["cauldron"] tool_tags = ["cauldron"]
consumable_tags = ["rawhide", "oak bark", "water"] consumable_tags = ["rawhide", "oak bark", "water"]
output_prototypes = [ output_prototypes = [
{"key": "Piece of Leather", {
"desc": "A piece of leather.", "key": "Piece of Leather",
"tags": [("leather", "crafting_material")]} "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. is wrapped in leather, but that will be added at the end.
""" """
name = "sword handle" name = "sword handle"
tool_tags = ["knife"] tool_tags = ["knife"]
consumable_tags = ["cleaned oak wood"] consumable_tags = ["cleaned oak wood"]
output_prototypes = [ output_prototypes = [
{"key": "Sword handle", {
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.", "key": "Sword handle",
"tags": [("sword handle", "crafting_material")]} "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. This covers only a single 'sword' type.
""" """
name = "sword" name = "sword"
tool_tags = ["hammer", "furnace", "knife"] tool_tags = ["hammer", "furnace", "knife"]
consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", "leather"]
"leather"]
output_prototypes = [ output_prototypes = [
{"key": "Sword", {
"desc": "A bladed weapon.", "key": "Sword",
# setting the tag as well - who knows if one can make something from this too! "desc": "A bladed weapon.",
"tags": [("sword", "crafting_material")]} # setting the tag as well - who knows if one can make something from this too!
# obviously there would be other properties of a 'sword' added here "tags": [("sword", "crafting_material")],
# too, depending on how combat works in the your game! }
# 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 # this requires more precision
exact_consumable_order = True exact_consumable_order = True

View file

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