Some updates to the crafting contrib readme

This commit is contained in:
Griatch 2021-05-13 10:39:45 +02:00
parent 055bbcfee3
commit 8e19017dc3
4 changed files with 159 additions and 78 deletions

View file

@ -1,39 +1,39 @@
# Crafting system contrib # Crafting system contrib
_Contrib by Griatch 2020_ _Contrib by Griatch 2020_
```versionadded:: 1.0 ```versionadded:: 1.0
``` ```
This contrib implements a full Crafting system that can be expanded and modified to fit your game. 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 - See the [evennia/contrib/crafting/crafting.py API](api:evennia.contrib.crafting.crafting) for installation
instructrions. instructrions.
- See the [sword example](api:evennia.contrib.crafting.example_recipes) for an example of how to design - 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. a crafting tree for crafting a sword from base elements.
From in-game it uses the new `craft` command: From in-game it uses the new `craft` command:
```bash ```bash
> craft bread from flour, eggs, salt, water, yeast using oven, roller > craft bread from flour, eggs, salt, water, yeast using oven, roller
> craft bandage from cloth using scissors > craft bandage from cloth using scissors
``` ```
The syntax is `craft <recipe> [from <ingredient>,...][ using <tool>,...]`. 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 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. 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 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 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. 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: If you wanted, you could also picture recipes without any consumables:
``` ```
> craft fireball using wand, spellbook > craft fireball using wand, spellbook
``` ```
With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or
magic systems. magic systems.
In code, you can craft using the `evennia.contrib.crafting.crafting.craft` function: In code, you can craft using the `evennia.contrib.crafting.crafting.craft` function:
@ -46,22 +46,22 @@ result = craft(caller, "recipename", *inputs)
``` ```
Here, `caller` is the one doing the crafting and `*inputs` is any combination of consumables and/or tool 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) Objects. The system will identify which is which by the [Tags](../Components/Tags) on them (see below)
The `result` is always a list. The `result` is always a list.
## Adding new recipes ## Adding new recipes
A *recipe* is a class inheriting from `evennia.contrib.crafting.crafting.CraftingRecipe`. This class 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 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. 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 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. line to your `mygame/server/conf/settings.py` file, with a list to any new modules with recipe classes.
```python ```python
CRAFT_RECIPE_MODULES = ["world.myrecipes"] CRAFT_RECIPE_MODULES = ["world.myrecipes"]
``` ```
(You need to reload after adding this). All global-level classes in these modules (whose names don't start (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. 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: Here we assume you created `mygame/world/myrecipes.py` to match the above example setting:
@ -80,23 +80,23 @@ class WoodenPuppetRecipe(CraftingRecipe):
{"key": "A carved wooden doll", {"key": "A carved wooden doll",
"typeclass": "typeclasses.objects.decorations.Toys", "typeclass": "typeclasses.objects.decorations.Toys",
"desc": "A small carved doll"} "desc": "A small carved doll"}
] ]
``` ```
This specifies which tags to look for in the inputs. It defines a [Prototype](../Components/Prototypes) This specifies which tags to look for in the inputs. It defines a [Prototype](../Components/Prototypes)
for the recipe to use to spawn the result on the fly (a recipe could spawn more than one result if needed). for the recipe to use 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 Instead of specifying the full prototype-dict, you could also just provide a list of `prototype_key`s to
existing prototypes you have. existing prototypes you have.
After reloading the server, this recipe would now be available to use. To try it we should After reloading the server, this recipe would now be available to use. To try it we should
create materials and tools to insert into the recipe. create materials and tools to insert into the recipe.
The recipe analyzes inputs, looking for [Tags](../Components/Tags) with specific tag-categories. The recipe analyzes inputs, looking for [Tags](../Components/Tags) with specific tag-categories.
The tag-category used can be set per-recipe using the (`.consumable_tag_category` and 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 `.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: the puppet we need one object with the `wood` tag and another with the `knife` tag:
```python ```python
from evennia import create_object from evennia import create_object
@ -105,9 +105,9 @@ knife = create_object(key="Hobby knife", tags=[("knife", "crafting_tool")])
wood = create_object(key="Piece of wood", tags[("wood", "crafting_material")]) 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 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 "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 potentially interesting for use in puzzles and to allow users to experiment and find alternatives to
know ingredients. know ingredients.
By the way, there is also a simple shortcut for doing this: By the way, there is also a simple shortcut for doing this:
@ -116,15 +116,15 @@ By the way, there is also a simple shortcut for doing this:
tools, consumables = WoodenPuppetRecipe.seed() tools, consumables = WoodenPuppetRecipe.seed()
``` ```
The `seed` class-method will create simple dummy objects that fulfills the recipe's requirements. This The `seed` class-method will create simple dummy objects that fulfills the recipe's requirements. This
is great for testing. is great for testing.
Assuming these objects were put in our inventory, we could now craft using the in-game command: Assuming these objects were put in our inventory, we could now craft using the in-game command:
```bash ```bash
> craft wooden puppet from wood using hobby knife > craft wooden puppet from wood using hobby knife
``` ```
In code we would do In code we would do
```python ```python
from evennia.contrub.crafting.crafting import craft from evennia.contrub.crafting.crafting import craft
@ -132,7 +132,7 @@ 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 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. is which based on their tags.
## Deeper customization of recipes ## Deeper customization of recipes
@ -147,36 +147,36 @@ recipe = MyRecipe(crafter, *(tools + consumables))
result = recipe.craft() result = recipe.craft()
``` ```
This is useful for testing and allows you to use the class directly without adding it to a module This is useful for testing and allows you to use the class directly without adding it to a module
in `settings.CRAFTING_RECIPE_MODULES`. in `settings.CRAFTING_RECIPE_MODULES`.
Even without modifying more than the class properties, there are a lot of options to set on 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 the `CraftingRecipe` class. Easiest is to refer to the
[CraftingRecipe api documentation](evennia.contrib.crafting.crafting.html#evennia.contrib.crafting.crafting.CraftingRecipe). [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 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. 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: 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 - `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 `validated_tools` respectively. On error, this reports the error to the crafter and raises the
`CraftingValidationError`. `CraftingValidationError`.
- `do_craft` - this will only be called if `pre_craft` finished without an exception. This should - `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 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 fails for some reason. This is the place to add skill-checks or random chance if you need it
for your game. for your game.
- `post_craft` - this receives the result from `do_craft` and handles error messages and also deletes - `post_craft` - this receives the result from `craft` and handles error messages and also deletes
any consumables as needed. It may also modify the result before returning it. 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 - `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. 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 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. into each crafting hook. These are unused by default but could be used to customize things per-call.
### Skilled crafters ### Skilled crafters
What the crafting system does not have out of the box is a 'skill' system - the notion of being able 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 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. this you need to make your own recipe parent class and have your recipes inherit from this.
@ -189,7 +189,7 @@ class SkillRecipe(CraftingRecipe):
difficulty = 20 difficulty = 20
def do_craft(self, **kwargs): def craft(self, **kwargs):
"""The input is ok. Determine if crafting succeeds""" """The input is ok. Determine if crafting succeeds"""
# this is set at initialization # this is set at initialization
@ -201,15 +201,15 @@ class SkillRecipe(CraftingRecipe):
# roll for success: # roll for success:
if randint(1, 100) <= (crafting_skill - self.difficulty): if randint(1, 100) <= (crafting_skill - self.difficulty):
# all is good, craft away # all is good, craft away
return super().do_craft() return super().craft()
else: else:
self.msg("You are not good enough to craft this. Better luck next time!") self.msg("You are not good enough to craft this. Better luck next time!")
return [] return []
``` ```
In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see 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 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 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. 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 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. of a random skill-check being implemented in a parent and then inherited for multiple use.
@ -218,5 +218,5 @@ of a random skill-check being implemented in a parent and then inherited for mul
If you want to build something even more custom (maybe using different input types of validation logic) If you want to build something even more custom (maybe using different input types of validation logic)
you could also look at the `CraftingRecipe` parent class `CraftingRecipeBase`. you could also look at the `CraftingRecipe` parent class `CraftingRecipeBase`.
It implements just the minimum needed to be a recipe and for big changes you may be better off starting It implements just the minimum needed to be a recipe and for big changes you may be better off starting
from this rather than the more opinionated `CraftingRecipe`. from this rather than the more opinionated `CraftingRecipe`.

View file

@ -1,20 +1,102 @@
# Crafting system # Crafting system
Contrib - Griatch 2020 Contrib - Griatch 2020
This implements a full crafting system. The principle is that of a 'recipe': This implements a full crafting system. The principle is that of a 'recipe':
object1 + object2 + ... -> craft_recipe -> objectA, objectB, ... ingredient1 + ingredient2 + ... + tool1 + tool2 + ... + craft_recipe -> objectA, objectB, ...
The recipe is a class that specifies input and output hooks. By default the Here, 'ingredients' are consumed by the crafting process, whereas 'tools' are
input is a list of object-tags (using the "crafting_material" tag-category) necessary for the process by will not be destroyed by it.
and objects passing this check must be passed into the recipe.
The output is given by a set of prototypes. If the input is correct and other An example would be to use the tools 'bowl' and 'oven' to use the ingredients
checks are passed (such as crafting skill, for example), these prototypes will 'flour', 'salt', 'yeast' and 'water' to create 'bread' using the 'bread recipe'.
be used to generate the new objects being 'crafted'.
Each recipe is a stand-alone entity which allows for very advanced customization A recipe does not have to use tools, like 'snow' + 'snowball-recipe' becomes
for every recipe - for example one could have a recipe where the input ingredients 'snowball'. Conversely one could also imagine using tools without consumables,
are not destroyed in the process, or which require other properties of the input like using 'spell book' and 'wand' to produce 'fireball' by having the recipe
(such as a 'quality'). check some magic skill on the character.
The system is generic enough to be used also for adventure-like puzzles, like
combining 'stick', 'string' and 'hook' to get a 'makeshift fishing rod' that
you can use with 'storm drain' (treated as a tool) to get 'key' ...
## Intallation and Usage
Import the `CmdCraft` command from evennia/contrib/crafting/crafting.py and
add it to your Character cmdset. Reload and the `craft` command will be
available to you:
craft <recipe> [from <ingredient>,...] [using <tool>, ...]
For example
craft toy car from plank, wooden wheels, nails using saw, hammer
To use crafting you need recipes. Add a new variable to `mygame/server/conf/settings.py`:
CRAFT_RECIPE_MODULES = ['world.recipes']
All top-level classes in these modules (whose name does not start with `_`)
will be parsed by Evennia as recipes to make available to the crafting system.
Using the above example, create `mygame/world/recipes.py` and add your recipies
in there:
```python
from evennia.contrib.crafting.crafting import CraftingRecipe, CraftingValidationError
class RecipeBread(CraftingRecipe):
"""
Bread is good for making sandwitches!
"""
name = "bread" # used to identify this recipe in 'craft' command
tool_tags = ["bowl", "oven"]
consumable_tags = ["flour", "salt", "yeast", "water"]
output_prototypes = [
{"key": "Loaf of Bread",
"aliases": ["bread"],
"desc": "A nice load of bread.",
"typeclass": "typeclasses.objects.Food", # assuming this exists
"tags": [("bread", "crafting_material")] # this makes it usable in other recipes ...
}
]
def pre_craft(self, **kwargs):
# validates inputs etc. Raise `CraftingValidationError` if fails
def craft(self, **kwargs):
# performs the craft - but it can still fail (check skills etc here)
def craft(self, result, **kwargs):
# any post-crafting effects. Always called, even if crafting failed (be
# result would be None then)
```
## Technical
The Recipe is a class that specifies the consumables, tools and output along
with various methods (that you can override) to do the the validation of inputs
and perform the crafting itself.
By default the input is a list of object-tags (using the "crafting_material"
and "crafting_tool" tag-categories respectively). Providing a set of objects
matching these tags are required for the crafting to be done. The use of tags
means that multiple different objects could all work for the same recipe, as
long as they have the right tag. This can be very useful for allowing players
to experiment and explore alternative ways to create things!
The output is given by a set of prototype-dicts. If the input is correct and
other checks are passed (such as crafting skill, for example), these prototypes
will be used to generate the new object(s) being crafted.
Each recipe is a stand-alone entity which allows for very advanced
customization for every recipe - for example one could have a recipe that
checks other properties of the inputs (like quality, color etc) and have that
affect the result. Your recipes could also (and likely would) tie into your
game's skill system to determine the success or outcome of the crafting.

View file

@ -186,7 +186,7 @@ class CraftingRecipeBase:
are optional but will be passed into all of the following hooks. are optional but will be passed into all of the following hooks.
2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in 2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in
`.validated_inputs.`. Raises `CraftingValidationError` otherwise. `.validated_inputs.`. Raises `CraftingValidationError` otherwise.
4. `.do_craft(**kwargs)` - should return the crafted item(s) or the empty list. Any 4. `.craft(**kwargs)` - should return the crafted item(s) or the empty list. Any
crafting errors should be immediately reported to user. crafting errors should be immediately reported to user.
5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft` 5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft`
raised a `CraftingError` or `CraftingValidationError`. raised a `CraftingError` or `CraftingValidationError`.
@ -252,7 +252,7 @@ class CraftingRecipeBase:
else: else:
raise CraftingValidationError raise CraftingValidationError
def do_craft(self, **kwargs): def craft(self, **kwargs):
""" """
Hook to override. Hook to override.
@ -277,7 +277,7 @@ class CraftingRecipeBase:
method is to delete the inputs. method is to delete the inputs.
Args: Args:
crafting_result (any): The outcome of crafting, as returned by `do_craft`. crafting_result (any): The outcome of crafting, as returned by `craft()`.
**kwargs: Any extra flags passed at initialization. **kwargs: Any extra flags passed at initialization.
Returns: Returns:
@ -324,7 +324,7 @@ class CraftingRecipeBase:
if raise_exception: if raise_exception:
raise raise
else: else:
craft_result = self.do_craft(**craft_kwargs) craft_result = self.craft(**craft_kwargs)
finally: finally:
craft_result = self.post_craft(craft_result, **craft_kwargs) craft_result = self.post_craft(craft_result, **craft_kwargs)
except (CraftingError, CraftingValidationError): except (CraftingError, CraftingValidationError):
@ -455,7 +455,7 @@ class CraftingRecipe(CraftingRecipeBase):
3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should 3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError` be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
otherwise. otherwise.
4. `.do_craft(**kwargs)` will not be called if validation failed. Should return 4. `.craft(**kwargs)` will not be called if validation failed. Should return
a list of the things crafted. a list of the things crafted.
5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation 5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation
failed (`crafting_result` will then be falsy). It does any cleanup. By default failed (`crafting_result` will then be falsy). It does any cleanup. By default
@ -819,7 +819,7 @@ class CraftingRecipe(CraftingRecipeBase):
self.validated_tools = tools self.validated_tools = tools
self.validated_consumables = consumables self.validated_consumables = consumables
def do_craft(self, **kwargs): def craft(self, **kwargs):
""" """
Hook to override. This will not be called if validation in `pre_craft` Hook to override. This will not be called if validation in `pre_craft`
fails. fails.
@ -847,7 +847,7 @@ class CraftingRecipe(CraftingRecipeBase):
this method is to delete the inputs. this method is to delete the inputs.
Args: Args:
craft_result (list): The crafted result, provided by `self.do_craft`. craft_result (list): The crafted result, provided by `self.craft()`.
**kwargs (any): Passed from `self.craft`. **kwargs (any): Passed from `self.craft`.
Returns: Returns:
@ -958,7 +958,6 @@ class CmdCraft(Command):
things in the current location, like a furnace, windmill or anvil. things in the current location, like a furnace, windmill or anvil.
""" """
key = "craft" key = "craft"
locks = "cmd:all()" locks = "cmd:all()"
help_category = "General" help_category = "General"

View file

@ -91,7 +91,7 @@ class TestCraftingRecipeBase(TestCase):
"""Test craft hook, the main access method.""" """Test craft hook, the main access method."""
expected_result = _TestMaterial("test_result") expected_result = _TestMaterial("test_result")
self.recipe.do_craft = mock.MagicMock(return_value=expected_result) self.recipe.craft = mock.MagicMock(return_value=expected_result)
self.assertTrue(self.recipe.allow_craft) self.assertTrue(self.recipe.allow_craft)
@ -99,7 +99,7 @@ class TestCraftingRecipeBase(TestCase):
# check result # check result
self.assertEqual(result, expected_result) self.assertEqual(result, expected_result)
self.recipe.do_craft.assert_called_with(kw1=1, kw2=2) self.recipe.craft.assert_called_with(kw1=1, kw2=2)
# since allow_reuse is False, this usage should now be turned off # since allow_reuse is False, this usage should now be turned off
self.assertFalse(self.recipe.allow_craft) self.assertFalse(self.recipe.allow_craft)
@ -110,7 +110,7 @@ class TestCraftingRecipeBase(TestCase):
def test_craft_hook__fail(self): def test_craft_hook__fail(self):
"""Test failing the call""" """Test failing the call"""
self.recipe.do_craft = mock.MagicMock(return_value=None) self.recipe.craft = mock.MagicMock(return_value=None)
# trigger exception # trigger exception
with self.assertRaises(crafting.CraftingError): with self.assertRaises(crafting.CraftingError):