From 6e1355ed2000fd0c6cd5e31406aef35b197be4ba Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 13 May 2021 10:39:45 +0200 Subject: [PATCH] Some updates to the crafting contrib readme --- docs/source/Contribs/Crafting.md | 110 +++++++++++++-------------- evennia/contrib/crafting/README.md | 106 +++++++++++++++++++++++--- evennia/contrib/crafting/crafting.py | 15 ++-- evennia/contrib/crafting/tests.py | 6 +- 4 files changed, 159 insertions(+), 78 deletions(-) diff --git a/docs/source/Contribs/Crafting.md b/docs/source/Contribs/Crafting.md index b3d909564d..7b2950e23f 100644 --- a/docs/source/Contribs/Crafting.md +++ b/docs/source/Contribs/Crafting.md @@ -1,39 +1,39 @@ -# Crafting system contrib +# 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. +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. -- 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. -From in-game it uses the new `craft` command: +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 [from ,...][ using ,...]`. +The syntax is `craft [from ,...][ using ,...]`. -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 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 `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: +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 +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: @@ -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 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 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. +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. ```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 +(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: @@ -80,23 +80,23 @@ class WoodenPuppetRecipe(CraftingRecipe): {"key": "A carved wooden doll", "typeclass": "typeclasses.objects.decorations.Toys", "desc": "A small carved doll"} - ] + ] ``` -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). -Instead of specifying the full prototype-dict, you could also just provide a list of `prototype_key`s to +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). +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 to insert into the recipe. +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. -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 -`.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 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 +`.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 @@ -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")]) ``` -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 +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. 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() ``` -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. -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 > craft wooden puppet from wood using hobby knife ``` -In code we would do +In code we would do ```python 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 -is which based on their tags. +is which based on their tags. ## Deeper customization of recipes @@ -147,36 +147,36 @@ 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`. +This is useful for testing and allows you to use the class directly without adding it to a module +in `settings.CRAFTING_RECIPE_MODULES`. -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 +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 +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 +- `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 +- `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 `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 +- `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 +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 +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. @@ -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,15 +201,15 @@ 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 [] ``` -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. +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. @@ -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) 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`. diff --git a/evennia/contrib/crafting/README.md b/evennia/contrib/crafting/README.md index 3d5b40895c..9f89b2935c 100644 --- a/evennia/contrib/crafting/README.md +++ b/evennia/contrib/crafting/README.md @@ -1,20 +1,102 @@ -# Crafting system +# Crafting system 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 [from ,...] [using , ...] + +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. diff --git a/evennia/contrib/crafting/crafting.py b/evennia/contrib/crafting/crafting.py index 92ef2ff1d6..13260d268f 100644 --- a/evennia/contrib/crafting/crafting.py +++ b/evennia/contrib/crafting/crafting.py @@ -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" diff --git a/evennia/contrib/crafting/tests.py b/evennia/contrib/crafting/tests.py index 7c5ac6e4ff..069a528431 100644 --- a/evennia/contrib/crafting/tests.py +++ b/evennia/contrib/crafting/tests.py @@ -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):