Expand with many crafting unit tests

This commit is contained in:
Griatch 2020-11-28 17:11:49 +01:00
parent add5f90609
commit e890bd9040
5 changed files with 1032 additions and 176 deletions

View file

@ -63,7 +63,8 @@ Recipes are put in one or more modules added as a list to the
CRAFT_MODULE_RECIPES = ['world.recipes_weapons', 'world.recipes_potions']
Below is an example of a crafting recipe. See the `CraftingRecipe` class for
details of which properties and methods are available.
details of which properties and methods are available to override - the craft
behavior can be modified substantially this way.
```python
@ -93,6 +94,8 @@ recipes.
from copy import copy
from evennia.utils.utils import (
iter_to_str, callables_from_module, inherits_from, make_iter)
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.prototypes.spawner import spawn
from evennia.utils.create import create_object
@ -131,8 +134,11 @@ class CraftingValidationError(CraftingError):
class CraftingRecipeBase:
"""
This is the base of the crafting system. The recipe handles all aspects of
performing a 'craft' operation.
The recipe handles all aspects of performing a 'craft' operation. This is
the base of the crafting system, intended to be replace if you want to
adapt it for very different functionality - see the `CraftingRecipe` child
class for an implementation of the most common type of crafting using
objects.
Example of usage:
::
@ -144,6 +150,19 @@ class CraftingRecipeBase:
consumed - so in that case the recipe cannot be used a second time (doing so
will raise a `CraftingError`)
Process:
1. `.craft(**kwargs)` - this starts the process on the initialized recipe. The kwargs
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
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`.
Should return `crafted_result` (modified or not).
"""
name = "recipe base"
@ -151,8 +170,6 @@ class CraftingRecipeBase:
# don't set this unless crafting inputs are *not* consumed by the crafting
# process (otherwise subsequent calls will fail).
allow_reuse = False
# this is set to avoid re-validation if recipe is re-run
is_validated = False
def __init__(self, crafter, *inputs, **kwargs):
"""
@ -183,45 +200,27 @@ class CraftingRecipeBase:
"""
self.crafter.msg(message, {"type": "crafting"})
def validate_inputs(self, **kwargs):
def pre_craft(self, **kwargs):
"""
Hook to override.
Make sure the provided inputs are valid. This should always be run.
This should validate `self.inputs` which are the inputs given when
creating this recipe.
This is called just before crafting operation and is normally
responsible for validating the inputs, storing data on
`self.validated_inputs`.
Args:
**kwargs: These are optional extra flags passed during intialization.
**kwargs: Optional extra flags passed during initialization or
`.craft(**kwargs)`.
Raises:
CraftingValidationError: If validation fails.
Note:
This method should store validated results on the recipe for the
other hooks to access. It is also responsible for properly sending
error messages to e.g. self.crafter (usually via `self.msg`).
"""
if self.allow_craft:
self.validated_inputs = self.inputs[:]
else:
raise CraftingValidationError
def pre_craft(self, **kwargs):
"""
Hook to override.
This is called just before crafting operation, after inputs have been
validated. At this point the validated inputs are available on the
class instance.
Args:
**kwargs: Optional extra flags passed during initialization.
"""
pass
def do_craft(self, **kwargs):
"""
Hook to override.
@ -231,7 +230,6 @@ class CraftingRecipeBase:
inputs are available on this recipe instance.
Args:
validated_inputs (any): Data previously returned from `pre_craft`.
**kwargs: Any extra flags passed at initialization.
Returns:
@ -281,29 +279,31 @@ class CraftingRecipeBase:
"""
craft_result = None
err = ""
if self.allow_craft:
# override/extend craft_kwargs from initialization.
craft_kwargs = copy(self.craft_kwargs)
craft_kwargs.update(kwargs)
try:
# this assigns to self.validated_inputs
if not self.is_validated:
self.validate_inputs(**craft_kwargs)
self.is_validated = True
# run the crafting process - each accesses validated data directly
self.pre_craft(**craft_kwargs)
craft_result = self.do_craft(**craft_kwargs)
self.post_craft(craft_result, **craft_kwargs)
except (CraftingError, CraftingValidationError) as exc:
# use this to abort crafting early
try:
# this assigns to self.validated_inputs
self.pre_craft(**craft_kwargs)
except (CraftingError, CraftingValidationError):
if raise_exception:
raise
else:
craft_result = self.do_craft(**craft_kwargs)
finally:
craft_result = self.post_craft(craft_result, **craft_kwargs)
except (CraftingError, CraftingValidationError):
if raise_exception:
raise
# possibly turn off re-use depending on class setting
self.allow_craft = self.allow_reuse
elif not self.allow_reuse:
raise CraftingError("Cannot re-run crafting without refreshing recipe first.")
raise CraftingError("Cannot re-run crafting without re-initializing recipe first.")
if craft_result is None and raise_exception:
raise CraftingError(f"Crafting of {self.name} failed.")
return craft_result
@ -312,44 +312,52 @@ class CraftingRecipeBase:
class CraftingRecipe(CraftingRecipeBase):
"""
The CraftRecipe implements the most common form of crafting: Combining (and
optionally consuming) inputs to produce a new result. This type of recipe
only works with typeclassed entities as inputs and outputs, since it's
based on Tags and prototypes.
consuming) inputs to produce a new result. This type of recipe only works
with typeclassed entities as inputs and outputs, since it's based on Tags
and Prototypes.
There are two types of crafting ingredients: 'tools' and 'consumables'. The
difference between them is that the former is not consumed in the crafting
process. So if you need a hammer and anvil to craft a sword, they are 'tools'
whereas the materials of the sword are 'consumables'.
process. So if you need a hammer and anvil to craft a sword, they are
'tools' whereas the materials of the sword are 'consumables'.
Examples:
::
class SwordRecipe(CraftRecipe):
name = "sword"
input_tags = ["hilt", "pommel", "strips of leather", "sword blade"]
class FlourRecipe(CraftRecipe):
name = "flour"
tool_tags = ['windmill']
consumable_tags = ["wheat"]
output_prototypes = [
{"key": "sword",
"typeclass": "typeclassess.weapons.bladed.Sword",
"tags": [("sword", "weapon"), ("melee", "weapontype"),
("edged", "weapontype")]
{"key": "Bag of flour",
"typeclass": "typeclasses.food.Flour",
"desc": "A small bag of flour."
"tags": [("flour", "crafting_material"),
}
]
class BreadRecipe(CraftRecipe):
name = "bread"
tool_tags = ["roller", "owen"]
consumable_tags = ["flour", "egg", "egg", "salt", "water", "yeast"]
output_prototypes = [
{"key": "bread",
"desc": "A tasty bread."
}
## Properties on the class level:
- `name` (str): The name of this recipe. This should be globally unique.
### tools
- `tool_tag_category` (str): What tag-category tools must use. Default is
'crafting_tool'.
- `consumable_tag_category` (str): What tag-category consumables must use.
Default is 'crafting_material'.
- `tool_tags` (list): Object-tags to use for tooling. If more than one instace
of a tool is needed, add multiple entries here.
### cool-settings
- `tool_names` (list): Human-readable names for tools. These are used for informative
messages/errors. If not given, tags will be used. If given, this list should
match the length of `tool_tags`.
messages/errors. If not given, the tags will be used. If given, this list should
match the length of `tool_tags`.:
- `exact_tools` (bool, default True): Must have exactly the right tools, any extra
leads to failure.
- `exact_tool_order` (bool, default False): Tools must be added in exactly the
@ -357,6 +365,8 @@ class CraftingRecipe(CraftingRecipeBase):
### consumables
- `consumable_tag_category` (str): What tag-category consumables must use.
Default is 'crafting_material'.
- `consumable_tags` (list): Tags for objects that will be consumed as part of
running the recipe.
- `consumable_names` (list): Human-readable names for consumables. Same as for tools.
@ -368,7 +378,8 @@ class CraftingRecipe(CraftingRecipeBase):
another order than given will lead to failing crafting.
- `consume_on_fail` (bool, default False): Normally, consumables remain if
crafting fails. With this flag, a failed crafting will still consume
ingredients.
consumables. Note that this will also consume any 'extra' consumables
added not part of the recipe!
### outputs (result of crafting)
@ -380,9 +391,12 @@ class CraftingRecipe(CraftingRecipeBase):
### custom error messages
custom messages all have custom formatting markers (default strings are shown):
custom messages all have custom formatting markers. Many are empty strings
when not applicable.
::
{missing}: Comma-separated list of tool/consumable missing for missing/out of order errors.
{excess}: Comma-separated list of tool/consumable added in excess of recipe
{inputs}: Comma-separated list of any inputs (tools + consumables) involved in error.
{tools}: Comma-sepatated list of tools involved in error.
{consumables}: Comma-separated list of consumables involved in error.
@ -392,23 +406,29 @@ class CraftingRecipe(CraftingRecipeBase):
{o0}..{oN-1}: Individual outputs, same order as `.output_names`.
- `error_tool_missing_message`: "Could not craft {outputs} without {missing}."
- `error_tool_order_message`: "Could not craft {outputs} since
{missing} was added in the wrong order."
- `error_tool_order_message`:
"Could not craft {outputs} since {missing} was added in the wrong order."
- `error_tool_excess_message`: "Could not craft {outputs} (extra {excess})."
- `error_consumable_missing_message`: "Could not craft {outputs} without {missing}."
- `error_consumable_order_message`: "Could not craft {outputs} since
{missing} was added in the wrong order."
- `error_consumable_order_message`:
"Could not craft {outputs} since {missing} was added in the wrong order."
- `error_consumable_excess_message`: "Could not craft {outputs} (excess {excess})."
- `success_message`: "You successfuly craft {outputs}!"
- `failed_message`: "You failed to craft {outputs}."
- `failure_message`: "" (this is handled by the other error messages by default)
## Hooks
1. Crafting starts by calling `.craft` on the parent class.
2. `.validate_inputs` is called. This returns all valid `(tools, consumables)`
3. `.pre_craft` is called with the valid `(tools, consumables)`.
4. `.do_craft` is called, it should return the final result, if any
5. `.post_craft` is called with both inputs and final result, if any. It should
return the final result or None. By default, this calls the
success/error messages and deletes consumables.
1. Crafting starts by calling `.craft(**kwargs)` on the parent class. The
`**kwargs` are optional, extends any `**kwargs` passed to the class
constructor and will be passed into all the following hooks.
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
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
this deletes consumables.
Use `.msg` to conveniently send messages to the crafter. Raise
`evennia.contrib.crafting.crafting.CraftingError` exception to abort
@ -440,6 +460,9 @@ class CraftingRecipe(CraftingRecipeBase):
# tool out of order
error_tool_order_message = \
"Could not craft {outputs} since {missing} was added in the wrong order."
# if .exact_tools is set and there are more than needed
error_tool_excess_message = \
"Could not craft {outputs} without the exact tools (extra {excess})."
# a list of tag-keys (of the `tag_category`). If more than one of each type
# is needed, there should be multiple same-named entries in this list.
@ -462,6 +485,9 @@ class CraftingRecipe(CraftingRecipeBase):
# consumable out of order
error_consumable_order_message = \
"Could not craft {outputs} since {missing} was added in the wrong order."
# if .exact_consumables is set and there are more than needed
error_consumable_excess_message = \
"Could not craft {outputs} without the exact ingredients (extra {excess})."
# this is a list of one or more prototypes (prototype_keys to existing
# prototypes or full prototype-dicts) to use to build the result. All of
@ -472,25 +498,35 @@ class CraftingRecipe(CraftingRecipeBase):
# prototype's key or typeclass will be used. If given, this must have the same length
# as `output_prototypes`.
output_names = []
# general craft-failure msg to show after other error-messages.
failure_message = ""
# show after a successful craft
success_message = "You successfully craft {outputs}!"
# custom craft-failure.
failed_message = "Failed to craft {outputs}."
def __init__(self, *args, **kwargs):
def __init__(self, crafter, *inputs, **kwargs):
"""
Internally, this class stores validated data in
`.validated_consumables` and `.validated_tools` respectively. The
`.validated_inputs` holds a list of all types in the order inserted
to the class constructor.
Args:
crafter (Object): The one doing the crafting.
*inputs (Object): The ingredients (+tools) of the recipe to use. The
The recipe will itself figure out (from tags) which is a tool and
which is a consumable.
**kwargs (any): Any other parameters that are relevant for
this recipe. These will be passed into the crafting hooks.
Notes:
Internally, this class stores validated data in
`.validated_consumables` and `.validated_tools` respectively. The
`.validated_inputs` property (from parent) holds a list of everything
types in the order inserted to the class constructor.
"""
super().__init__(*args, **kwargs)
super().__init__(crafter, *inputs, **kwargs)
self.validated_consumables = []
self.validated_tools = []
# validate class properties
if self.consumable_names:
assert len(self.consumable_names) == len(self.consumable_tags), \
f"Crafting {self.__class__}.consumable_names list must " \
@ -526,11 +562,12 @@ class CraftingRecipe(CraftingRecipeBase):
def _format_message(self, message, **kwargs):
missing = iter_to_str(kwargs.get("missing", ""))
excess = iter_to_str(kwargs.get("excess", ""))
involved_tools = iter_to_str(kwargs.get("tools", ""))
involved_cons = iter_to_str(kwargs.get("consumables", ""))
# build template context
mapping = {"missing": iter_to_str(missing)}
mapping = {"missing": missing, "excess": excess}
mapping.update({
f"i{ind}": self.consumable_names[ind]
for ind, name in enumerate(self.consumable_names or self.consumable_tags)
@ -548,18 +585,19 @@ class CraftingRecipe(CraftingRecipeBase):
# populate template and return
return message.format(**mapping)
def seed(self, tool_kwargs=None, consumable_kwargs=None):
@classmethod
def seed(cls, tool_kwargs=None, consumable_kwargs=None):
"""
This is a helper method for easy testing and application of this
recipe. When called, it will create simple dummy ingredients with
names and tags needed by this recipe.
This is a helper class-method for easy testing and application of this
recipe. When called, it will create simple dummy ingredients with names
and tags needed by this recipe.
Args:
consumable_kwargs (dict, optional): This will be passed as
`**kwargs` into the `create_object` call for each consumable.
`**consumable_kwargs` into the `create_object` call for each consumable.
If not given, matching `consumable_name` or `consumable_tag`
will be used for key.
tool_kwargs (dict, optional): Will be passed as `**kwargs` into the `create_object`
tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object`
call for each tool. If not given, the matching
`tool_name` or `tool_tag` will be used for key.
@ -567,6 +605,13 @@ class CraftingRecipe(CraftingRecipeBase):
tuple: A tuple `(tools, consumables)` with newly created dummy
objects matching the recipe ingredient list.
Example:
::
tools, consumables = SwordRecipe.seed()
recipe = SwordRecipe(caller, *(tools + consumables))
result = recipe.craft()
Notes:
If `key` is given in `consumable/tool_kwargs` then _every_ created item
of each type will have the same key.
@ -582,76 +627,103 @@ class CraftingRecipe(CraftingRecipeBase):
cons_tags = consumable_kwargs.pop("tags", [])
tools = []
for itag, tag in enumerate(self.tool_tags):
tools.extend(
for itag, tag in enumerate(cls.tool_tags):
tools.append(
create_object(
key=tool_key or (self.tool_names[itag] if self.tool_names
else tag.capitalize()),
tags=[(tag, self.tool_tag_category), *tool_tags],
key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
tags=[(tag, cls.tool_tag_category), *tool_tags],
**tool_kwargs
)
)
consumables = []
for itag, tag in enumerate(self.consumable_tags):
consumables.extend(
for itag, tag in enumerate(cls.consumable_tags):
consumables.append(
create_object(
key=cons_key or (self.consumable_names[itag] if
self.consumable_names else
key=cons_key or (cls.consumable_names[itag] if
cls.consumable_names else
tag.capitalize()),
tags=[(tag, self.consumable_tag_category), *cons_tags]
tags=[(tag, cls.consumable_tag_category), *cons_tags],
**consumable_kwargs
)
)
return tools, consumables
def validate_inputs(self, **kwargs):
def pre_craft(self, **kwargs):
"""
Check so the given inputs are what is needed. This operates on `self.inputs` which
is set to the inputs added to the class constructor. Validated data is stored as
lists on `.validated_tools` and `.validated_consumables` respectively.
Do pre-craft checks, including input validation.
Check so the given inputs are what is needed. This operates on
`self.inputs` which is set to the inputs added to the class
constructor. Validated data is stored as lists on `.validated_tools`
and `.validated_consumables` respectively.
Args:
**kwargs: Any optional extra kwargs passed during initialization of
the recipe class.
Raises:
CraftingValidationError: If validation fails. At this point the crafter
is expected to have been informed of the problem already.
"""
def _check_completeness(
tagmap, taglist, namelist, exact_match, exact_order,
error_missing_message, error_order_message):
"""Compare tagmap to taglist"""
error_missing_message, error_order_message, error_excess_message):
"""Compare tagmap (inputs) to taglist (required)"""
valids = []
for itag, tagkey in enumerate(taglist):
found_obj = None
for obj, taglist in tagmap.items():
if tagkey in taglist:
for obj, objtags in tagmap.items():
if tagkey in objtags:
found_obj = obj
break
if exact_match:
# if we get here, we have a no-match
self.msg(self._format_message(
error_missing_message,
missing=namelist[itag] if namelist else tagkey.capitalize()))
raise CraftingValidationError
if exact_order:
# if we get here order is wrong
self.msg(self._format_message(
err = self._format_message(
error_order_message,
missing=namelist[itag] if namelist else tagkey.capitalize()))
raise CraftingValidationError
missing=obj.get_display_name(looker=self.crafter))
self.msg(err)
raise CraftingValidationError(err)
# since we pop from the mapping, it gets ever shorter
match = tagmap.pop(found_obj, None)
if match:
valids.append(found_obj)
elif exact_match:
err = self._format_message(
error_missing_message,
missing=namelist[itag] if namelist else tagkey.capitalize())
self.msg(err)
raise CraftingValidationError(err)
if exact_match and tagmap:
# something is left in tagmap, that means it was never popped and
# thus this is not an exact match
err = self._format_message(
error_excess_message,
excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap])
self.msg(err)
raise CraftingValidationError(err)
return valids
# get tools and consumables from self.inputs
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
for obj in self.inputs if obj and hasattr(obj, "tags")}
consumable_map = {obj: obj.tags.get(category=self.tag_category, return_list=True)
for obj in self.inputs if obj and hasattr(obj, "tags") and
inherits_from(obj, "evennia.objects.models.ObjectDB")}
tool_map = {obj: tags for obj, tags in tool_map.items() if tags}
consumable_map = {obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
for obj in self.inputs
if obj and hasattr(obj, "tags") and obj not in tool_map}
if obj and hasattr(obj, "tags") and obj not in tool_map and
inherits_from(obj, "evennia.objects.models.ObjectDB")}
consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags}
# we set these so they are available for error management at all times,
# they will be updated with the actual values at the end
self.validated_tools = [obj for obj in tool_map]
self.validated_consumables = [obj for obj in consumable_map]
tools = _check_completeness(
tool_map,
@ -660,7 +732,8 @@ class CraftingRecipe(CraftingRecipeBase):
self.exact_tools,
self.exact_tool_order,
self.error_tool_missing_message,
self.error_tool_order_message
self.error_tool_order_message,
self.error_tool_excess_message,
)
consumables = _check_completeness(
consumable_map,
@ -669,53 +742,38 @@ class CraftingRecipe(CraftingRecipeBase):
self.exact_consumables,
self.exact_consumable_order,
self.error_consumable_missing_message,
self.error_consumable_order_message
self.error_consumable_order_message,
self.error_consumable_excess_message,
)
# regardless of flags, the tools/consumable lists much contain exactly
# all the recipe needs now.
if len(tools) != len(self.tool_tags):
raise CraftingValidationError
if len(consumables) == len(self.consumable_tags):
raise CraftingValidationError
raise CraftingValidationError(
f"Tools {tools}'s tags do not match expected tags {self.tool_tags}")
if len(consumables) != len(self.consumable_tags):
raise CraftingValidationError(
f"Consumables {consumables}'s tags do not match "
f"expected tags {self.consumable_tags}")
# all is ok!
self.validated_tools = tools
self.validated_consumables = tools
# including also empty hooks here for easier reference
def pre_craft(self, **kwargs):
"""
Hook to override.
This is called just before crafting operation, after inputs have
been validated.
Args:
*validated_inputs (any): Data previously returned from
`validate_inputs`. This is a tuple `(tools, consumables)`.
**kwargs (any): Passed from `self.craft`.
Returns:
any: The validated_inputs, modified or not.
"""
pass
self.validated_consumables = consumables
def do_craft(self, **kwargs):
"""
Hook to override.
Hook to override. This will not be called if validation in `pre_craft`
fails.
This performs the actual crafting. At this point the inputs are
expected to have been verified already.
Args:
validated_inputs (tuple): A tuple `(tools, consumables)`.
Returns:
list: A list of spawned objects created from the inputs.
list: A list of spawned objects created from the inputs, or None
on a failure.
Notes:
This method should use `self.msg` to inform the user about the
specific reason of failure immediately.
We may want to analyze the tools in some way here to affect the
crafting process.
@ -725,22 +783,24 @@ class CraftingRecipe(CraftingRecipeBase):
def post_craft(self, craft_result, **kwargs):
"""
Hook to override.
This is called just after crafting has finished. A common use of
this method is to delete the inputs.
Args:
craft_result (any): The crafted result, provided by `self.do_craft`.
validated_inputs (tuple): the validated inputs, a tuple `(tools, consumables)`.
craft_result (list): The crafted result, provided by `self.do_craft`.
**kwargs (any): Passed from `self.craft`.
Returns:
any: The return of the craft, possibly modified in this method.
list: The return(s) of the craft, possibly modified in this method.
Notes:
This is _always_ called, also if validation in `pre_craft` fails
(`craft_result` will then be `None`).
"""
if craft_result:
self.msg(self._format_message(self.success_message))
else:
elif self.failure_message:
self.msg(self._format_message(self.failure_message))
if craft_result or self.consume_on_fail:
@ -751,10 +811,10 @@ class CraftingRecipe(CraftingRecipeBase):
return craft_result
# access functions
# access function
def craft(crafter, recipe_name, *inputs, return_list=True, 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
Python module containing recipe classes. Note that this requires
@ -763,10 +823,8 @@ def craft(crafter, recipe_name, *inputs, return_list=True, raise_exception=False
Args:
crafter (Object): The one doing the crafting.
recipe_name (str): This should match the `CraftRecipe.name` to use.
recipe_name (str): The `CraftRecipe.name` to use.
*inputs: Suitable ingredients (Objects) to use in the crafting.
return_list (bool, optional): Always return a list, even if zero or one items were
cracted.
raise_exception (bool, optional): If crafting failed for whatever
reason, raise `CraftingError`. The user will still be informed by the recipe.
**kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft).
@ -792,3 +850,115 @@ def craft(crafter, recipe_name, *inputs, return_list=True, raise_exception=False
f"has a name matching {recipe_name}")
recipe = RecipeClass(crafter, *inputs, **kwargs)
return recipe.craft(raise_exception=raise_exception)
# craft command/cmdset
class CraftingCmdSet(CmdSet):
"""
Store crafting command.
"""
key = "Crafting cmdset"
def at_cmdset_creation(self):
self.add(CmdCraft())
class CmdCraft(Command):
"""
Craft an item using ingredients and tools
Usage:
craft <recipe> [from <ingredient>,...] [using <tool>, ...]
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
Notes:
Ingredients must be in the crafter's inventory. Tools can also be
things in the current location, like a furnace, windmill or anvil.
"""
def parse(self):
"""
Handle parsing of
::
<recipe> [FROM <ingredients>] [USING <tools>]
"""
self.args = args = self.args.strip().lower()
recipe, ingredients, tools = "", "", ""
if 'from' in args:
recipe, *rest = args.split(" from ", 1)
rest = rest[0] if rest else ""
ingredients, *tools = rest.split(" using ", 1)
elif 'using' in args:
recipe, *tools = args.split(" using ", 1)
tools = tools[0] if tools else ""
self.recipe = recipe.strip()
self.ingredients = [ingr.strip() for ingr in ingredients.split(",")]
self.tools = [tool.strip() for tool in tools.split(",")]
def func(self):
"""
Perform crafting.
Will check the `craft` locktype. If a consumable/ingredient does not pass
this check, we will check for the 'crafting_consumable_err_msg'
Attribute, otherwise will use a default. If failing on a tool, will use
the `crafting_tool_err_msg` if available.
"""
caller = self.caller
if not self.args or not self.recipe:
self.caller.msg("Usage: craft <recipe> from <ingredient>, ... [using <tool>,...]")
return
ingredients = []
for ingr_key in self.ingredients:
if not ingr_key:
continue
obj = caller.search(ingr_key, location=self.caller)
# since ingredients are consumed we need extra check so we don't
# try to include characters or accounts etc.
if not obj:
return
if (not inherits_from(obj, "evennia.objects.models.ObjectDB")
or obj.sessions.all() or not obj.access(caller, "craft", default=True)):
# We don't allow to include puppeted objects nor those with the
# 'negative' permission 'nocraft'.
caller.msg(obj.attributes.get(
"crafting_consumable_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this."))
return
ingredients.append(obj)
tools = []
for tool_key in self.tools:
if not tool_key:
continue
# tools are not consumed, can also exist in the current room
obj = caller.search(tool_key)
if not obj:
return None
if not obj.access(caller, "craft", default=True):
caller.msg(obj.attributes.get(
"crafting_tool_err_msg",
default=f"{obj.get_display_name(looker=caller)} can't be used for this."))
return
tools.append(obj)
# perform craft and make sure result is in inventory
# (the recipe handles all returns to caller)
result = craft(caller, self.recipe, *(tools + ingredients))
if result:
for obj in result:
obj.location = caller

View file

@ -32,10 +32,10 @@ around with them.
sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]
rawhide = fur + knife[T]
oak bark = oak wood + knife[T]
oak bark + cleaned oak wood = oak wood + knife[T]
leather = rawhide + oak bark + water + cauldron[T]
sword handle = oak wood + knife[T]
sword handle = cleaned oak wood + knife[T]
sword = sword blade + sword guard + sword pommel
+ sword handle + leather + knife[T] + hammer[T] + furnace[T]
@ -90,7 +90,7 @@ class _SwordSmithingBaseRecipe(CraftingRecipe):
failed_message = ("You work and work but you are not happy with the result. "
"You need to start over.")
def do_craft(self, validated_inputs, **kwargs):
def do_craft(self, **kwargs):
"""
Making a sword blade takes skill. Here we emulate this by introducing a
random chance of failure (in a real game this could be a skill check
@ -117,7 +117,7 @@ class _SwordSmithingBaseRecipe(CraftingRecipe):
if random.random() < 0.8:
# 80% chance of success. This will spawn the sword and show
# success-message.
return super().do_craft()
return super().do_craft(**kwargs)
else:
# fail and show failed message
return None
@ -161,7 +161,7 @@ class SwordGuardRecipe(_SwordSmithingBaseRecipe):
sword's blade and also protects the hand when parrying.
"""
name = "sword pommel"
name = "sword guard"
tool_tags = ["hammer", "anvil", "furnace"]
consumable_tags = ["crucible steel"]
output_prototypes = [
@ -191,6 +191,7 @@ class OakBarkRecipe(CraftingRecipe):
The actual thing needed for tanning leather is Tannin, but we skip
the step of refining tannin from the bark and use the bark as-is.
This produces two outputs - the bark and the cleaned wood.
"""
name = "oak bark"
tool_tags = ["knife"]
@ -198,7 +199,11 @@ class OakBarkRecipe(CraftingRecipe):
output_prototypes = [
{"key": "Oak bark",
"desc": "Bark of oak, stripped from the core wood.",
"tags": [("oak bark", "crafting_material")]}
"tags": [("oak bark", "crafting_material")]},
{"key": "Oak Wood (cleaned)",
"desc": "Oakwood core, stripped of bark.",
"tags": [("cleaned oak wood", "crafting_material")]},
]
@ -228,7 +233,7 @@ class SwordHandleRecipe(CraftingRecipe):
"""
name = "sword handle"
tool_tags = ["knife"]
consumable_tags = ["oak wood"]
consumable_tags = ["cleaned oak wood"]
output_prototypes = [
{"key": "Sword handle",
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",

View file

@ -0,0 +1,672 @@
"""
Unit tests for the crafting system contrib.
"""
from unittest import mock
from anything import Something
from django.test import override_settings
from django.core.exceptions import ObjectDoesNotExist
from evennia.commands.default.tests import CommandTest
from evennia.utils.test_resources import TestCase, EvenniaTest
from evennia.utils.create import create_object
from . import crafting, example_recipes
class TestCraftUtils(TestCase):
"""
Test helper utils for crafting.
"""
maxDiff = None
@override_settings(CRAFT_RECIPE_MODULES=[])
def test_load_recipes(self):
"""This should only load the example module now"""
crafting._load_recipes()
self.assertEqual(
crafting._RECIPE_CLASSES,
{
'crucible steel': example_recipes.CrucibleSteelRecipe,
'leather': example_recipes.LeatherRecipe,
'oak bark': example_recipes.OakBarkRecipe,
'pig iron': example_recipes.PigIronRecipe,
'rawhide': example_recipes.RawhideRecipe,
'sword': example_recipes.SwordRecipe,
'sword blade': example_recipes.SwordBladeRecipe,
'sword guard': example_recipes.SwordGuardRecipe,
'sword handle': example_recipes.SwordHandleRecipe,
'sword pommel': example_recipes.SwordPommelRecipe,
}
)
class _TestMaterial:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
class TestCraftingRecipeBase(TestCase):
"""
Test the parent recipe class.
"""
def setUp(self):
self.crafter = mock.MagicMock()
self.crafter.msg = mock.MagicMock()
self.inp1 = _TestMaterial("test1")
self.inp2 = _TestMaterial("test2")
self.inp3 = _TestMaterial("test3")
self.kwargs = {"kw1": 1, "kw2": 2}
self.recipe = crafting.CraftingRecipeBase(
self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs)
def test_msg(self):
"""Test messaging to crafter"""
self.recipe.msg("message")
self.crafter.msg.assert_called_with("message", {"type": "crafting"})
def test_pre_craft(self):
"""Test validating hook"""
self.recipe.pre_craft()
self.assertEqual(
self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3)
)
def test_pre_craft_fail(self):
"""Should rase error if validation fails"""
self.recipe.allow_craft = False
with self.assertRaises(crafting.CraftingValidationError):
self.recipe.pre_craft()
def test_craft_hook__succeed(self):
"""Test craft hook, the main access method."""
expected_result = _TestMaterial("test_result")
self.recipe.do_craft = mock.MagicMock(return_value=expected_result)
self.assertTrue(self.recipe.allow_craft)
result = self.recipe.craft()
# check result
self.assertEqual(result, expected_result)
self.recipe.do_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)
# trying to re-run again should fail since rerun is False
with self.assertRaises(crafting.CraftingError):
self.recipe.craft()
def test_craft_hook__fail(self):
"""Test failing the call"""
self.recipe.do_craft = mock.MagicMock(return_value=None)
# trigger exception
with self.assertRaises(crafting.CraftingError):
self.recipe.craft(raise_exception=True)
# reset and try again without exception
self.recipe.allow_craft = True
result = self.recipe.craft()
self.assertEqual(result, None)
class _MockRecipe(crafting.CraftingRecipe):
name = "testrecipe"
tool_tags = ["tool1", "tool2"]
consumable_tags = ["cons1", "cons2", "cons3"]
output_prototypes = [
{"key": "Result1",
"prototype_key": "resultprot",
"tags": [("result1", "crafting_material")]}
]
@override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftingRecipe(TestCase):
"""
Test the CraftingRecipe class with one recipe
"""
maxDiff = None
def setUp(self):
self.crafter = mock.MagicMock()
self.crafter.msg = mock.MagicMock()
self.tool1 = create_object(key="tool1", tags=[("tool1", "crafting_tool")], nohome=True)
self.tool2 = create_object(key="tool2", tags=[("tool2", "crafting_tool")], nohome=True)
self.cons1 = create_object(key="cons1", tags=[("cons1", "crafting_material")], nohome=True)
self.cons2 = create_object(key="cons2", tags=[("cons2", "crafting_material")], nohome=True)
self.cons3 = create_object(key="cons3", tags=[("cons3", "crafting_material")], nohome=True)
def tearDown(self):
try:
self.tool1.delete()
self.tool2.delete()
self.cons1.delete()
self.cons2.delete()
self.cons3.delete()
except ObjectDoesNotExist:
pass
def test_error_format(self):
"""Test the automatic error formatter """
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
)
msg = ("{missing},{tools},{consumables},{inputs},{outputs}"
"{i0},{i1},{o0}")
kwargs = {"missing": "foo", "tools": ["bar", "bar2", "bar3"],
"consumables": ["cons1", "cons2"]}
expected = {
'missing': 'foo', 'i0': 'cons1', 'i1': 'cons2', 'i2': 'cons3', 'o0':
'Result1', 'tools': 'bar, bar2 and bar3', 'consumables': 'cons1 and cons2',
'inputs': 'cons1, cons2 and cons3', 'outputs': 'Result1'}
result = recipe._format_message(msg, **kwargs)
self.assertEqual(result, msg.format(**expected))
def test_craft__success(self):
"""Test to create a result from the recipe"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
)
result = recipe.craft()
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ['result1', 'resultprot'])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
self.assertIsNone(self.cons2.pk)
self.assertIsNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_seed__succcess(self):
"""Test seed helper classmethod"""
# call classmethod directly
tools, consumables = _MockRecipe.seed()
# this should be a normal successful crafting
recipe = _MockRecipe(
self.crafter,
*(tools + consumables)
)
result = recipe.craft()
self.assertEqual(result[0].key, "Result1")
self.assertEqual(result[0].tags.all(), ['result1', 'resultprot'])
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
# make sure consumables are gone
for cons in consumables:
self.assertIsNone(cons.pk)
# make sure tools remain
for tool in tools:
self.assertIsNotNone(tool.pk)
def test_craft_missing_tool__fail(self):
"""Fail craft by missing tool2"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.cons1, self.cons2, self.cons3
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_missing_message.format(outputs="Result1", missing='tool2'),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_missing_cons__fail(self):
"""Fail craft by missing cons3"""
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(
outputs="Result1", missing='cons3'),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_missing_cons__always_consume__fail(self):
"""Fail craft by missing cons3, with always-consume flag"""
cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, cons4
)
recipe.consume_on_fail = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_missing_message.format(
outputs="Result1", missing='cons3'),
{"type": "crafting"})
# make sure consumables are deleted even though we failed
self.assertIsNone(self.cons1.pk)
self.assertIsNone(self.cons2.pk)
# the extra should also be gone
self.assertIsNone(cons4.pk)
# but cons3 should be fine since it was not included
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain as normal
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_wrong_tool__fail(self):
"""Fail craft by including a wrong tool"""
wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, wrong
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_tool_excess__fail(self):
"""Fail by too many consumables"""
# note that this is a valid tag!
tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_excess_message.format(
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
self.assertIsNotNone(tool3.pk)
def test_craft_cons_excess__fail(self):
"""Fail by too many consumables"""
# note that this is a valid tag!
cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
)
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_excess_message.format(
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
self.assertIsNotNone(cons4.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_tool_excess__sucess(self):
"""Allow too many consumables"""
tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
)
recipe.exact_tools = False
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
self.assertIsNone(self.cons2.pk)
self.assertIsNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_cons_excess__sucess(self):
"""Allow too many consumables"""
cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
)
recipe.exact_consumables = False
result = recipe.craft()
self.assertTrue(result)
self.crafter.msg.assert_called_with(
recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
# make sure consumables are gone
self.assertIsNone(self.cons1.pk)
self.assertIsNone(self.cons2.pk)
self.assertIsNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_tool_order__fail(self):
"""Strict tool-order recipe fail """
recipe = _MockRecipe(
self.crafter,
self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
)
recipe.exact_tool_order = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_tool_order_message.format(
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
def test_craft_cons_order__fail(self):
"""Strict tool-order recipe fail """
recipe = _MockRecipe(
self.crafter,
self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
)
recipe.exact_consumable_order = True
result = recipe.craft()
self.assertFalse(result)
self.crafter.msg.assert_called_with(
recipe.error_consumable_order_message.format(
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)),
{"type": "crafting"})
# make sure consumables are still there
self.assertIsNotNone(self.cons1.pk)
self.assertIsNotNone(self.cons2.pk)
self.assertIsNotNone(self.cons3.pk)
# make sure tools remain
self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk)
class TestCraftSword(TestCase):
"""
Test the `craft` function by crafting the example sword.
"""
def setUp(self):
self.crafter = mock.MagicMock()
self.crafter.msg = mock.MagicMock()
@override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
@mock.patch("evennia.contrib.crafting.example_recipes.random")
def test_craft_sword(self, mockrandom):
"""
Craft example sword. For the test, every crafting works.
"""
# make sure every craft succeeds
mockrandom.random = mock.MagicMock(return_value=0.2)
def _co(key, tagkey, is_tool=False):
tagcat = "crafting_tool" if is_tool else "crafting_material"
return create_object(key=key, tags=[(tagkey, tagcat)], nohome=True)
def _craft(recipe_name, *inputs):
"""shortcut to shorten and return only one element"""
result = crafting.craft(self.crafter, recipe_name, *inputs, raise_exception=True)
return result[0] if len(result) == 1 else result
# generate base materials
iron_ore1 = _co("Iron ore ingot", "iron ore")
iron_ore2 = _co("Iron ore ingot", "iron ore")
iron_ore3 = _co("Iron ore ingot", "iron ore")
ash1 = _co("Pile of Ash", "ash")
ash2 = _co("Pile of Ash", "ash")
ash3 = _co("Pile of Ash", "ash")
sand1 = _co("Pile of sand", "sand")
sand2 = _co("Pile of sand", "sand")
sand3 = _co("Pile of sand", "sand")
coal01 = _co("Pile of coal", "coal")
coal02 = _co("Pile of coal", "coal")
coal03 = _co("Pile of coal", "coal")
coal04 = _co("Pile of coal", "coal")
coal05 = _co("Pile of coal", "coal")
coal06 = _co("Pile of coal", "coal")
coal07 = _co("Pile of coal", "coal")
coal08 = _co("Pile of coal", "coal")
coal09 = _co("Pile of coal", "coal")
coal10 = _co("Pile of coal", "coal")
coal11 = _co("Pile of coal", "coal")
coal12 = _co("Pile of coal", "coal")
oak_wood = _co("Pile of oak wood", "oak wood")
water = _co("Bucket of water", "water")
fur = _co("Bundle of Animal fur", "fur")
# tools
blast_furnace = _co("Blast furnace", "blast furnace", is_tool=True)
furnace = _co("Smithing furnace", "furnace", is_tool=True)
crucible = _co("Smelting crucible", "crucible", is_tool=True)
anvil = _co("Smithing anvil", "anvil", is_tool=True)
hammer = _co("Smithing hammer", "hammer", is_tool=True)
knife = _co("Working knife", "knife", is_tool=True)
cauldron = _co("Cauldron", "cauldron", is_tool=True)
# making pig iron
inputs = [iron_ore1, coal01, coal02, blast_furnace]
pig_iron1 = _craft("pig iron", *inputs)
inputs = [iron_ore2, coal03, coal04, blast_furnace]
pig_iron2 = _craft("pig iron", *inputs)
inputs = [iron_ore3, coal05, coal06, blast_furnace]
pig_iron3 = _craft("pig iron", *inputs)
# making crucible steel
inputs = [pig_iron1, ash1, sand1, coal07, coal08, crucible]
crucible_steel1 = _craft("crucible steel", *inputs)
inputs = [pig_iron2, ash2, sand2, coal09, coal10, crucible]
crucible_steel2 = _craft("crucible steel", *inputs)
inputs = [pig_iron3, ash3, sand3, coal11, coal12, crucible]
crucible_steel3 = _craft("crucible steel", *inputs)
# smithing
inputs = [crucible_steel1, hammer, anvil, furnace]
sword_blade = _craft("sword blade", *inputs)
inputs = [crucible_steel2, hammer, anvil, furnace]
sword_pommel = _craft("sword pommel", *inputs)
inputs = [crucible_steel3, hammer, anvil, furnace]
sword_guard = _craft("sword guard", *inputs)
# stripping fur
inputs = [fur, knife]
rawhide = _craft("rawhide", *inputs)
# making bark (tannin) and cleaned wood
inputs = [oak_wood, knife]
oak_bark, cleaned_oak_wood = _craft("oak bark", *inputs)
# leathermaking
inputs = [rawhide, oak_bark, water, cauldron]
leather = _craft("leather", *inputs)
# sword handle
inputs = [cleaned_oak_wood, knife]
sword_handle = _craft("sword handle", *inputs)
# sword (order matters)
inputs = [sword_blade, sword_guard, sword_pommel, sword_handle,
leather, knife, hammer, furnace]
sword = _craft("sword", *inputs)
self.assertEqual(sword.key, "Sword")
# make sure all materials and intermediaries are deleted
self.assertIsNone(iron_ore1.pk)
self.assertIsNone(iron_ore2.pk)
self.assertIsNone(iron_ore3.pk)
self.assertIsNone(ash1.pk)
self.assertIsNone(ash2.pk)
self.assertIsNone(ash3.pk)
self.assertIsNone(sand1.pk)
self.assertIsNone(sand2.pk)
self.assertIsNone(sand3.pk)
self.assertIsNone(coal01.pk)
self.assertIsNone(coal02.pk)
self.assertIsNone(coal03.pk)
self.assertIsNone(coal04.pk)
self.assertIsNone(coal05.pk)
self.assertIsNone(coal06.pk)
self.assertIsNone(coal07.pk)
self.assertIsNone(coal08.pk)
self.assertIsNone(coal09.pk)
self.assertIsNone(coal10.pk)
self.assertIsNone(coal11.pk)
self.assertIsNone(coal12.pk)
self.assertIsNone(oak_wood.pk)
self.assertIsNone(water.pk)
self.assertIsNone(fur.pk)
self.assertIsNone(pig_iron1.pk)
self.assertIsNone(pig_iron2.pk)
self.assertIsNone(pig_iron3.pk)
self.assertIsNone(crucible_steel1.pk)
self.assertIsNone(crucible_steel2.pk)
self.assertIsNone(crucible_steel3.pk)
self.assertIsNone(sword_blade.pk)
self.assertIsNone(sword_pommel.pk)
self.assertIsNone(sword_guard.pk)
self.assertIsNone(rawhide.pk)
self.assertIsNone(oak_bark.pk)
self.assertIsNone(leather.pk)
self.assertIsNone(sword_handle.pk)
# make sure all tools remain
self.assertIsNotNone(blast_furnace)
self.assertIsNotNone(furnace)
self.assertIsNotNone(crucible)
self.assertIsNotNone(anvil)
self.assertIsNotNone(hammer)
self.assertIsNotNone(knife)
self.assertIsNotNone(cauldron)
@mock.patch("evennia.contrib.crafting.crafting._load_recipes",
new=mock.MagicMock())
@mock.patch("evennia.contrib.crafting.crafting._RECIPE_CLASSES",
new={"testrecipe": _MockRecipe})
@override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
class TestCraftCommand(CommandTest):
"""Test the crafting command"""
def setUp(self):
super().setUp()
tools, consumables = _MockRecipe.seed(
tool_kwargs={"location": self.char1},
consumable_kwargs={"location": self.char1})
def test_craft__success(self):
"Successfully craft using command"
self.call(
crafting.CmdCraft(),
"testrecipe from cons1, cons2, cons3 using tool1, tool2",
_MockRecipe.success_message.format(outputs="Result1")
)
def test_craft__notools__failure(self):
"Craft fail no tools"
self.call(
crafting.CmdCraft(),
"testrecipe from cons1, cons2, cons3",
_MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1")
)
def test_craft__nocons__failure(self):
self.call(
crafting.CmdCraft(),
"testrecipe using tool1, tool2",
_MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1")
)

View file

@ -405,7 +405,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
to search. Note that this is used to query the *contents* of a
location and will not match for the location itself -
if you want that, don't set this or use `candidates` to specify
exactly which objects should be searched.
exactly which objects should be searched. If this nor candidates are
given, candidates will include caller's inventory, current location and
all objects in the current location.
attribute_name (str): Define which property to search. If set, no
key+alias search will be performed. This can be used
to search database fields (db_ will be automatically

View file

@ -915,8 +915,15 @@ def spawn(*prototypes, **kwargs):
val = prot.pop("location", None)
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
val = prot.pop("home", settings.DEFAULT_HOME)
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
val = prot.pop("home", None)
if val:
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
else:
try:
create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj)
except ObjectDB.DoesNotExist:
# settings.DEFAULT_HOME not existing is common for unittests
pass
val = prot.pop("destination", None)
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)