Expand with many crafting unit tests
This commit is contained in:
parent
add5f90609
commit
e890bd9040
5 changed files with 1032 additions and 176 deletions
|
|
@ -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']
|
CRAFT_MODULE_RECIPES = ['world.recipes_weapons', 'world.recipes_potions']
|
||||||
|
|
||||||
Below is an example of a crafting recipe. See the `CraftingRecipe` class for
|
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
|
```python
|
||||||
|
|
||||||
|
|
@ -93,6 +94,8 @@ recipes.
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
iter_to_str, callables_from_module, inherits_from, make_iter)
|
iter_to_str, callables_from_module, inherits_from, make_iter)
|
||||||
|
from evennia.commands.cmdset import CmdSet
|
||||||
|
from evennia.commands.command import Command
|
||||||
from evennia.prototypes.spawner import spawn
|
from evennia.prototypes.spawner import spawn
|
||||||
from evennia.utils.create import create_object
|
from evennia.utils.create import create_object
|
||||||
|
|
||||||
|
|
@ -131,8 +134,11 @@ class CraftingValidationError(CraftingError):
|
||||||
|
|
||||||
class CraftingRecipeBase:
|
class CraftingRecipeBase:
|
||||||
"""
|
"""
|
||||||
This is the base of the crafting system. The recipe handles all aspects of
|
The recipe handles all aspects of performing a 'craft' operation. This is
|
||||||
performing a 'craft' operation.
|
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:
|
Example of usage:
|
||||||
::
|
::
|
||||||
|
|
@ -144,6 +150,19 @@ class CraftingRecipeBase:
|
||||||
consumed - so in that case the recipe cannot be used a second time (doing so
|
consumed - so in that case the recipe cannot be used a second time (doing so
|
||||||
will raise a `CraftingError`)
|
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"
|
name = "recipe base"
|
||||||
|
|
||||||
|
|
@ -151,8 +170,6 @@ class CraftingRecipeBase:
|
||||||
# don't set this unless crafting inputs are *not* consumed by the crafting
|
# don't set this unless crafting inputs are *not* consumed by the crafting
|
||||||
# process (otherwise subsequent calls will fail).
|
# process (otherwise subsequent calls will fail).
|
||||||
allow_reuse = False
|
allow_reuse = False
|
||||||
# this is set to avoid re-validation if recipe is re-run
|
|
||||||
is_validated = False
|
|
||||||
|
|
||||||
def __init__(self, crafter, *inputs, **kwargs):
|
def __init__(self, crafter, *inputs, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -183,45 +200,27 @@ class CraftingRecipeBase:
|
||||||
"""
|
"""
|
||||||
self.crafter.msg(message, {"type": "crafting"})
|
self.crafter.msg(message, {"type": "crafting"})
|
||||||
|
|
||||||
def validate_inputs(self, **kwargs):
|
def pre_craft(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to override.
|
Hook to override.
|
||||||
|
|
||||||
Make sure the provided inputs are valid. This should always be run.
|
This is called just before crafting operation and is normally
|
||||||
This should validate `self.inputs` which are the inputs given when
|
responsible for validating the inputs, storing data on
|
||||||
creating this recipe.
|
`self.validated_inputs`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: These are optional extra flags passed during intialization.
|
**kwargs: Optional extra flags passed during initialization or
|
||||||
|
`.craft(**kwargs)`.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
CraftingValidationError: If validation fails.
|
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:
|
if self.allow_craft:
|
||||||
self.validated_inputs = self.inputs[:]
|
self.validated_inputs = self.inputs[:]
|
||||||
else:
|
else:
|
||||||
raise CraftingValidationError
|
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):
|
def do_craft(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to override.
|
Hook to override.
|
||||||
|
|
@ -231,7 +230,6 @@ class CraftingRecipeBase:
|
||||||
inputs are available on this recipe instance.
|
inputs are available on this recipe instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
validated_inputs (any): Data previously returned from `pre_craft`.
|
|
||||||
**kwargs: Any extra flags passed at initialization.
|
**kwargs: Any extra flags passed at initialization.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -281,29 +279,31 @@ class CraftingRecipeBase:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
craft_result = None
|
craft_result = None
|
||||||
err = ""
|
|
||||||
if self.allow_craft:
|
if self.allow_craft:
|
||||||
|
|
||||||
|
# override/extend craft_kwargs from initialization.
|
||||||
craft_kwargs = copy(self.craft_kwargs)
|
craft_kwargs = copy(self.craft_kwargs)
|
||||||
craft_kwargs.update(kwargs)
|
craft_kwargs.update(kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# this assigns to self.validated_inputs
|
try:
|
||||||
if not self.is_validated:
|
# this assigns to self.validated_inputs
|
||||||
self.validate_inputs(**craft_kwargs)
|
self.pre_craft(**craft_kwargs)
|
||||||
self.is_validated = True
|
except (CraftingError, CraftingValidationError):
|
||||||
|
if raise_exception:
|
||||||
# run the crafting process - each accesses validated data directly
|
raise
|
||||||
self.pre_craft(**craft_kwargs)
|
else:
|
||||||
craft_result = self.do_craft(**craft_kwargs)
|
craft_result = self.do_craft(**craft_kwargs)
|
||||||
self.post_craft(craft_result, **craft_kwargs)
|
finally:
|
||||||
except (CraftingError, CraftingValidationError) as exc:
|
craft_result = self.post_craft(craft_result, **craft_kwargs)
|
||||||
# use this to abort crafting early
|
except (CraftingError, CraftingValidationError):
|
||||||
if raise_exception:
|
if raise_exception:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# possibly turn off re-use depending on class setting
|
# possibly turn off re-use depending on class setting
|
||||||
self.allow_craft = self.allow_reuse
|
self.allow_craft = self.allow_reuse
|
||||||
elif not 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:
|
if craft_result is None and raise_exception:
|
||||||
raise CraftingError(f"Crafting of {self.name} failed.")
|
raise CraftingError(f"Crafting of {self.name} failed.")
|
||||||
return craft_result
|
return craft_result
|
||||||
|
|
@ -312,44 +312,52 @@ class CraftingRecipeBase:
|
||||||
class CraftingRecipe(CraftingRecipeBase):
|
class CraftingRecipe(CraftingRecipeBase):
|
||||||
"""
|
"""
|
||||||
The CraftRecipe implements the most common form of crafting: Combining (and
|
The CraftRecipe implements the most common form of crafting: Combining (and
|
||||||
optionally consuming) inputs to produce a new result. This type of recipe
|
consuming) inputs to produce a new result. This type of recipe only works
|
||||||
only works with typeclassed entities as inputs and outputs, since it's
|
with typeclassed entities as inputs and outputs, since it's based on Tags
|
||||||
based on Tags and prototypes.
|
and Prototypes.
|
||||||
|
|
||||||
There are two types of crafting ingredients: 'tools' and 'consumables'. The
|
There are two types of crafting ingredients: 'tools' and 'consumables'. The
|
||||||
difference between them is that the former is not consumed in the crafting
|
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'
|
process. So if you need a hammer and anvil to craft a sword, they are
|
||||||
whereas the materials of the sword are 'consumables'.
|
'tools' whereas the materials of the sword are 'consumables'.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
::
|
::
|
||||||
|
|
||||||
class SwordRecipe(CraftRecipe):
|
class FlourRecipe(CraftRecipe):
|
||||||
name = "sword"
|
name = "flour"
|
||||||
input_tags = ["hilt", "pommel", "strips of leather", "sword blade"]
|
tool_tags = ['windmill']
|
||||||
|
consumable_tags = ["wheat"]
|
||||||
output_prototypes = [
|
output_prototypes = [
|
||||||
{"key": "sword",
|
{"key": "Bag of flour",
|
||||||
"typeclass": "typeclassess.weapons.bladed.Sword",
|
"typeclass": "typeclasses.food.Flour",
|
||||||
"tags": [("sword", "weapon"), ("melee", "weapontype"),
|
"desc": "A small bag of flour."
|
||||||
("edged", "weapontype")]
|
"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:
|
## Properties on the class level:
|
||||||
|
|
||||||
- `name` (str): The name of this recipe. This should be globally unique.
|
- `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
|
- `tool_tag_category` (str): What tag-category tools must use. Default is
|
||||||
'crafting_tool'.
|
'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
|
- `tool_tags` (list): Object-tags to use for tooling. If more than one instace
|
||||||
of a tool is needed, add multiple entries here.
|
of a tool is needed, add multiple entries here.
|
||||||
|
|
||||||
### cool-settings
|
|
||||||
|
|
||||||
- `tool_names` (list): Human-readable names for tools. These are used for informative
|
- `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
|
messages/errors. If not given, the tags will be used. If given, this list should
|
||||||
match the length of `tool_tags`.
|
match the length of `tool_tags`.:
|
||||||
- `exact_tools` (bool, default True): Must have exactly the right tools, any extra
|
- `exact_tools` (bool, default True): Must have exactly the right tools, any extra
|
||||||
leads to failure.
|
leads to failure.
|
||||||
- `exact_tool_order` (bool, default False): Tools must be added in exactly the
|
- `exact_tool_order` (bool, default False): Tools must be added in exactly the
|
||||||
|
|
@ -357,6 +365,8 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
|
|
||||||
### consumables
|
### 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
|
- `consumable_tags` (list): Tags for objects that will be consumed as part of
|
||||||
running the recipe.
|
running the recipe.
|
||||||
- `consumable_names` (list): Human-readable names for consumables. Same as for tools.
|
- `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.
|
another order than given will lead to failing crafting.
|
||||||
- `consume_on_fail` (bool, default False): Normally, consumables remain if
|
- `consume_on_fail` (bool, default False): Normally, consumables remain if
|
||||||
crafting fails. With this flag, a failed crafting will still consume
|
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)
|
### outputs (result of crafting)
|
||||||
|
|
||||||
|
|
@ -380,9 +391,12 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
|
|
||||||
### custom error messages
|
### 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.
|
{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.
|
{inputs}: Comma-separated list of any inputs (tools + consumables) involved in error.
|
||||||
{tools}: Comma-sepatated list of tools involved in error.
|
{tools}: Comma-sepatated list of tools involved in error.
|
||||||
{consumables}: Comma-separated list of consumables 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`.
|
{o0}..{oN-1}: Individual outputs, same order as `.output_names`.
|
||||||
|
|
||||||
- `error_tool_missing_message`: "Could not craft {outputs} without {missing}."
|
- `error_tool_missing_message`: "Could not craft {outputs} without {missing}."
|
||||||
- `error_tool_order_message`: "Could not craft {outputs} since
|
- `error_tool_order_message`:
|
||||||
{missing} was added in the wrong order."
|
"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_missing_message`: "Could not craft {outputs} without {missing}."
|
||||||
- `error_consumable_order_message`: "Could not craft {outputs} since
|
- `error_consumable_order_message`:
|
||||||
{missing} was added in the wrong order."
|
"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}!"
|
- `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
|
## Hooks
|
||||||
|
|
||||||
1. Crafting starts by calling `.craft` on the parent class.
|
1. Crafting starts by calling `.craft(**kwargs)` on the parent class. The
|
||||||
2. `.validate_inputs` is called. This returns all valid `(tools, consumables)`
|
`**kwargs` are optional, extends any `**kwargs` passed to the class
|
||||||
3. `.pre_craft` is called with the valid `(tools, consumables)`.
|
constructor and will be passed into all the following hooks.
|
||||||
4. `.do_craft` is called, it should return the final result, if any
|
3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
|
||||||
5. `.post_craft` is called with both inputs and final result, if any. It should
|
be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
|
||||||
return the final result or None. By default, this calls the
|
otherwise.
|
||||||
success/error messages and deletes consumables.
|
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
|
Use `.msg` to conveniently send messages to the crafter. Raise
|
||||||
`evennia.contrib.crafting.crafting.CraftingError` exception to abort
|
`evennia.contrib.crafting.crafting.CraftingError` exception to abort
|
||||||
|
|
@ -440,6 +460,9 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
# tool out of order
|
# tool out of order
|
||||||
error_tool_order_message = \
|
error_tool_order_message = \
|
||||||
"Could not craft {outputs} since {missing} was added in the wrong order."
|
"Could not craft {outputs} since {missing} was added in the wrong order."
|
||||||
|
# if .exact_tools is set and there are more than needed
|
||||||
|
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
|
# a list of tag-keys (of the `tag_category`). If more than one of each type
|
||||||
# is needed, there should be multiple same-named entries in this list.
|
# is needed, there should be multiple same-named entries in this list.
|
||||||
|
|
@ -462,6 +485,9 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
# consumable out of order
|
# consumable out of order
|
||||||
error_consumable_order_message = \
|
error_consumable_order_message = \
|
||||||
"Could not craft {outputs} since {missing} was added in the wrong order."
|
"Could not craft {outputs} since {missing} was added in the wrong order."
|
||||||
|
# if .exact_consumables is set and there are more than needed
|
||||||
|
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
|
# this is a list of one or more prototypes (prototype_keys to existing
|
||||||
# prototypes or full prototype-dicts) to use to build the result. All of
|
# prototypes or full prototype-dicts) to use to build the result. All of
|
||||||
|
|
@ -472,25 +498,35 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
# prototype's key or typeclass will be used. If given, this must have the same length
|
# prototype's key or typeclass will be used. If given, this must have the same length
|
||||||
# as `output_prototypes`.
|
# as `output_prototypes`.
|
||||||
output_names = []
|
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}!"
|
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
|
Args:
|
||||||
`.validated_consumables` and `.validated_tools` respectively. The
|
crafter (Object): The one doing the crafting.
|
||||||
`.validated_inputs` holds a list of all types in the order inserted
|
*inputs (Object): The ingredients (+tools) of the recipe to use. The
|
||||||
to the class constructor.
|
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_consumables = []
|
||||||
self.validated_tools = []
|
self.validated_tools = []
|
||||||
|
|
||||||
|
# validate class properties
|
||||||
if self.consumable_names:
|
if self.consumable_names:
|
||||||
assert len(self.consumable_names) == len(self.consumable_tags), \
|
assert len(self.consumable_names) == len(self.consumable_tags), \
|
||||||
f"Crafting {self.__class__}.consumable_names list must " \
|
f"Crafting {self.__class__}.consumable_names list must " \
|
||||||
|
|
@ -526,11 +562,12 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
def _format_message(self, message, **kwargs):
|
def _format_message(self, message, **kwargs):
|
||||||
|
|
||||||
missing = iter_to_str(kwargs.get("missing", ""))
|
missing = iter_to_str(kwargs.get("missing", ""))
|
||||||
|
excess = iter_to_str(kwargs.get("excess", ""))
|
||||||
involved_tools = iter_to_str(kwargs.get("tools", ""))
|
involved_tools = iter_to_str(kwargs.get("tools", ""))
|
||||||
involved_cons = iter_to_str(kwargs.get("consumables", ""))
|
involved_cons = iter_to_str(kwargs.get("consumables", ""))
|
||||||
|
|
||||||
# build template context
|
# build template context
|
||||||
mapping = {"missing": iter_to_str(missing)}
|
mapping = {"missing": missing, "excess": excess}
|
||||||
mapping.update({
|
mapping.update({
|
||||||
f"i{ind}": self.consumable_names[ind]
|
f"i{ind}": self.consumable_names[ind]
|
||||||
for ind, name in enumerate(self.consumable_names or self.consumable_tags)
|
for ind, name in enumerate(self.consumable_names or self.consumable_tags)
|
||||||
|
|
@ -548,18 +585,19 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
# populate template and return
|
# populate template and return
|
||||||
return message.format(**mapping)
|
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
|
This is a helper class-method for easy testing and application of this
|
||||||
recipe. When called, it will create simple dummy ingredients with
|
recipe. When called, it will create simple dummy ingredients with names
|
||||||
names and tags needed by this recipe.
|
and tags needed by this recipe.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
consumable_kwargs (dict, optional): This will be passed as
|
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`
|
If not given, matching `consumable_name` or `consumable_tag`
|
||||||
will be used for key.
|
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
|
call for each tool. If not given, the matching
|
||||||
`tool_name` or `tool_tag` will be used for key.
|
`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
|
tuple: A tuple `(tools, consumables)` with newly created dummy
|
||||||
objects matching the recipe ingredient list.
|
objects matching the recipe ingredient list.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
::
|
||||||
|
|
||||||
|
tools, consumables = SwordRecipe.seed()
|
||||||
|
recipe = SwordRecipe(caller, *(tools + consumables))
|
||||||
|
result = recipe.craft()
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
If `key` is given in `consumable/tool_kwargs` then _every_ created item
|
If `key` is given in `consumable/tool_kwargs` then _every_ created item
|
||||||
of each type will have the same key.
|
of each type will have the same key.
|
||||||
|
|
@ -582,76 +627,103 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
cons_tags = consumable_kwargs.pop("tags", [])
|
cons_tags = consumable_kwargs.pop("tags", [])
|
||||||
|
|
||||||
tools = []
|
tools = []
|
||||||
for itag, tag in enumerate(self.tool_tags):
|
for itag, tag in enumerate(cls.tool_tags):
|
||||||
tools.extend(
|
|
||||||
|
tools.append(
|
||||||
create_object(
|
create_object(
|
||||||
key=tool_key or (self.tool_names[itag] if self.tool_names
|
key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
|
||||||
else tag.capitalize()),
|
tags=[(tag, cls.tool_tag_category), *tool_tags],
|
||||||
tags=[(tag, self.tool_tag_category), *tool_tags],
|
|
||||||
**tool_kwargs
|
**tool_kwargs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
consumables = []
|
consumables = []
|
||||||
for itag, tag in enumerate(self.consumable_tags):
|
for itag, tag in enumerate(cls.consumable_tags):
|
||||||
consumables.extend(
|
consumables.append(
|
||||||
create_object(
|
create_object(
|
||||||
key=cons_key or (self.consumable_names[itag] if
|
key=cons_key or (cls.consumable_names[itag] if
|
||||||
self.consumable_names else
|
cls.consumable_names else
|
||||||
tag.capitalize()),
|
tag.capitalize()),
|
||||||
tags=[(tag, self.consumable_tag_category), *cons_tags]
|
tags=[(tag, cls.consumable_tag_category), *cons_tags],
|
||||||
|
**consumable_kwargs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return tools, consumables
|
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
|
Do pre-craft checks, including input validation.
|
||||||
is set to the inputs added to the class constructor. Validated data is stored as
|
|
||||||
lists on `.validated_tools` and `.validated_consumables` respectively.
|
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:
|
Args:
|
||||||
**kwargs: Any optional extra kwargs passed during initialization of
|
**kwargs: Any optional extra kwargs passed during initialization of
|
||||||
the recipe class.
|
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(
|
def _check_completeness(
|
||||||
tagmap, taglist, namelist, exact_match, exact_order,
|
tagmap, taglist, namelist, exact_match, exact_order,
|
||||||
error_missing_message, error_order_message):
|
error_missing_message, error_order_message, error_excess_message):
|
||||||
"""Compare tagmap to taglist"""
|
"""Compare tagmap (inputs) to taglist (required)"""
|
||||||
valids = []
|
valids = []
|
||||||
for itag, tagkey in enumerate(taglist):
|
for itag, tagkey in enumerate(taglist):
|
||||||
found_obj = None
|
found_obj = None
|
||||||
for obj, taglist in tagmap.items():
|
for obj, objtags in tagmap.items():
|
||||||
if tagkey in taglist:
|
if tagkey in objtags:
|
||||||
found_obj = obj
|
found_obj = obj
|
||||||
break
|
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 exact_order:
|
||||||
# if we get here order is wrong
|
# if we get here order is wrong
|
||||||
self.msg(self._format_message(
|
err = self._format_message(
|
||||||
error_order_message,
|
error_order_message,
|
||||||
missing=namelist[itag] if namelist else tagkey.capitalize()))
|
missing=obj.get_display_name(looker=self.crafter))
|
||||||
raise CraftingValidationError
|
self.msg(err)
|
||||||
|
raise CraftingValidationError(err)
|
||||||
|
|
||||||
# since we pop from the mapping, it gets ever shorter
|
# since we pop from the mapping, it gets ever shorter
|
||||||
match = tagmap.pop(found_obj, None)
|
match = tagmap.pop(found_obj, None)
|
||||||
if match:
|
if match:
|
||||||
valids.append(found_obj)
|
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
|
return valids
|
||||||
|
|
||||||
# get tools and consumables from self.inputs
|
# get tools and consumables from self.inputs
|
||||||
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
|
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
|
||||||
for obj in self.inputs if obj and hasattr(obj, "tags")}
|
for obj in self.inputs if obj and hasattr(obj, "tags") and
|
||||||
consumable_map = {obj: obj.tags.get(category=self.tag_category, return_list=True)
|
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
|
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(
|
tools = _check_completeness(
|
||||||
tool_map,
|
tool_map,
|
||||||
|
|
@ -660,7 +732,8 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
self.exact_tools,
|
self.exact_tools,
|
||||||
self.exact_tool_order,
|
self.exact_tool_order,
|
||||||
self.error_tool_missing_message,
|
self.error_tool_missing_message,
|
||||||
self.error_tool_order_message
|
self.error_tool_order_message,
|
||||||
|
self.error_tool_excess_message,
|
||||||
)
|
)
|
||||||
consumables = _check_completeness(
|
consumables = _check_completeness(
|
||||||
consumable_map,
|
consumable_map,
|
||||||
|
|
@ -669,53 +742,38 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
self.exact_consumables,
|
self.exact_consumables,
|
||||||
self.exact_consumable_order,
|
self.exact_consumable_order,
|
||||||
self.error_consumable_missing_message,
|
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
|
# regardless of flags, the tools/consumable lists much contain exactly
|
||||||
# all the recipe needs now.
|
# all the recipe needs now.
|
||||||
if len(tools) != len(self.tool_tags):
|
if len(tools) != len(self.tool_tags):
|
||||||
raise CraftingValidationError
|
raise CraftingValidationError(
|
||||||
if len(consumables) == len(self.consumable_tags):
|
f"Tools {tools}'s tags do not match expected tags {self.tool_tags}")
|
||||||
raise CraftingValidationError
|
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_tools = tools
|
||||||
self.validated_consumables = tools
|
self.validated_consumables = consumables
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def do_craft(self, **kwargs):
|
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
|
This performs the actual crafting. At this point the inputs are
|
||||||
expected to have been verified already.
|
expected to have been verified already.
|
||||||
|
|
||||||
Args:
|
|
||||||
validated_inputs (tuple): A tuple `(tools, consumables)`.
|
|
||||||
|
|
||||||
Returns:
|
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:
|
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
|
We may want to analyze the tools in some way here to affect the
|
||||||
crafting process.
|
crafting process.
|
||||||
|
|
||||||
|
|
@ -725,22 +783,24 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
def post_craft(self, craft_result, **kwargs):
|
def post_craft(self, craft_result, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to override.
|
Hook to override.
|
||||||
|
|
||||||
This is called just after crafting has finished. A common use of
|
This is called just after crafting has finished. A common use of
|
||||||
this method is to delete the inputs.
|
this method is to delete the inputs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
craft_result (any): The crafted result, provided by `self.do_craft`.
|
craft_result (list): The crafted result, provided by `self.do_craft`.
|
||||||
validated_inputs (tuple): the validated inputs, a tuple `(tools, consumables)`.
|
|
||||||
**kwargs (any): Passed from `self.craft`.
|
**kwargs (any): Passed from `self.craft`.
|
||||||
|
|
||||||
Returns:
|
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:
|
if craft_result:
|
||||||
self.msg(self._format_message(self.success_message))
|
self.msg(self._format_message(self.success_message))
|
||||||
else:
|
elif self.failure_message:
|
||||||
self.msg(self._format_message(self.failure_message))
|
self.msg(self._format_message(self.failure_message))
|
||||||
|
|
||||||
if craft_result or self.consume_on_fail:
|
if craft_result or self.consume_on_fail:
|
||||||
|
|
@ -751,10 +811,10 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
return craft_result
|
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
|
Craft a given recipe from a source recipe module. A recipe module is a
|
||||||
Python module containing recipe classes. Note that this requires
|
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:
|
Args:
|
||||||
crafter (Object): The one doing the crafting.
|
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.
|
*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
|
raise_exception (bool, optional): If crafting failed for whatever
|
||||||
reason, raise `CraftingError`. The user will still be informed by the recipe.
|
reason, raise `CraftingError`. The user will still be informed by the recipe.
|
||||||
**kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft).
|
**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}")
|
f"has a name matching {recipe_name}")
|
||||||
recipe = RecipeClass(crafter, *inputs, **kwargs)
|
recipe = RecipeClass(crafter, *inputs, **kwargs)
|
||||||
return recipe.craft(raise_exception=raise_exception)
|
return recipe.craft(raise_exception=raise_exception)
|
||||||
|
|
||||||
|
|
||||||
|
# craft command/cmdset
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ around with them.
|
||||||
sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||||
|
|
||||||
rawhide = fur + knife[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]
|
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 = sword blade + sword guard + sword pommel
|
||||||
+ sword handle + leather + knife[T] + hammer[T] + furnace[T]
|
+ 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. "
|
failed_message = ("You work and work but you are not happy with the result. "
|
||||||
"You need to start over.")
|
"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
|
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
|
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:
|
if random.random() < 0.8:
|
||||||
# 80% chance of success. This will spawn the sword and show
|
# 80% chance of success. This will spawn the sword and show
|
||||||
# success-message.
|
# success-message.
|
||||||
return super().do_craft()
|
return super().do_craft(**kwargs)
|
||||||
else:
|
else:
|
||||||
# fail and show failed message
|
# fail and show failed message
|
||||||
return None
|
return None
|
||||||
|
|
@ -161,7 +161,7 @@ class SwordGuardRecipe(_SwordSmithingBaseRecipe):
|
||||||
sword's blade and also protects the hand when parrying.
|
sword's blade and also protects the hand when parrying.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = "sword pommel"
|
name = "sword guard"
|
||||||
tool_tags = ["hammer", "anvil", "furnace"]
|
tool_tags = ["hammer", "anvil", "furnace"]
|
||||||
consumable_tags = ["crucible steel"]
|
consumable_tags = ["crucible steel"]
|
||||||
output_prototypes = [
|
output_prototypes = [
|
||||||
|
|
@ -191,6 +191,7 @@ class OakBarkRecipe(CraftingRecipe):
|
||||||
The actual thing needed for tanning leather is Tannin, but we skip
|
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.
|
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"
|
name = "oak bark"
|
||||||
tool_tags = ["knife"]
|
tool_tags = ["knife"]
|
||||||
|
|
@ -198,7 +199,11 @@ class OakBarkRecipe(CraftingRecipe):
|
||||||
output_prototypes = [
|
output_prototypes = [
|
||||||
{"key": "Oak bark",
|
{"key": "Oak bark",
|
||||||
"desc": "Bark of oak, stripped from the core wood.",
|
"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"
|
name = "sword handle"
|
||||||
tool_tags = ["knife"]
|
tool_tags = ["knife"]
|
||||||
consumable_tags = ["oak wood"]
|
consumable_tags = ["cleaned oak wood"]
|
||||||
output_prototypes = [
|
output_prototypes = [
|
||||||
{"key": "Sword handle",
|
{"key": "Sword handle",
|
||||||
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
|
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
|
||||||
|
|
|
||||||
672
evennia/contrib/crafting/tests.py
Normal file
672
evennia/contrib/crafting/tests.py
Normal 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")
|
||||||
|
)
|
||||||
|
|
@ -405,7 +405,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||||
to search. Note that this is used to query the *contents* of a
|
to search. Note that this is used to query the *contents* of a
|
||||||
location and will not match for the location itself -
|
location and will not match for the location itself -
|
||||||
if you want that, don't set this or use `candidates` to specify
|
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
|
attribute_name (str): Define which property to search. If set, no
|
||||||
key+alias search will be performed. This can be used
|
key+alias search will be performed. This can be used
|
||||||
to search database fields (db_ will be automatically
|
to search database fields (db_ will be automatically
|
||||||
|
|
|
||||||
|
|
@ -915,8 +915,15 @@ def spawn(*prototypes, **kwargs):
|
||||||
val = prot.pop("location", None)
|
val = prot.pop("location", None)
|
||||||
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
|
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
|
||||||
|
|
||||||
val = prot.pop("home", settings.DEFAULT_HOME)
|
val = prot.pop("home", None)
|
||||||
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
|
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)
|
val = prot.pop("destination", None)
|
||||||
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)
|
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue