|
@@ -0,0 +1,385 @@
|
|
|
+from copy import copy
|
|
|
+from evennia import logger
|
|
|
+from evennia.utils import evtable
|
|
|
+from evennia.utils.utils import callables_from_module, inherits_from, make_iter, iter_to_string, list_to_string
|
|
|
+from evennia.commands.cmdset import CmdSet
|
|
|
+from commands.command import Command
|
|
|
+from typeclasses.scripts import CmdActionScript
|
|
|
+from utils.utils import toggle_effect, has_effect
|
|
|
+from evennia.utils.create import create_object, create_script
|
|
|
+
|
|
|
+_SPELL_CLASSES = {}
|
|
|
+
|
|
|
+
|
|
|
+def _load_spells():
|
|
|
+ """
|
|
|
+ Delayed loading of recipe classes. This parses
|
|
|
+ `settings.SPELL_MODULES`.
|
|
|
+
|
|
|
+ """
|
|
|
+ from django.conf import settings
|
|
|
+
|
|
|
+ global _SPELL_CLASSES
|
|
|
+ if not _SPELL_CLASSES:
|
|
|
+ paths = []
|
|
|
+ if hasattr(settings, "SPELL_MODULES"):
|
|
|
+ paths += make_iter(settings.SPELL_MODULES)
|
|
|
+ for path in paths:
|
|
|
+ for cls in callables_from_module(path).values():
|
|
|
+ if inherits_from(cls, Spell):
|
|
|
+ _SPELL_CLASSES[cls.name] = cls
|
|
|
+
|
|
|
+
|
|
|
+class Spell:
|
|
|
+ name = "spell base"
|
|
|
+ casting_time = 1
|
|
|
+ duration = 1
|
|
|
+ target_type = None
|
|
|
+ # general cast-failure msg to show after other error-messages.
|
|
|
+ failure_message = ""
|
|
|
+ # show after a successful cast
|
|
|
+ success_message = "You cast {}.".format(name)
|
|
|
+
|
|
|
+ def __init__(self, caster, **kwargs):
|
|
|
+ self.caster = caster
|
|
|
+ self.target_obj = None
|
|
|
+ self.cast_kwargs = kwargs
|
|
|
+ self.allow_cast = True
|
|
|
+
|
|
|
+ def msg(self, message, **kwargs):
|
|
|
+ """
|
|
|
+ Send message to caster. This is a central point to override if wanting
|
|
|
+ to change casting return style in some way.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ message(str): The message to send.
|
|
|
+ **kwargs: Any optional properties relevant to this send.
|
|
|
+
|
|
|
+ """
|
|
|
+ self.caster.msg(message, oob=({"type": "casting"}))
|
|
|
+
|
|
|
+ def pre_cast(self, **kwargs):
|
|
|
+ """
|
|
|
+ Hook to override.
|
|
|
+
|
|
|
+ This is called just before casting operation.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ **kwargs: Any optional extra kwargs passed during initialization of
|
|
|
+ the spell class.
|
|
|
+
|
|
|
+ Raises:
|
|
|
+ CastingError: If validation fails. At this point the caster
|
|
|
+ is expected to have been informed of the problem already.
|
|
|
+ """
|
|
|
+ caster = self.caster
|
|
|
+ self.target_obj = None
|
|
|
+
|
|
|
+ if self.target_type:
|
|
|
+ if not self.cast_kwargs.get('target_name', None):
|
|
|
+ self.msg("You need a target to cast {} on.".format(self.name))
|
|
|
+ raise CastingError("You need a target to cast {} on.".format(self.name))
|
|
|
+
|
|
|
+ target_name = self.cast_kwargs.get('target_name')
|
|
|
+ self.target_obj = caster.search(target_name, location=[caster, caster.location])
|
|
|
+
|
|
|
+ if not self.target_obj:
|
|
|
+ self.msg("{} is not a valid target for {}.".format(target_name, self.name))
|
|
|
+ raise CastingError("{} is not a valid target for {}.".format(target_name, self.name))
|
|
|
+
|
|
|
+ # TODO
|
|
|
+ # gestire i vari tipi di target_type
|
|
|
+
|
|
|
+ def do_cast(self, **kwargs):
|
|
|
+ """
|
|
|
+ Hook to override. This will not be called if validation in `pre_cast`
|
|
|
+ fails.
|
|
|
+
|
|
|
+ This performs the actual casting. At this point the inputs are
|
|
|
+ expected to have been verified already.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ any: The result of casting if any.
|
|
|
+
|
|
|
+ Notes:
|
|
|
+ This method should use `self.msg` to inform the user about the
|
|
|
+ specific reason of failure immediately.
|
|
|
+
|
|
|
+ """
|
|
|
+ return True
|
|
|
+
|
|
|
+ def post_cast(self, cast_result, **kwargs):
|
|
|
+ """
|
|
|
+ Hook to override.
|
|
|
+
|
|
|
+ This is called just after casting has finished.
|
|
|
+
|
|
|
+ """
|
|
|
+ if cast_result:
|
|
|
+ self.msg(self.success_message)
|
|
|
+ elif self.failure_message:
|
|
|
+ self.msg(self.failure_message)
|
|
|
+
|
|
|
+ return cast_result
|
|
|
+
|
|
|
+ def cast(self, raise_exception=False, **kwargs):
|
|
|
+ """
|
|
|
+ Main casting call method. Call this to cast a spell and make
|
|
|
+ sure all hooks run correctly.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ raise_exception (bool): If casting would go wrong, raise
|
|
|
+ exception instead.
|
|
|
+ **kwargs (any): Any other parameters that is relevant
|
|
|
+ for this particular cast operation. This will temporarily
|
|
|
+ override same-named kwargs given at the creation of this recipe
|
|
|
+ and be passed into all the casting hooks.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ any: The result of the cast, or `None`.
|
|
|
+
|
|
|
+ Raises:
|
|
|
+ CastingError: If casting validation failed and
|
|
|
+ `raise_exception` is True.
|
|
|
+ """
|
|
|
+ cast_result = None
|
|
|
+ if self.allow_cast:
|
|
|
+
|
|
|
+ # override/extend cast_kwargs from initialization.
|
|
|
+ cast_kwargs = copy(self.cast_kwargs)
|
|
|
+ cast_kwargs.update(kwargs)
|
|
|
+
|
|
|
+ try:
|
|
|
+ try:
|
|
|
+ # this assigns to self.validated_inputs
|
|
|
+ self.pre_cast(**cast_kwargs)
|
|
|
+ except CastingError:
|
|
|
+ if raise_exception:
|
|
|
+ raise
|
|
|
+ else:
|
|
|
+ cast_result = self.do_cast(**cast_kwargs)
|
|
|
+ finally:
|
|
|
+ cast_result = self.post_cast(cast_result, **cast_kwargs)
|
|
|
+ except CastingError:
|
|
|
+ if raise_exception:
|
|
|
+ raise
|
|
|
+
|
|
|
+ if cast_result is None and raise_exception:
|
|
|
+ raise CastingError(f"Casting of {self.name} failed.")
|
|
|
+ return cast_result
|
|
|
+
|
|
|
+
|
|
|
+def cast(caster, spell_name, raise_exception=False, **kwargs):
|
|
|
+ # delayed loading/caching of spells
|
|
|
+ _load_spells()
|
|
|
+
|
|
|
+ SpellClass = search_spell(caster, spell_name)
|
|
|
+
|
|
|
+ if not SpellClass:
|
|
|
+ raise KeyError(
|
|
|
+ f"No spell in settings.SPELL_MODULES has a name matching {spell_name}"
|
|
|
+ )
|
|
|
+ spell = SpellClass(caster, **kwargs)
|
|
|
+ return spell.cast(raise_exception=raise_exception)
|
|
|
+
|
|
|
+
|
|
|
+def can_cast(caster, spell_name, **kwargs):
|
|
|
+ """
|
|
|
+ Access function.Check if crafter can craft a given recipe from a source recipe module.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ caster (Object): The one doing the crafting.
|
|
|
+ spell_name (str): The `Spell.name` to use. This uses fuzzy-matching
|
|
|
+ if the result is unique.
|
|
|
+ **kwargs: Optional kwargs to pass into the casting.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ list: Error messages, if any.
|
|
|
+ """
|
|
|
+ # delayed loading/caching of spells
|
|
|
+ _load_spells()
|
|
|
+
|
|
|
+ SpellClass = search_spell(caster, spell_name)
|
|
|
+
|
|
|
+ if not SpellClass:
|
|
|
+ raise KeyError(
|
|
|
+ f"No spell in settings.SPELL_MODULES has a name matching {spell_name}"
|
|
|
+ )
|
|
|
+ spell = SpellClass(caster, **kwargs)
|
|
|
+
|
|
|
+ if spell.allow_cast:
|
|
|
+
|
|
|
+ # override/extend craft_kwargs from initialization.
|
|
|
+ cast_kwargs = copy(spell.cast_kwargs)
|
|
|
+ cast_kwargs.update(kwargs)
|
|
|
+
|
|
|
+ try:
|
|
|
+ spell.pre_cast(**cast_kwargs)
|
|
|
+ except CastingError:
|
|
|
+ logger.log_err(CastingError.args)
|
|
|
+ return False
|
|
|
+ else:
|
|
|
+ return True
|
|
|
+
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+def search_spell(caster, spell_name):
|
|
|
+ # delayed loading/caching of spells
|
|
|
+ _load_spells()
|
|
|
+
|
|
|
+ spell_class = _SPELL_CLASSES.get(spell_name, None)
|
|
|
+ if not spell_class:
|
|
|
+ # try a startswith fuzzy match
|
|
|
+ matches = [key for key in _SPELL_CLASSES if key.startswith(spell_name)]
|
|
|
+ if not matches:
|
|
|
+ # try in-match
|
|
|
+ matches = [key for key in _SPELL_CLASSES if spell_name in key]
|
|
|
+ if len(matches) == 1:
|
|
|
+ spell_class = _SPELL_CLASSES.get(matches[0], None)
|
|
|
+
|
|
|
+ return spell_class
|
|
|
+
|
|
|
+
|
|
|
+class CastingCmdSet(CmdSet):
|
|
|
+ """
|
|
|
+ Store Casting command.
|
|
|
+ """
|
|
|
+
|
|
|
+ key = "Casting cmdset"
|
|
|
+
|
|
|
+ def at_cmdset_creation(self):
|
|
|
+ self.add(CmdCast())
|
|
|
+ self.add(CmdSpells())
|
|
|
+
|
|
|
+
|
|
|
+class CmdSpells(Command):
|
|
|
+ """
|
|
|
+ List known spells.
|
|
|
+
|
|
|
+ Usage:
|
|
|
+ spells
|
|
|
+ """
|
|
|
+
|
|
|
+ key = "spells"
|
|
|
+ locks = "cmd:all()"
|
|
|
+ help_category = "General"
|
|
|
+ arg_regex = r"\s|$"
|
|
|
+
|
|
|
+ def func(self):
|
|
|
+ _load_spells()
|
|
|
+
|
|
|
+ caller = self.caller
|
|
|
+
|
|
|
+ table = evtable.EvTable("|wName|n", "|wTarget", "|wCasting time", "|wDuration",
|
|
|
+ border_left_char="|y|||n", border_right_char="|y|||n",
|
|
|
+ border_top_char="|y-|n", border_bottom_char="|y-|n",
|
|
|
+ corner_char="|y+|n")
|
|
|
+
|
|
|
+ for spell in _SPELL_CLASSES.values():
|
|
|
+ if (spell.name in caller.db.spells) or caller.is_superuser:
|
|
|
+ table.add_row("|W{}".format(spell.name), "|M{}".format(spell.target_type), "|G{}".format(spell.casting_time), "|M{}".format(spell.duration))
|
|
|
+
|
|
|
+ caller.msg(table)
|
|
|
+
|
|
|
+
|
|
|
+class CmdCast(Command):
|
|
|
+ """
|
|
|
+ cast a spell.
|
|
|
+
|
|
|
+ Usage:
|
|
|
+ cast <spell> [at <target>]
|
|
|
+
|
|
|
+ Casts a spell.
|
|
|
+ """
|
|
|
+ key = "cast"
|
|
|
+ aliases = ["cs"]
|
|
|
+ lock = "cmd:false()"
|
|
|
+ help_category = "General"
|
|
|
+ arg_regex = r"\s.+|$"
|
|
|
+
|
|
|
+ def parse(self):
|
|
|
+ """
|
|
|
+ Handle parsing of:
|
|
|
+ ::
|
|
|
+ <spell> [at|on <target>]
|
|
|
+ """
|
|
|
+ self.args = args = self.args.strip().lower()
|
|
|
+
|
|
|
+ if " at " in args:
|
|
|
+ spell_name, *rest = args.split(" at ", 1)
|
|
|
+ elif " on " in args:
|
|
|
+ spell_name, *rest = args.split(" on ", 1)
|
|
|
+ else:
|
|
|
+ spell_name, rest = args, []
|
|
|
+
|
|
|
+ target_name = rest[0] if rest else ""
|
|
|
+
|
|
|
+ self.spell_name = spell_name.strip()
|
|
|
+ self.target_name = target_name.strip()
|
|
|
+
|
|
|
+ def func(self):
|
|
|
+ caller = self.caller
|
|
|
+
|
|
|
+ if not self.args or not self.spell_name:
|
|
|
+ caller.msg(self.get_help(caller, self.cmdset))
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.spell_name not in caller.db.spells:
|
|
|
+ caller.msg("You don't know how to cast {}.".format(self.spell_name))
|
|
|
+ return
|
|
|
+
|
|
|
+ if has_effect(caller, "is_busy"):
|
|
|
+ caller.msg("You are already busy {}.".format(caller.db.current_action.busy_msg()))
|
|
|
+ return
|
|
|
+
|
|
|
+ spell_cls = search_spell(caller, self.spell_name)
|
|
|
+ if not spell_cls:
|
|
|
+ caller.msg("You don't know how to cast {}.".format(self.spell_name))
|
|
|
+ return
|
|
|
+
|
|
|
+ if not can_cast(caller, spell_cls.name, target_name=self.target_name):
|
|
|
+ return
|
|
|
+
|
|
|
+ toggle_effect(caller, "is_busy")
|
|
|
+ caller.msg("You start casting {}.".format(spell_cls.name))
|
|
|
+ action_script = create_script("utils.spells.CastCompleteScript", obj=caller, interval=spell_cls.casting_time,
|
|
|
+ attributes=[("spell", spell_cls.name),
|
|
|
+ ("target_name", self.target_name)])
|
|
|
+ caller.db.current_action = action_script
|
|
|
+
|
|
|
+
|
|
|
+class CastCompleteScript(CmdActionScript):
|
|
|
+ def at_script_creation(self):
|
|
|
+ super().at_script_creation()
|
|
|
+
|
|
|
+ self.key = "cmd_cast_complete"
|
|
|
+ self.desc = ""
|
|
|
+
|
|
|
+ self.db.spell = ""
|
|
|
+ self.db.target_name = ""
|
|
|
+
|
|
|
+ def at_repeat(self):
|
|
|
+ caller = self.obj
|
|
|
+
|
|
|
+ if has_effect(caller, "is_busy"):
|
|
|
+ toggle_effect(caller, "is_busy")
|
|
|
+
|
|
|
+ # perform casting (the recipe handles all returns to caller).
|
|
|
+ result = cast(caller, self.db.spell, target_name=self.db.target_name)
|
|
|
+ if result and not isinstance(result, bool):
|
|
|
+ for obj in result:
|
|
|
+ if inherits_from(obj, "typeclasses.objects.Feature"):
|
|
|
+ obj.location = caller.location
|
|
|
+ else:
|
|
|
+ obj.location = caller
|
|
|
+
|
|
|
+ def busy_msg(self):
|
|
|
+ return "casting {}.".format(self.db.spell)
|
|
|
+
|
|
|
+
|
|
|
+class CastingError(RuntimeError):
|
|
|
+ """
|
|
|
+ Casting error.
|
|
|
+
|
|
|
+ """
|