mirror of
https://github.com/evennia/evennia.git
synced 2026-03-24 08:46:31 +01:00
Add crafting contrib
This commit is contained in:
parent
915f1e7023
commit
3db4a35c1e
4 changed files with 1034 additions and 3 deletions
20
evennia/contrib/crafting/README.md
Normal file
20
evennia/contrib/crafting/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Crafting system
|
||||
|
||||
Contrib - Griatch 2020
|
||||
|
||||
This implements a full crafting system. The principle is that of a 'recipe':
|
||||
|
||||
object1 + object2 + ... -> craft_recipe -> objectA, objectB, ...
|
||||
|
||||
The recipe is a class that specifies input and output hooks. By default the
|
||||
input is a list of object-tags (using the "crafting_material" tag-category)
|
||||
and objects passing this check must be passed into the recipe.
|
||||
|
||||
The output is given by a set of prototypes. If the input is correct and other
|
||||
checks are passed (such as crafting skill, for example), these prototypes will
|
||||
be used to generate the new objects being 'crafted'.
|
||||
|
||||
Each recipe is a stand-alone entity which allows for very advanced customization
|
||||
for every recipe - for example one could have a recipe where the input ingredients
|
||||
are not destroyed in the process, or which require other properties of the input
|
||||
(such as a 'quality').
|
||||
747
evennia/contrib/crafting/crafting.py
Normal file
747
evennia/contrib/crafting/crafting.py
Normal file
|
|
@ -0,0 +1,747 @@
|
|||
"""
|
||||
Crafting - Griatch 2020
|
||||
|
||||
This is a general crafting engine. The basic functionality of crafting is to
|
||||
combine any number of of items in a 'recipe' to produce a new result. This is
|
||||
useful not only for traditional crafting but also for puzzle-solving or
|
||||
similar.
|
||||
|
||||
## Installation
|
||||
|
||||
- Create a new module and add it to a new list in your settings file
|
||||
(`server/conf/settings.py`) named `CRAFT_MODULE_RECIPES`.
|
||||
- In the new module, create one or more classes, each a child of
|
||||
`CraftingRecipe` from this module. Each such class must have a unique `.name`
|
||||
property. It also defines what inputs are required and what is created using
|
||||
this recipe.
|
||||
- Objects to use for crafting should (by default) be tagged with tags using the
|
||||
tag-category `crafting_material`. The name of the object doesn't matter, only
|
||||
its tag.
|
||||
- Add the `CmdCraft` command from this module to your default cmdset. This is a
|
||||
very simple example-command (your real command will most likely need to do
|
||||
skill-checks etc!).
|
||||
|
||||
## Usage
|
||||
|
||||
By default the crafter needs to specify which components
|
||||
should be used for the recipe:
|
||||
|
||||
craft spiked club from club, nails
|
||||
|
||||
Here, `spiked club` specifies the recipe while `club` and `nails` are objects
|
||||
the crafter must have in their inventory. These will be consumed during
|
||||
crafting (by default only if crafting was successful).
|
||||
|
||||
A recipe can also require _tools_. These must be either in inventory or in
|
||||
the current location. Tools are not consumed during the crafting.
|
||||
|
||||
craft wooden doll from wood with knife
|
||||
|
||||
In code, you should use the helper function `craft` from this module. This
|
||||
specifies the name of the recipe to use and expects all suitable
|
||||
ingredients/tools as arguments (consumables and tools should be added together,
|
||||
tools will be identified before consumables).
|
||||
|
||||
spiked_club = craft(crafter, "spiked club", club, nails)
|
||||
|
||||
A fail leads to an empty return. The crafter should already have been notified
|
||||
of any error in this case (this should be handle by the recipe itself).
|
||||
|
||||
## Recipes
|
||||
|
||||
A _recipe_ works like an input/output blackbox: you put consumables (and/or
|
||||
tools) into it and if they match the recipe, a new result is spit out.
|
||||
Consumables are consumed in the process while tools are not.
|
||||
|
||||
This module contains a base class for making new ingredient types
|
||||
(`CraftingRecipeBase`) and an implementation of the most common form of
|
||||
crafting (`CraftingRecipe`) using objects and prototypes.
|
||||
|
||||
Recipes are put in one or more modules added as a list to the
|
||||
`CRAFT_MODULE_RECIPES` setting, for example:
|
||||
|
||||
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.
|
||||
|
||||
```python
|
||||
|
||||
from evennia.contrib.crafting.crafting import CraftingRecipe
|
||||
|
||||
class PigIronRecipe(CraftingRecipe):
|
||||
# Pig iron is a high-carbon result of melting iron in a blast furnace.
|
||||
|
||||
name = "pig iron"
|
||||
tool_tags = ["blast furnace"]
|
||||
consumable_tags = ["iron ore", "coal", "coal"]
|
||||
output_prototypes = [
|
||||
{"key": "Pig Iron ingot",
|
||||
"desc": "An ingot of crude pig iron.",
|
||||
"tags": [("pig iron", "crafting_material")]}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
The `evennia/contrib/crafting/example_recipes.py` module has more examples of
|
||||
recipes.
|
||||
|
||||
----
|
||||
|
||||
"""
|
||||
|
||||
from copy import copy
|
||||
from evennia.utils.utils import (
|
||||
iter_to_str, callables_from_module, inherits_from)
|
||||
from evennia.prototypes.spawner import spawn
|
||||
from evennia.utils.create import create_object
|
||||
|
||||
_RECIPE_CLASSES = {}
|
||||
|
||||
|
||||
def _load_recipes():
|
||||
"""
|
||||
Delayed loading of recipe classes. This parses
|
||||
`settings.CRAFT_RECIPE_MODULES`.
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
global _RECIPE_CLASSES
|
||||
if not _RECIPE_CLASSES:
|
||||
for path in settings.CRAFT_RECIPE_MODULES:
|
||||
for cls in callables_from_module(path):
|
||||
if inherits_from(cls, CraftingRecipe):
|
||||
_RECIPE_CLASSES[cls.name] = cls
|
||||
|
||||
|
||||
class CraftingError(RuntimeError):
|
||||
"""
|
||||
Crafting error.
|
||||
|
||||
"""
|
||||
|
||||
class CraftingRecipeBase:
|
||||
"""
|
||||
This is the base of the crafting system. The recipe handles all aspects of
|
||||
performing a 'craft' operation.
|
||||
|
||||
Example of usage:
|
||||
::
|
||||
|
||||
recipe = CraftRecipe(crafter, obj1, obj2, obj3)
|
||||
result = recipe.craft()
|
||||
|
||||
Note that the most common crafting operation is that the inputs are
|
||||
consumed - so in that case the recipe cannot be used a second time (doing so
|
||||
will raise a `CraftingError`)
|
||||
|
||||
"""
|
||||
name = "recipe base"
|
||||
|
||||
# if set, allow running `.craft` more than once on the same instance.
|
||||
# don't set this unless crafting inputs are *not* consumed by the crafting
|
||||
# process (otherwise subsequent calls will fail).
|
||||
allow_reuse = False
|
||||
|
||||
def __init__(self, crafter, *inputs, **kwargs):
|
||||
"""
|
||||
Initialize the recipe.
|
||||
|
||||
Args:
|
||||
crafter (Object): The one doing the crafting.
|
||||
*inputs (any): The ingredients of the recipe to use.
|
||||
**kwargs (any): Any other parameters that are relevant for
|
||||
this recipe.
|
||||
|
||||
"""
|
||||
self.crafter = crafter
|
||||
self.inputs = self.inputs
|
||||
self.craft_kwargs = kwargs
|
||||
self.allow_craft = True
|
||||
|
||||
def msg(self, message, **kwargs):
|
||||
"""
|
||||
Send message to crafter. This is a central point to override if wanting
|
||||
to change crafting return style in some way.
|
||||
|
||||
Args:
|
||||
message(str): The message to send.
|
||||
**kwargs: Any optional properties relevant to this send.
|
||||
|
||||
"""
|
||||
self.crafter.msg(message, {"type": "crafting"})
|
||||
|
||||
def validate_inputs(self, *inputs, **kwargs):
|
||||
"""
|
||||
Hook to override.
|
||||
|
||||
Make sure the provided inputs are valid. This should always be run.
|
||||
|
||||
Args:
|
||||
inputs (any): Items to be tried. .
|
||||
Returns:
|
||||
list or None: Return whichever items were validated (some recipes
|
||||
may allow for partial/too many ingredients) or `None` if validation failed.
|
||||
|
||||
Note:
|
||||
This method is also responsible for properly sending error messages
|
||||
to e.g. self.crafter (usually via `self.msg`).
|
||||
|
||||
"""
|
||||
if self.allow_craft:
|
||||
return self.inputs[:]
|
||||
|
||||
def pre_craft(self, validated_inputs, **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`.
|
||||
**kwargs (any): Passed from `self.craft`.
|
||||
|
||||
Returns:
|
||||
any: The validated_inputs, modified or not.
|
||||
|
||||
"""
|
||||
if not validated_inputs:
|
||||
raise CraftingError()
|
||||
|
||||
return validated_inputs
|
||||
|
||||
def do_craft(self, validated_inputs, **kwargs):
|
||||
"""
|
||||
Hook to override.
|
||||
|
||||
This performs the actual crafting. At this point the inputs are
|
||||
expected to have been verified already.
|
||||
|
||||
Args:
|
||||
validated_inputs (any): Data previously returned from `pre_craft`.
|
||||
kwargs (any): Passed from `self.craft`.
|
||||
|
||||
Returns:
|
||||
any: The result of the crafting.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_craft(self, validated_inputs, 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:
|
||||
validated_inputs (any): The inputs used as part of the crafting.
|
||||
craft_result (any): 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.
|
||||
|
||||
|
||||
"""
|
||||
return craft_result
|
||||
|
||||
def craft(self, raise_exception=False, **kwargs):
|
||||
"""
|
||||
Main crafting call method. Call this to produce a result and make
|
||||
sure all hooks run correctly.
|
||||
|
||||
Args:
|
||||
raise_exception (bool): If crafting would return `None`, raise
|
||||
exception instead.
|
||||
**kwargs (any): Any other parameters that is relevant
|
||||
for this particular craft operation. This will temporarily
|
||||
override same-named kwargs given at the creation of this recipe
|
||||
and be passed into all of the crafting hooks.
|
||||
|
||||
Returns:
|
||||
any: The result of the craft, or `None` if crafting failed.
|
||||
|
||||
Raises:
|
||||
CraftingError: If crafting would return `None` and raise_exception`
|
||||
is set.
|
||||
|
||||
"""
|
||||
craft_result = None
|
||||
err = ""
|
||||
if self.allow_craft:
|
||||
craft_kwargs = copy(self.craft_kwargs)
|
||||
craft_kwargs.update(kwargs)
|
||||
|
||||
try:
|
||||
# this assigns to self.validated_inputs
|
||||
validated_inputs = self.validate_inputs(*self.inputs, **craft_kwargs)
|
||||
|
||||
# run the crafting process
|
||||
self.pre_craft(validated_inputs, **craft_kwargs)
|
||||
craft_result = self.do_craft(validated_inputs, **craft_kwargs)
|
||||
craft_result = self.post_craft(validated_inputs, craft_result, **craft_kwargs)
|
||||
except CraftingError as exc:
|
||||
# use this to abort crafting early
|
||||
if exc.message:
|
||||
self.msg(exc.message)
|
||||
# possibly turn off re-use depending on class setting
|
||||
self.allow_craft = self.allow_reuse
|
||||
else:
|
||||
err = "Cannot re-run crafting without refreshing recipe first."
|
||||
if craft_result is None and raise_exception:
|
||||
raise CraftingError(err)
|
||||
return craft_result
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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'.
|
||||
|
||||
Examples:
|
||||
::
|
||||
|
||||
class SwordRecipe(CraftRecipe):
|
||||
name = "sword"
|
||||
input_tags = ["hilt", "pommel", "strips of leather", "sword blade"]
|
||||
output_prototypes = [
|
||||
{"key": "sword",
|
||||
"typeclass": "typeclassess.weapons.bladed.Sword",
|
||||
"tags": [("sword", "weapon"), ("melee", "weapontype"),
|
||||
("edged", "weapontype")]
|
||||
}
|
||||
]
|
||||
|
||||
## Properties on the class level:
|
||||
|
||||
- `name` (str): The name of this recipe. This should be globally unique.
|
||||
- `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`.
|
||||
- `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
|
||||
right order for crafting to pass.
|
||||
|
||||
### consumables
|
||||
|
||||
- `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.
|
||||
- `exact_consumables` (bool, default True): Normally, adding more consumables
|
||||
than needed leads to a a crafting error. If this is False, the craft will
|
||||
still succeed (only the needed ingredients will be consumed).
|
||||
- `exact_consumable_order` (bool, default False): Normally, the order in which
|
||||
ingredients are added does not matter. With this set, trying to add consumables in
|
||||
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.
|
||||
|
||||
### outputs (result of crafting)
|
||||
|
||||
- `output_prototypes` (list): One or more prototypes (`prototype_keys` or
|
||||
full dicts) describing how to create the result(s) of this recipe.
|
||||
- `output_names` (list): Human-readable names for (prospective) prototypes.
|
||||
This is used in error messages. If not given, this is extracted from the
|
||||
prototypes' `key` if possible.
|
||||
|
||||
### custom error messages
|
||||
|
||||
custom messages all have custom formatting markers (default strings are shown):
|
||||
|
||||
{missing}: Comma-separated list of components missing for missing/out of order errors.
|
||||
{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.
|
||||
{outputs}: Comma-separated list of (expected) outputs
|
||||
{t0}..{tN-1}: Individual tools, same order as `.tool_names`.
|
||||
{c0}..{cN-1}: Individual consumables, same order as `.consumable_names`.
|
||||
{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_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."
|
||||
- `success_message`: "You successfuly craft {outputs}!"
|
||||
- `failed_message`: "You failed to craft {outputs}."
|
||||
|
||||
## 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.
|
||||
|
||||
Use `.msg` to conveniently send messages to the crafter. Raise
|
||||
`evennia.contrib.crafting.crafting.CraftingError` exception to abort
|
||||
crafting at any time in the sequence. If raising with a text, this will be
|
||||
shown to the crafter automatically
|
||||
|
||||
"""
|
||||
name = "crafting recipe"
|
||||
|
||||
# this define the overall category all material tags must have
|
||||
consumable_tag_category = "crafting_material"
|
||||
# tag category for tool objects
|
||||
tool_tag_category = "crafting_tool"
|
||||
|
||||
# the tools needed to perform this crafting. Tools are never consumed (if they were,
|
||||
# they'd need to be a consumable). If more than one instance of a tool is needed,
|
||||
# there should be multiple entries in this list.
|
||||
tool_tags = []
|
||||
# human-readable names for the tools. This will be used for informative messages
|
||||
# or when usage fails. If empty
|
||||
tool_names = []
|
||||
# if we must have exactly the right tools, no more
|
||||
exact_tools = True
|
||||
# if the order of the tools matters
|
||||
exact_tool_order = False
|
||||
# error to show if missing tools
|
||||
error_tool_missing_message = "Could not craft {outputs} without {missing}."
|
||||
# error to show if tool-order matters and it was wrong. Missing is the first
|
||||
# tool out of order
|
||||
error_tool_order_message = \
|
||||
"Could not craft {outputs} since {missing} was added in the wrong order."
|
||||
|
||||
# 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.
|
||||
consumable_tags = []
|
||||
# these are human-readable names for the items to use. This is used for informative
|
||||
# messages or when usage fails. If empty, the tag-names will be used. If given, this
|
||||
# must have the same length as `consumable_tags`.
|
||||
consumable_names = []
|
||||
# if True, consume valid inputs also if crafting failed (returned None)
|
||||
consume_on_fail = False
|
||||
# if True, having any wrong input result in failing the crafting. If False,
|
||||
# extra components beyond the recipe are ignored.
|
||||
exact_consumables = True
|
||||
# if True, the exact order in which inputs are provided matters and must match
|
||||
# the order of `consumable_tags`. If False, order doesn't matter.
|
||||
exact_consumable_order = False
|
||||
# error to show if missing consumables
|
||||
error_consumable_missing_message = "Could not craft {outputs} without {missing}."
|
||||
# error to show if consumable order matters and it was wrong. Missing is the first
|
||||
# consumable out of order
|
||||
error_consumable_order_message = \
|
||||
"Could not craft {outputs} since {missing} was added in the wrong order."
|
||||
|
||||
# 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
|
||||
# these will be returned (as a list) if crafting succeeded.
|
||||
output_prototypes = []
|
||||
# human-readable name(s) for the (expected) result of this crafting. This will usually only
|
||||
# be used for error messages (to report what would have been). If not given, the
|
||||
# prototype's key or typeclass will be used. If given, this must have the same length
|
||||
# as `output_prototypes`.
|
||||
output_names = []
|
||||
|
||||
success_message = "You successfully craft {outputs}!"
|
||||
# custom craft-failure.
|
||||
failed_message = "Failed to craft {outputs}."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.consumable_names:
|
||||
assert len(self.consumable_names) == len(self.consumable_tags), \
|
||||
"Crafting .consumable_names list must have the same length as .consumable_tags."
|
||||
else:
|
||||
self.consumable_names = self.consumable_tags
|
||||
if self.output_names:
|
||||
assert len(self.consumable_names) == len(self.consumable_tags), \
|
||||
"Crafting .output_names list must have the same length as .output_prototypes."
|
||||
else:
|
||||
self.output_names = [
|
||||
prot.get("key", prot.get("typeclass"), "unnamed")
|
||||
for prot in self.output_prototypes]
|
||||
|
||||
self.allow_reuse = not self.consume_inputs
|
||||
|
||||
def _format_message(self, message, **kwargs):
|
||||
|
||||
missing = iter_to_str(kwargs.get("missing", "nothing"))
|
||||
|
||||
# build template context
|
||||
mapping = {"missing": iter_to_str(missing)}
|
||||
mapping.update({
|
||||
f"i{ind}": self.consumable_names[ind]
|
||||
for ind, name in enumerate(self.consumable_names.values())
|
||||
})
|
||||
mapping.update({
|
||||
f"o{ind}": self.output_names[ind]
|
||||
for ind, name in enumerate(self.output_names.values())
|
||||
})
|
||||
mapping["inputs"] = iter_to_str(self.consumable_names)
|
||||
mapping["outputs"] = iter_to_str(self.output_names)
|
||||
|
||||
# populate template and return
|
||||
return message.format(**mapping)
|
||||
|
||||
def seed(self, 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.
|
||||
|
||||
Args:
|
||||
consumable_kwargs (dict, optional): This will be passed as
|
||||
`**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`
|
||||
call for each tool. If not given, the matching
|
||||
`tool_name` or `tool_tag` will be used for key.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple `(tools, consumables)` with newly created dummy
|
||||
objects matching the recipe ingredient list.
|
||||
|
||||
Notes:
|
||||
If `key` is given in `consumable/tool_kwargs` then _every_ created item
|
||||
of each type will have the same key.
|
||||
|
||||
"""
|
||||
if not tool_kwargs:
|
||||
tool_kwargs = {}
|
||||
if not consumable_kwargs:
|
||||
consumable_kwargs = {}
|
||||
tool_key = tool_kwargs.pop("key", None)
|
||||
cons_key = consumable_kwargs.pop("key", None)
|
||||
tool_tags = tool_kwargs.pop("tags", [])
|
||||
cons_tags = consumable_kwargs.pop("tags", [])
|
||||
|
||||
tools = []
|
||||
for itag, tag in enumerate(self.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],
|
||||
**tool_kwargs
|
||||
)
|
||||
)
|
||||
consumables = []
|
||||
for itag, tag in enumerate(self.consumable_tags):
|
||||
consumables.append(
|
||||
create_object(
|
||||
key=cons_key or (self.consumable_names[itag] if
|
||||
self.consumable_names else
|
||||
tag.capitalize()),
|
||||
tags=[(tag, self.consumable_tag_category), *cons_tags]
|
||||
|
||||
)
|
||||
)
|
||||
return tools, consumables
|
||||
|
||||
def validate_inputs(self, *inputs, **kwargs):
|
||||
"""
|
||||
Check so the given inputs are what is needed.
|
||||
|
||||
Note that on successful validation we return a tuple `(tools, consumables)`.
|
||||
|
||||
"""
|
||||
|
||||
def _check_completeness(
|
||||
tagmap, taglist, namelist, exact_match, exact_order,
|
||||
error_missing_message, error_order_message):
|
||||
"""Compare tagmap to taglist"""
|
||||
valids = []
|
||||
for itag, tagkey in enumerate(taglist):
|
||||
found_obj = None
|
||||
for obj, taglist in tagmap.items():
|
||||
if tagkey in taglist:
|
||||
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()))
|
||||
return []
|
||||
if exact_order:
|
||||
# if we get here order is wrong
|
||||
self.msg(self._format_message(
|
||||
error_order_message,
|
||||
missing=namelist[itag] if namelist else tagkey.capitalize()))
|
||||
return []
|
||||
|
||||
# since we pop from the mapping, it gets ever shorter
|
||||
match = tagmap.pop(found_obj, None)
|
||||
if match:
|
||||
valids.append(match)
|
||||
return valids
|
||||
|
||||
# get tools and consumables from inputs from
|
||||
tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
|
||||
for obj in inputs if obj and hasattr(obj, "tags")}
|
||||
consumable_map = {obj: obj.tags.get(category=self.tag_category, return_list=True)
|
||||
for obj in inputs
|
||||
if obj and hasattr(obj, "tags") and obj not in tool_map}
|
||||
|
||||
tools = _check_completeness(
|
||||
tool_map,
|
||||
self.tool_tags,
|
||||
self.tool_names,
|
||||
self.exact_tools,
|
||||
self.exact_tool_order,
|
||||
self.error_tool_message,
|
||||
self.error_tool_order_message
|
||||
)
|
||||
consumables = _check_completeness(
|
||||
consumable_map,
|
||||
self.consumable_tags,
|
||||
self.consumable_names,
|
||||
self.exact_consumables,
|
||||
self.exact_consumable_order,
|
||||
self.error_consumable_missing_message,
|
||||
self.error_consumable_order_message
|
||||
)
|
||||
# regardless of flags, the tools/consumable lists much contain exactly
|
||||
# all the recipe needs now.
|
||||
if (len(tools) == len(self.tool_tags) and len(consumables) == len(self.consumable_tags)):
|
||||
return tools, consumables
|
||||
return None
|
||||
|
||||
# including also empty hooks here for easier reference
|
||||
|
||||
def pre_craft(self, validated_inputs, **kwargs):
|
||||
"""
|
||||
Hook to override.
|
||||
|
||||
This is called just before crafting operation, after inputs have
|
||||
been validated.
|
||||
|
||||
Args:
|
||||
validated_inputs (tuple): 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.
|
||||
|
||||
"""
|
||||
if not validated_inputs:
|
||||
# abort crafting here, remove if wanting some other action
|
||||
raise CraftingError(f"Crafting validation error {self.name}")
|
||||
|
||||
return validated_inputs
|
||||
|
||||
def do_craft(self, validated_inputs, **kwargs):
|
||||
"""
|
||||
Hook to override.
|
||||
|
||||
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.
|
||||
|
||||
Notes:
|
||||
We may want to analyze the tools in some way here to affect the
|
||||
crafting process.
|
||||
|
||||
"""
|
||||
return spawn(*self.output_prototypes)
|
||||
|
||||
def post_craft(self, validated_inputs, 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:
|
||||
validated_inputs (tuple): the validated inputs, a tuple `(tools, consumables)`.
|
||||
craft_result (any): 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.
|
||||
|
||||
|
||||
"""
|
||||
consume = self.consume_inputs
|
||||
|
||||
_, consumables = validated_inputs or (None, None)
|
||||
|
||||
if craft_result:
|
||||
self.msg(self._format_message(self.success_message))
|
||||
else:
|
||||
self.msg(self._format_message(self.failure_message))
|
||||
consume = self.consume_on_fail
|
||||
|
||||
if consume and consumables:
|
||||
# consume the inputs
|
||||
for obj in consumables:
|
||||
obj.delete()
|
||||
|
||||
return craft_result
|
||||
|
||||
|
||||
# access functions
|
||||
|
||||
|
||||
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
|
||||
`settings.CRAFT_RECIPE_MODULES` to be added to a list of one or more
|
||||
python-paths to modules holding Recipe-classes.
|
||||
|
||||
Args:
|
||||
crafter (Object): The one doing the crafting.
|
||||
recipe_name (str): This should match the `CraftRecipe.name` to use.
|
||||
*inputs: Suitable ingredients (Objects) to use in the crafting.
|
||||
raise_exception (bool, optional): If crafting failed for whatever
|
||||
reason, raise `CraftingError`.
|
||||
**kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft).
|
||||
|
||||
Returns:
|
||||
list: Crafted objects, if any.
|
||||
|
||||
Raises:
|
||||
CraftingError: If `raise_exception` is True and crafting failed to produce an output.
|
||||
KeyError: If `recipe_name` failed to find a matching recipe class.
|
||||
|
||||
Notes:
|
||||
If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and
|
||||
lastly fall back to the example module `"evennia.contrib."`
|
||||
|
||||
"""
|
||||
# delayed loading/caching of recipes
|
||||
_load_recipes()
|
||||
|
||||
RecipeClass = _RECIPE_CLASSES.get(recipe_name, None)
|
||||
if not RecipeClass:
|
||||
raise KeyError("No recipe in settings.CRAFT_RECIPE_MODULES "
|
||||
f"has a name matching {recipe_name}")
|
||||
recipe = RecipeClass(crafter, *inputs, **kwargs)
|
||||
return recipe.craft(raise_exception=raise_exception)
|
||||
263
evennia/contrib/crafting/example_recipes.py
Normal file
263
evennia/contrib/crafting/example_recipes.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
"""
|
||||
Example recipes for the crafting system - how to make a sword.
|
||||
|
||||
See the _SwordSmithingBaseRecipe for an example of extendng the recipe with a
|
||||
mocked 'skill' system (just random chance in our case). The skill system used
|
||||
is game-specific but likely to be needed for most 'real' crafting systems.
|
||||
|
||||
Note that 'tools' are references to the tools used - they don't need to be in
|
||||
the inventory of the crafter. So when 'blast furnace' is given below, it is a
|
||||
reference to a blast furnace used, not suggesting the crafter is carrying it
|
||||
around with them.
|
||||
|
||||
::
|
||||
|
||||
Sword crafting tree
|
||||
|
||||
# base materials (consumables)
|
||||
|
||||
iron ore, ash, sand, coal, oak wood, water, fur
|
||||
|
||||
# base tools (marked with [T] for clarity and assumed to already exist)
|
||||
|
||||
blast furnace[T], furnace[T], crucible[T], anvil[T],
|
||||
hammer[T], knife[T], cauldron[T]
|
||||
|
||||
# recipes for making a sword
|
||||
|
||||
pig iron = iron ore + 2xcoal + blast furnace[T]
|
||||
crucible_steel = pig iron + ash + sand + 2xcoal + crucible[T]
|
||||
sword blade = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
sword pommel = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
|
||||
rawhide = fur + knife[T]
|
||||
oak bark = oak wood + knife[T]
|
||||
leather = rawhide + oak bark + water + cauldron[T]
|
||||
|
||||
sword handle = oak wood + knife[T]
|
||||
|
||||
sword = sword blade + sword guard + sword pommel
|
||||
+ sword handle + leather + knife[T] + hammer[T] + furnace[T]
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from random import random
|
||||
from .crafting import CraftingRecipe
|
||||
|
||||
|
||||
class PigIronRecipe(CraftingRecipe):
|
||||
"""
|
||||
Pig iron is a high-carbon result of melting iron in a blast furnace.
|
||||
|
||||
"""
|
||||
name = "pig iron"
|
||||
tool_tags = ["blast furnace"]
|
||||
consumable_tags = ["iron ore", "coal", "coal"]
|
||||
output_prototypes = [
|
||||
{"key": "Pig Iron ingot",
|
||||
"desc": "An ingot of crude pig iron.",
|
||||
"tags": [("pig iron", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class CrucibleSteelRecipe(CraftingRecipe):
|
||||
"""
|
||||
Mixing pig iron with impurities like ash and sand and melting it in a
|
||||
crucible produces a medieval level of steel (like damascus steel).
|
||||
|
||||
"""
|
||||
name = "crucible steel"
|
||||
tool_tags = ["crucible"]
|
||||
consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"]
|
||||
output_prototypes = [
|
||||
{"key": "Crucible steel ingot",
|
||||
"desc": "An ingot of multi-colored crucible steel.",
|
||||
"tags": [("crucible steel", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class _SwordSmithingBaseRecipe(CraftingRecipe):
|
||||
"""
|
||||
A parent for all metallurgy sword-creation recipes. Those have a chance to
|
||||
failure but since steel is not lost in the process you can always try
|
||||
again.
|
||||
|
||||
"""
|
||||
|
||||
success_message = "Your smithing work bears fruit and you craft {outputs}!"
|
||||
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):
|
||||
"""
|
||||
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
|
||||
against a skill found on `self.crafter`). In this case you can always
|
||||
start over since steel is not lost but can be re-smelted again for
|
||||
another try.
|
||||
|
||||
Args:
|
||||
validated_inputs (list): all consumables/tools being used.
|
||||
**kwargs: any extra kwargs passed during crafting.
|
||||
|
||||
Returns:
|
||||
any: The result of the craft, or None if a failure.
|
||||
|
||||
Notes:
|
||||
Depending on if we return a crafting result from this
|
||||
method or not, `success_message` or `failure_message`
|
||||
will be echoed to the crafter.
|
||||
|
||||
(for more control we could also message directly and raise
|
||||
crafting.CraftingError to abort craft process on failure).
|
||||
|
||||
"""
|
||||
if random.random() < 0.8:
|
||||
# 80% chance of success. This will spawn the sword and show
|
||||
# success-message.
|
||||
return super().do_craft()
|
||||
else:
|
||||
# fail and show failed message
|
||||
return None
|
||||
|
||||
|
||||
class SwordBladeRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
A [sword]blade requires hammering the steel out into shape using heat and
|
||||
force. This also includes the tang, which is the base for the hilt (the
|
||||
part of the sword you hold on to).
|
||||
|
||||
"""
|
||||
name = "sword blade"
|
||||
tool_tags = ["hammer", "anvil", "furnace"]
|
||||
consumable_tags = ["crucible steel"]
|
||||
output_prototypes = [
|
||||
{"key": "Sword blade",
|
||||
"desc": "A long blade that may one day become a sword.",
|
||||
"tags": [("sword blade", "crafting_material")]}
|
||||
]
|
||||
|
||||
class SwordPommelRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding
|
||||
it together.
|
||||
|
||||
"""
|
||||
name = "sword pommel"
|
||||
tool_tags = ["hammer", "anvil", "furnace"]
|
||||
consumable_tags = ["crucible steel"]
|
||||
output_prototypes = [
|
||||
{"key": "Sword pommel",
|
||||
"desc": "The pommel for a future sword.",
|
||||
"tags": [("sword pommel", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class SwordGuardRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
The guard stops the hand from accidentally sliding off the hilt onto the
|
||||
sword's blade and also protects the hand when parrying.
|
||||
|
||||
"""
|
||||
name = "sword pommel"
|
||||
tool_tags = ["hammer", "anvil", "furnace"]
|
||||
consumable_tags = ["crucible steel"]
|
||||
output_prototypes = [
|
||||
{"key": "Sword guard",
|
||||
"desc": "The cross-guard for a future sword.",
|
||||
"tags": [("sword guard", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class RawhideRecipe(CraftingRecipe):
|
||||
"""
|
||||
Rawhide is animal skin cleaned and stripped of hair.
|
||||
|
||||
"""
|
||||
name = "rawhide"
|
||||
tool_tags = ["knife"]
|
||||
consumable_tags = ["fur"]
|
||||
output_prototypes = [
|
||||
{"key": "Rawhide",
|
||||
"desc": "Animal skin, cleaned and with hair removed.",
|
||||
"tags": [("rawhide", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
name = "oak bark"
|
||||
tool_tags = ["knife"]
|
||||
consumable_tags = ["oak wood"]
|
||||
output_prototypes = [
|
||||
{"key": "Oak bark",
|
||||
"desc": "Bark of oak, stripped from the core wood.",
|
||||
"tags": [("oak bark", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class LeatherRecipe(CraftingRecipe):
|
||||
"""
|
||||
Leather is produced by tanning rawhide in a process traditionally involving
|
||||
the chemical Tannin. Here we abbreviate this process a bit. Maybe a
|
||||
'tanning rack' tool should be required too ...
|
||||
|
||||
"""
|
||||
name = "leather"
|
||||
tool_tags = ["cauldron"]
|
||||
consumable_tags = ["rawhide", "oak bark", "water"]
|
||||
output_prototypes = [
|
||||
{"key": "Piece of Leather",
|
||||
"desc": "A piece of leather.",
|
||||
"tags": [("leather", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class SwordHandleRecipe(CraftingRecipe):
|
||||
"""
|
||||
The handle is the part of the hilt between the guard and the pommel where
|
||||
you hold the sword. It consists of wooden pieces around the steel tang. It
|
||||
is wrapped in leather, but that will be added at the end.
|
||||
|
||||
"""
|
||||
name = "sword handle"
|
||||
tool_tags = ["knife"]
|
||||
consumable_tags = ["oak wood"]
|
||||
output_prototypes = [
|
||||
{"key": "Sword handle",
|
||||
"desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
|
||||
"tags": [("sword handle", "crafting_material")]}
|
||||
]
|
||||
|
||||
|
||||
class SwordRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
A finished sword consists of a Blade ending in a non-sharp part called the
|
||||
Tang. The cross Guard is put over the tang against the edge of the blade.
|
||||
The Handle is put over the tang to give something easier to hold. The
|
||||
Pommel locks everything in place. The handle is wrapped in leather
|
||||
strips for better grip.
|
||||
|
||||
This covers only a single 'sword' type.
|
||||
|
||||
"""
|
||||
name = "sword"
|
||||
tool_tags = ["hammer", "furnace", "knife"]
|
||||
consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle",
|
||||
"leather"]
|
||||
output_prototypes = [
|
||||
{"key": "Sword",
|
||||
"desc": "A bladed weapon.",
|
||||
# setting the tag as well - who knows if one can make something from this too!
|
||||
"tags": [("sword", "crafting_material")]}
|
||||
# obviously there would be other properties of a 'sword' added here
|
||||
# too, depending on how combat works in the your game!
|
||||
]
|
||||
# this requires more precision
|
||||
exact_consumable_order = True
|
||||
|
|
@ -344,7 +344,7 @@ def columnize(string, columns=2, spacing=4, align="l", width=None):
|
|||
return "\n".join(rows)
|
||||
|
||||
|
||||
def iter_to_string(initer, endsep="and", addquote=False):
|
||||
def iter_to_str(initer, endsep="and", addquote=False):
|
||||
"""
|
||||
This pretty-formats an iterable list as string output, adding an optional
|
||||
alternative separator to the second to last entry. If `addquote`
|
||||
|
|
@ -391,8 +391,9 @@ def iter_to_string(initer, endsep="and", addquote=False):
|
|||
return ", ".join(str(v) for v in initer[:-1]) + "%s %s" % (endsep, initer[-1])
|
||||
|
||||
|
||||
# legacy alias
|
||||
list_to_string = iter_to_string
|
||||
# legacy aliases
|
||||
list_to_string = iter_to_str
|
||||
iter_to_string = iter_to_str
|
||||
|
||||
|
||||
def wildcard_to_regexp(instring):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue