Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.contrib.game_systems.crafting.example_recipes

    """
    How to make a sword - example crafting tree for the crafting system.

    See the `SwordSmithingBaseRecipe` in this module 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 + cleaned oak wood = oak wood + knife[T]
        leather = rawhide + oak bark + water + cauldron[T]

        sword handle = cleaned oak wood + knife[T]

        sword = sword blade + sword guard + sword pommel
                + sword handle + leather + knife[T] + hammer[T] + furnace[T]

    ## Recipes used for spell casting

    This is a simple example modifying the base Recipe to use as a way
    to describe magical spells instead. It combines tools with
    a skill (an attribute on the caster) in order to produce a magical effect.

    The example `CmdCast` command can be added to the CharacterCmdset in
    `mygame/commands/default_cmdsets` to test it out. The 'effects' are
    just mocked for the example.

    ::
        # base tools (assumed to already exist)

        spellbook[T], wand[T]

        # skill (stored as Attribute on caster)

        firemagic skill level10+

        # recipe for fireball

        fireball = spellbook[T] + wand[T] + [firemagic skill lvl10+]

    ----

    """

    from random import randint, random

    from evennia.commands.command import Command, InterruptCommand

    from .crafting import CraftingRecipe, CraftingValidationError, craft

    # ------------------------------------------------------------
    # Sword recipe
    # ------------------------------------------------------------

    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 craft(self, **kwargs):
            """
            Making a sword blade takes skill. Here we emulate this by introducing a
            random chance of failure (in a real game this could be a skill check
            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().craft(**kwargs)
            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 guard"
        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.

        This produces two outputs - the bark and the cleaned wood.
        """

        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")],
            },
            {
                "key": "Oak Wood (cleaned)",
                "desc": "Oakwood core, stripped of bark.",
                "tags": [("cleaned oak wood", "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 = ["cleaned oak wood"]
        output_prototypes = [
            {
                "key": "Sword handle",
                "desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.",
                "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

    # ------------------------------------------------------------
    # Recipes for spell casting
    # ------------------------------------------------------------

    class _MagicRecipe(CraftingRecipe):
        """
        A base 'recipe' to represent magical spells.

        We *could* treat this just like the sword above - by combining the wand and spellbook to make a
        fireball object that the user can then throw with another command. For this example we instead
        generate 'magical effects' as strings+values that we would then supposedly inject into a
        combat system or other resolution system.

        We also assume that the crafter has skills set on itself as plain Attributes.

        """

        name = ""
        # all spells require a spellbook and a wand (so there!)
        tool_tags = ["spellbook", "wand"]

        error_tool_missing_message = "Cannot cast spells without {missing}."
        success_message = "You successfully cast the spell!"
        # custom properties
        skill_requirement = []  # this should be on the form [(skillname, min_level)]
        skill_roll = ""  # skill to roll for success
        desired_effects = []  # on the form [(effect, value), ...]
        failure_effects = []  # ''
        error_too_low_skill_level = "Your skill {skill_name} is too low to cast {spell}."
        error_no_skill_roll = "You must have the skill {skill_name} to cast the spell {spell}."

        def pre_craft(self, **kwargs):
            """
            This is where we do input validation. We want to do the
            normal validation of the tools, but also check for a skill
            on the crafter. This must set the result on `self.validated_inputs`.
            We also set the crafter's relevant skill value on `self.skill_roll_value`.

            Args:
                **kwargs: Any optional extra kwargs passed during initialization of
                    the recipe class.

            Raises:
                CraftingValidationError: If validation fails. At this point the crafter
                    is expected to have been informed of the problem already.

            """
            # this will check so the spellbook and wand are at hand.
            super().pre_craft(**kwargs)

            # at this point we have the items available, let's also check for the skill. We
            # assume the  crafter has the skill available as an Attribute
            # on itself.

            crafter = self.crafter
            for skill_name, min_value in self.skill_requirements:
                skill_value = crafter.attributes.get(skill_name)

                if skill_value is None or skill_value < min_value:
                    self.msg(
                        self.error_too_low_skill_level.format(skill_name=skill_name, spell=self.name)
                    )
                    raise CraftingValidationError

            # get the value of the skill to roll
            self.skill_roll_value = self.crafter.attributes.get(self.skill_roll)
            if self.skill_roll_value is None:
                self.msg(self.error_no_skill_roll.format(skill_name=self.skill_roll, spell=self.name))
                raise CraftingValidationError

        def do_craft(self, **kwargs):
            """
            'Craft' the magical effect. When we get to this point we already know we have all the
            prequisite for creating the effect. In this example we will store the effect on the crafter;
            maybe this enhances the crafter or makes a new attack available to them in combat.

            An alternative to this would of course be to spawn an actual object for the effect, like
            creating a potion or an actual fireball-object to throw (this depends on how your combat
            works).

            """
            # we do a simple skill check here.
            if randint(1, 18) <= self.skill_roll_value:
                # a success!
                return True, self.desired_effects
            else:
                # a failure!
                return False, self.failure_effects

        def post_craft(self, craft_result, **kwargs):
            """
            Always called at the end of crafting, regardless of successful or not.

            Since we get a custom craft result (True/False, effects) we need to
            wrap the original post_craft to output the error messages for us
            correctly.

            """
            success = False
            if craft_result:
                success, _ = craft_result
            # default post_craft just checks if craft_result is truthy or not.
            # we don't care about its return value since we already have craft_result.
            super().post_craft(success, **kwargs)
            return craft_result

    class FireballRecipe(_MagicRecipe):
        """
        A Fireball is a magical effect that can be thrown at a target to cause damage.

        Note that the magic-effects are just examples, an actual rule system would
        need to be created to understand what they mean when used.

        """

        name = "fireball"
        skill_requirements = [("firemagic", 10)]  # skill 'firemagic' lvl 10 or higher
        skill_roll = "firemagic"
        success_message = "A ball of flame appears!"
        desired_effects = [("target_fire_damage", 25), ("ranged_attack", -2), ("mana_cost", 12)]
        failure_effects = [("self_fire_damage", 5), ("mana_cost", 5)]

    class HealingRecipe(_MagicRecipe):
        """
        Healing magic will restore a certain amount of health to the target over time.

        Note that the magic-effects are just examples, an actual rule system would
        need to be created to understand what they mean.

        """

        name = "heal"
        skill_requirements = [("bodymagic", 5), ("empathy", 10)]
        skill_roll = "bodymagic"
        success_message = "You successfully extend your healing aura."
        desired_effects = [("healing", 15), ("mana_cost", 5)]
        failure_effects = []

    class CmdCast(Command):
        """
        Cast a magical spell.

        Usage:
            cast <spell> <target>

        """

        key = "cast"

        def parse(self):
            """
            Simple parser, assuming spellname doesn't have spaces.
            Stores result in self.target and self.spellname.

            """
            args = self.args.strip().lower()
            target = None
            if " " in args:
                self.spellname, *target = args.split(" ", 1)
            else:
                self.spellname = args

            if not self.spellname:
                self.caller.msg("You must specify a spell name.")
                raise InterruptCommand

            if target:
                self.target = self.caller.search(target[0].strip())
                if not self.target:
                    raise InterruptCommand
            else:
                self.target = self.caller

        def func(self):
            # all items carried by the caller could work
            possible_tools = self.caller.contents

            try:
                # if this completes without an exception, the caster will have
                # a new magic_effect set on themselves, ready to use or apply in some way.
                success, effects = craft(
                    self.caller, self.spellname, *possible_tools, raise_exception=True
                )
            except CraftingValidationError:
                return
            except KeyError:
                self.caller.msg(f"You don't know of a spell called '{self.spellname}'")
                return

            # Applying the magical effect to target would happen below.
            # self.caller.db.active_spells[self.spellname] holds all the effects
            # of this particular prepared spell. For a fireball you could perform
            # an attack roll here and apply damage if you hit. For healing you would heal the target
            # (which could be yourself) by a number of health points given by the recipe.
            effect_txt = ", ".join(f"{eff[0]}({eff[1]})" for eff in effects)
            success_txt = "|gsucceeded|n" if success else "|rfailed|n"
            self.caller.msg(
                f"Casting the spell {self.spellname} on {self.target} {success_txt}, "
                f"causing the following effects: {effect_txt}."
            )

Note

You are reading an old version of the Evennia documentation. The latest version is here

.