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

@ -161,11 +161,11 @@ 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
- `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
- `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.
- `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.
@ -189,7 +189,7 @@ class SkillRecipe(CraftingRecipe):
difficulty = 20
def do_craft(self, **kwargs):
def craft(self, **kwargs):
"""The input is ok. Determine if crafting succeeds"""
# this is set at initialization
@ -201,7 +201,7 @@ class SkillRecipe(CraftingRecipe):
# roll for success:
if randint(1, 100) <= (crafting_skill - self.difficulty):
# all is good, craft away
return super().do_craft()
return super().craft()
else:
self.msg("You are not good enough to craft this. Better luck next time!")
return []

View file

@ -4,17 +4,99 @@ Contrib - Griatch 2020
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
input is a list of object-tags (using the "crafting_material" tag-category)
and objects passing this check must be passed into the recipe.
Here, 'ingredients' are consumed by the crafting process, whereas 'tools' are
necessary for the process by will not be destroyed by it.
The output is given by a set of prototypes. 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 objects being 'crafted'.
An example would be to use the tools 'bowl' and 'oven' to use the ingredients
'flour', 'salt', 'yeast' and 'water' to create 'bread' using the 'bread recipe'.
Each recipe is a stand-alone entity which allows for very advanced customization
for every recipe - for example one could have a recipe where the input ingredients
are not destroyed in the process, or which require other properties of the input
(such as a 'quality').
A recipe does not have to use tools, like 'snow' + 'snowball-recipe' becomes
'snowball'. Conversely one could also imagine using tools without consumables,
like using 'spell book' and 'wand' to produce 'fireball' by having the recipe
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.
2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in
`.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.
5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft`
raised a `CraftingError` or `CraftingValidationError`.
@ -252,7 +252,7 @@ class CraftingRecipeBase:
else:
raise CraftingValidationError
def do_craft(self, **kwargs):
def craft(self, **kwargs):
"""
Hook to override.
@ -277,7 +277,7 @@ class CraftingRecipeBase:
method is to delete the inputs.
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.
Returns:
@ -324,7 +324,7 @@ class CraftingRecipeBase:
if raise_exception:
raise
else:
craft_result = self.do_craft(**craft_kwargs)
craft_result = self.craft(**craft_kwargs)
finally:
craft_result = self.post_craft(craft_result, **craft_kwargs)
except (CraftingError, CraftingValidationError):
@ -455,7 +455,7 @@ class CraftingRecipe(CraftingRecipeBase):
3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
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.
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
@ -819,7 +819,7 @@ class CraftingRecipe(CraftingRecipeBase):
self.validated_tools = tools
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`
fails.
@ -847,7 +847,7 @@ class CraftingRecipe(CraftingRecipeBase):
this method is to delete the inputs.
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`.
Returns:
@ -958,7 +958,6 @@ class CmdCraft(Command):
things in the current location, like a furnace, windmill or anvil.
"""
key = "craft"
locks = "cmd:all()"
help_category = "General"

View file

@ -91,7 +91,7 @@ class TestCraftingRecipeBase(TestCase):
"""Test craft hook, the main access method."""
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)
@ -99,7 +99,7 @@ class TestCraftingRecipeBase(TestCase):
# check 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
self.assertFalse(self.recipe.allow_craft)
@ -110,7 +110,7 @@ class TestCraftingRecipeBase(TestCase):
def test_craft_hook__fail(self):
"""Test failing the call"""
self.recipe.do_craft = mock.MagicMock(return_value=None)
self.recipe.craft = mock.MagicMock(return_value=None)
# trigger exception
with self.assertRaises(crafting.CraftingError):