Add magic-crafting example to crafting readme. Resolve #2554
This commit is contained in:
parent
798fb7d92d
commit
fd2c0d9fb7
2 changed files with 244 additions and 9 deletions
|
|
@ -480,7 +480,7 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
# there should be multiple entries in this list.
|
# there should be multiple entries in this list.
|
||||||
tool_tags = []
|
tool_tags = []
|
||||||
# human-readable names for the tools. This will be used for informative messages
|
# human-readable names for the tools. This will be used for informative messages
|
||||||
# or when usage fails. If empty
|
# or when usage fails. If empty, use tag-names.
|
||||||
tool_names = []
|
tool_names = []
|
||||||
# if we must have exactly the right tools, no more
|
# if we must have exactly the right tools, no more
|
||||||
exact_tools = True
|
exact_tools = True
|
||||||
|
|
@ -628,20 +628,23 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
return message.format(**mapping)
|
return message.format(**mapping)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def seed(cls, tool_kwargs=None, consumable_kwargs=None):
|
def seed(cls, tool_kwargs=None, consumable_kwargs=None, location=None):
|
||||||
"""
|
"""
|
||||||
This is a helper class-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 names
|
recipe. When called, it will create simple dummy ingredients with names
|
||||||
and tags needed by this recipe.
|
and tags needed by this recipe.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object`
|
||||||
|
call for each tool. If not given, the matching
|
||||||
|
`tool_name` or `tool_tag` will be used for key.
|
||||||
consumable_kwargs (dict, optional): This will be passed as
|
consumable_kwargs (dict, optional): This will be passed as
|
||||||
`**consumable_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 `**tool_kwargs` into the `create_object`
|
location (Object, optional): If given, the created items will be created in this
|
||||||
call for each tool. If not given, the matching
|
location. This is a shortcut for adding {"location": <obj>} to both the
|
||||||
`tool_name` or `tool_tag` will be used for key.
|
consumable/tool kwargs (and will *override* any such setting in those kwargs).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: A tuple `(tools, consumables)` with newly created dummy
|
tuple: A tuple `(tools, consumables)` with newly created dummy
|
||||||
|
|
@ -649,8 +652,7 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
::
|
::
|
||||||
|
tools, consumables = SwordRecipe.seed(location=caller)
|
||||||
tools, consumables = SwordRecipe.seed()
|
|
||||||
recipe = SwordRecipe(caller, *(tools + consumables))
|
recipe = SwordRecipe(caller, *(tools + consumables))
|
||||||
result = recipe.craft()
|
result = recipe.craft()
|
||||||
|
|
||||||
|
|
@ -663,6 +665,11 @@ class CraftingRecipe(CraftingRecipeBase):
|
||||||
tool_kwargs = {}
|
tool_kwargs = {}
|
||||||
if not consumable_kwargs:
|
if not consumable_kwargs:
|
||||||
consumable_kwargs = {}
|
consumable_kwargs = {}
|
||||||
|
|
||||||
|
if location:
|
||||||
|
tool_kwargs['location'] = location
|
||||||
|
consumable_kwargs['location'] = location
|
||||||
|
|
||||||
tool_key = tool_kwargs.pop("key", None)
|
tool_key = tool_kwargs.pop("key", None)
|
||||||
cons_key = consumable_kwargs.pop("key", None)
|
cons_key = consumable_kwargs.pop("key", None)
|
||||||
tool_tags = tool_kwargs.pop("tags", [])
|
tool_tags = tool_kwargs.pop("tags", [])
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,41 @@ around with them.
|
||||||
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]
|
||||||
|
|
||||||
|
|
||||||
|
## 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 level3+
|
||||||
|
|
||||||
|
# recipe for fireball
|
||||||
|
|
||||||
|
fireball = spellbook[T] + wand[T] + [firemagic skill lvl3+]
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from random import random
|
from random import random, randint
|
||||||
from .crafting import CraftingRecipe
|
from evennia.commands.command import Command, InterruptCommand
|
||||||
|
from .crafting import craft, CraftingRecipe, CraftingValidationError
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
# Sword recipe
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
class PigIronRecipe(CraftingRecipe):
|
class PigIronRecipe(CraftingRecipe):
|
||||||
"""
|
"""
|
||||||
|
|
@ -300,3 +328,203 @@ class SwordRecipe(_SwordSmithingBaseRecipe):
|
||||||
]
|
]
|
||||||
# this requires more precision
|
# this requires more precision
|
||||||
exact_consumable_order = True
|
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}.")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue