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 [at ] Casts a spell. """ key = "cast" aliases = ["cs"] lock = "cmd:false()" help_category = "General" arg_regex = r"\s.+|$" def parse(self): """ Handle parsing of: :: [at|on ] """ 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. """