Browse Source

sistemati spells

Francesco Cappelli 2 years ago
parent
commit
5ef1b78c42

+ 0 - 58
commands/command.py

@@ -21,8 +21,6 @@ from typeclasses.rooms import IndoorRoom
 from typeclasses.objects import Item, EquippableItem
 from typeclasses.scripts import Script, CmdActionScript
 
-from world import spells
-
 CMD_SEARCH_TIME = 5
 
 
@@ -574,62 +572,6 @@ class CmdInventory(Command):
         self.caller.msg(string)
 
 
-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
-
-        spell_id = self.spell_name.replace(' ', '_')
-        if self.spell_name not in caller.db.spells:
-            caller.msg("You cannot cast {}.".format(self.spell_name))
-            return
-
-        if hasattr(spells, "spell_" + spell_id):
-            spell = getattr(spells, "spell_" + spell_id)
-        else:
-            logger.log_err("Cannot find spell {}.".format("spell_" + spell_id))
-            return
-
-        spell(caller, self.target_name)
-
-
 class CmdTestPy(Command):
     key = "testpy"
     aliases = ["testpy"]

+ 7 - 4
commands/default_cmdsets.py

@@ -24,7 +24,6 @@ from commands.command import CmdTestPy
 from commands.command import CmdSearch
 from commands.command import CmdEquip
 from commands.command import CmdInventory
-from commands.command import CmdCast
 from commands.command import CmdPut
 from commands.command import CmdOpenCloseDoor
 
@@ -35,7 +34,10 @@ from commands.builder import CmdZone
 from commands.builder import CmdAddToZone
 from commands.builder import CmdPopen
 
-from utils.crafting import CmdCraft
+from commands.unloggedin import CmdUnconnectedLook
+
+from utils.crafting import CraftingCmdSet
+from utils.spells import CastingCmdSet
 
 
 class CharacterCmdSet(default_cmds.CharacterCmdSet):
@@ -64,7 +66,6 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
         self.add(CmdSearch())
         self.add(CmdEquip())
         self.add(CmdInventory())
-        self.add(CmdCast())
         self.add(CmdPut())
         self.add(CmdOpenCloseDoor())
 
@@ -75,7 +76,8 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
         self.add(CmdAddToZone())
         self.add(CmdPopen())
 
-        self.add(CmdCraft())
+        self.add(CraftingCmdSet())
+        self.add(CastingCmdSet())
 
 
 class AccountCmdSet(default_cmds.AccountCmdSet):
@@ -114,6 +116,7 @@ class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
         #
         # any commands you add below will overload the default ones.
         #
+        self.add(CmdUnconnectedLook())
 
 
 class SessionCmdSet(default_cmds.SessionCmdSet):

+ 36 - 0
commands/unloggedin.py

@@ -0,0 +1,36 @@
+from evennia import Command
+from evennia.utils.evmenu import EvMenu
+from evennia.commands.cmdhandler import CMD_LOGINSTART
+
+from menus.login import plain_node_formatter
+
+
+class CmdUnconnectedLook(Command):
+    """
+    look when in unlogged-in state
+
+    Usage:
+      look
+
+    This is an unconnected version of the look command for simplicity.
+
+    This is called by the server and kicks everything in gear.
+    All it does is display the connect screen.
+    """
+
+    key = CMD_LOGINSTART
+    aliases = ["look", "l"]
+    locks = "cmd:all()"
+
+    def func(self):
+        """Show the connect screen."""
+        EvMenu(
+            self.caller,
+            "menus.login",
+            startnode="node_enter_username",
+            auto_look=False,
+            auto_quit=False,
+            cmd_on_exit=None,
+            node_formatter=plain_node_formatter,
+        )
+

+ 58 - 0
menus/char_manager.py

@@ -0,0 +1,58 @@
+_ACCOUNT_HELP = (
+    "Enter the name you used to log into the game before, " "or a new account-name if you are new."
+)
+_PASSWORD_HELP = (
+    "Password should be a minimum of 8 characters (preferably longer) and "
+    "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."
+)
+
+
+def _show_help(caller, raw_string, **kwargs):
+    """Echo help message, then re-run node that triggered it"""
+    help_entry = kwargs["help_entry"]
+    caller.msg(help_entry)
+    return None  # re-run calling node
+
+
+def node_enter_char_management(caller, raw_string, **kwargs):
+    account = caller.ndb._menutree.account
+
+    if not account:
+        raise Exception("No account found!")
+
+    # TODO
+    # text = "{}".format(w_first_login)
+    #
+    # options = (
+    #     {"key": "", "goto": "node_enter_char_management"},
+    #     {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
+    #     {"key": "_default", "goto": "node_enter_char_management"},
+    # )
+    #
+    # return text, options
+
+    # TODO
+    character = account.db._last_puppet
+    character.db.health = 10
+    character.db.mana = 10
+
+    character.db.spells = []
+    character.db.spells.append('light')
+    character.db.recipes = []
+    character.db.recipes.append('vial of blood')
+    character.db.recipes.append('torch')
+
+    caller.msg("|gLogging in ...|n")
+    caller.sessionhandler.login(caller, account)
+    return "", {}
+
+
+def plain_node_formatter(nodetext, optionstext, caller=None):
+    """Do not display the options, only the text.
+
+    This function is used by EvMenu to format the text of nodes. The menu login
+    is just a series of prompts, so we disable all automatic display decoration
+    and let the nodes handle everything on their own.
+
+    """
+    return nodetext

+ 203 - 0
menus/login.py

@@ -0,0 +1,203 @@
+from django.conf import settings
+from evennia.utils import utils
+
+from evennia.utils.evmenu import EvMenu
+
+CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
+_GUEST_ENABLED = settings.GUEST_ENABLED
+_ACCOUNT = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
+_GUEST = utils.class_from_module(settings.BASE_GUEST_TYPECLASS)
+
+_ACCOUNT_HELP = (
+    "Enter the name you used to log into the game before, " "or a new account-name if you are new."
+)
+_PASSWORD_HELP = (
+    "Password should be a minimum of 8 characters (preferably longer) and "
+    "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."
+)
+
+
+def _show_help(caller, raw_string, **kwargs):
+    """Echo help message, then re-run node that triggered it"""
+    help_entry = kwargs["help_entry"]
+    caller.msg(help_entry)
+    return None  # re-run calling node
+
+
+def node_enter_username(caller, raw_text, **kwargs):
+    """
+    Start node of menu
+    Start login by displaying the connection screen and ask for a user name.
+
+    """
+    def _check_input(caller, username, **kwargs):
+        """
+        'Goto-callable', set up to be called from the _default option below.
+
+        Called when user enters a username string. Check if this username already exists and set the flag
+        'new_user' if not. Will also directly login if the username is 'guest'
+        and GUEST_ENABLED is True.
+
+        The return from this goto-callable determines which node we go to next
+        and what kwarg it will be called with.
+
+        """
+        username = username.rstrip("\n")
+
+        if username == "guest" and _GUEST_ENABLED:
+            # do an immediate guest login
+            session = caller
+            address = session.address
+            account, errors = _GUEST.authenticate(ip=address)
+            if account:
+                return "node_quit_or_login", {"login": True, "account": account}
+            else:
+                session.msg("|R{}|n".format("\n".join(errors)))
+                return None  # re-run the username node
+
+        try:
+            _ACCOUNT.objects.get(username__iexact=username)
+        except _ACCOUNT.DoesNotExist:
+            new_user = True
+        else:
+            new_user = False
+
+        # pass username/new_user into next node as kwargs
+        return "node_enter_password", {"new_user": new_user, "username": username}
+
+    """Show the connect screen."""
+
+    callables = utils.callables_from_module(CONNECTION_SCREEN_MODULE)
+    if "connection_screen" in callables:
+        connection_screen = callables["connection_screen"]()
+    else:
+        connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE)
+        if not connection_screen:
+            connection_screen = "No connection screen found. Please contact an admin."
+
+    if _GUEST_ENABLED:
+        text = "Enter a new or existing user name to login (write 'guest' for a guest login):"
+    else:
+        text = "Enter a new or existing user name to login:"
+
+    text = "{}\n\n{}".format(connection_screen, text)
+
+    options = (
+        {"key": "", "goto": "node_enter_username"},
+        {"key": ("quit", "q"), "goto": "node_quit_or_login"},
+        {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})},
+        {"key": "_default", "goto": _check_input},
+    )
+    return text, options
+
+
+def node_enter_password(caller, raw_string, **kwargs):
+    """
+    Handle password input.
+
+    """
+
+    def _check_input(caller, password, **kwargs):
+        """
+        'Goto-callable', set up to be called from the _default option below.
+
+        Called when user enters a password string. Check username + password
+        viability. If it passes, the account will have been created and login
+        will be initiated.
+
+        The return from this goto-callable determines which node we go to next
+        and what kwarg it will be called with.
+
+        """
+        # these flags were set by the goto-callable
+        username = kwargs["username"]
+        new_user = kwargs["new_user"]
+        password = password.rstrip("\n")
+
+        session = caller
+        address = session.address
+        if new_user:
+            # create a new account
+            account, errors = _ACCOUNT.create(
+                username=username, password=password, ip=address, session=session
+            )
+        else:
+            # check password against existing account
+            account, errors = _ACCOUNT.authenticate(
+                username=username, password=password, ip=address, session=session
+            )
+
+        if account:
+            if new_user:
+                session.msg("|gA new account |c{}|g was created. Welcome!|n".format(username))
+            # pass login info to login node
+            return "node_quit_or_login", {"login": True, "account": account}
+        else:
+            # restart due to errors
+            session.msg("|R{}".format("\n".join(errors)))
+            kwargs["retry_password"] = True
+            return "node_enter_password", kwargs
+
+    def _restart_login(caller, *args, **kwargs):
+        caller.msg("|yCancelled login.|n")
+        return "node_enter_username"
+
+    username = kwargs["username"]
+    if kwargs["new_user"]:
+        if kwargs.get("retry_password"):
+            # Attempting to fix password
+            text = "Enter a new password:"
+        else:
+            text = "Creating a new account |c{}|n. " "Enter a password (empty to abort):".format(username)
+    else:
+        text = "Enter the password for account |c{}|n (empty to abort):".format(username)
+    options = (
+        {"key": "", "goto": _restart_login},
+        {"key": ("quit", "q"), "goto": "node_quit_or_login"},
+        {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})},
+        {"key": "_default", "goto": (_check_input, kwargs)},
+    )
+    return text, options
+
+
+def node_quit_or_login(caller, raw_text, **kwargs):
+    """
+    Exit menu, either by disconnecting or logging in.
+
+    """
+    session = caller
+    if kwargs.get("login"):
+        account = kwargs.get("account")
+        # TODO
+        # session.msg("|gLogging in ...|n")
+        # session.sessionhandler.login(session, account)
+
+        # # go OOC
+        # account.unpuppet_object(session)
+
+        EvMenu(
+            caller,
+            "menus.char_manager",
+            startnode="node_enter_char_management",
+            auto_look=False,
+            auto_quit=False,
+            cmd_on_exit=None,
+            node_formatter=plain_node_formatter,
+            account=account,
+        )
+
+
+    else:
+        session.sessionhandler.disconnect(session, "Goodbye! Logging off.")
+    return "", {}
+
+
+def plain_node_formatter(nodetext, optionstext, caller=None):
+    """Do not display the options, only the text.
+
+    This function is used by EvMenu to format the text of nodes. The menu login
+    is just a series of prompts, so we disable all automatic display decoration
+    and let the nodes handle everything on their own.
+
+    """
+    return nodetext

+ 1 - 0
server/conf/settings.py

@@ -41,6 +41,7 @@ GLOBAL_SCRIPTS = {
 
 PROTOTYPE_MODULES += ["world.tutorial_prototypes"]
 CRAFT_RECIPE_MODULES = ['world.recipes_base']
+SPELL_MODULES = ['world.spells']
 
 ######################################################################
 # Settings given in secret_settings.py override those in this file.

+ 1 - 0
typeclasses/characters.py

@@ -56,6 +56,7 @@ class Character(DefaultCharacter):
         }
 
         self.db.spells = []
+        self.db.recipes = []
         self.db.current_action = None
 
     def get_health(self):

+ 7 - 0
typeclasses/exits.py

@@ -39,6 +39,13 @@ class Exit(DefaultExit):
                                         defined, in which case that will simply be echoed.
     """
 
+    def at_init(self):
+        # check if this exit has directional alias
+        self.ndb.directional_alias = None
+        directional_aliases = [ele for ele in ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] if (ele in self.aliases.all())]
+        if directional_aliases:
+            self.ndb.directional_alias = directional_aliases[0]
+
     def at_traverse(self, traversing_object, target_location, **kwargs):
         if has_effect(traversing_object, "is_busy"):
             traversing_object.msg("You are already busy {}.".format(traversing_object.db.current_action.busy_msg()))

+ 51 - 3
typeclasses/rooms.py

@@ -33,8 +33,8 @@ class Room(DefaultRoom):
     def at_object_creation(self):
         self.locks.add("light:false()")
 
-        self.db.x = 0
-        self.db.y = 0
+        self.db.x = -1
+        self.db.y = -1
 
         self.db.map_icon = '|w⊡|n'
 
@@ -68,6 +68,7 @@ class IndoorRoom(Room):
         """
         Called when room is first recached (such as after a reload)
         """
+        super().at_init()
         self.check_light_state()
 
     def return_appearance(self, looker, **kwargs):
@@ -88,7 +89,10 @@ class IndoorRoom(Room):
         for con in visible:
             key = con.get_display_name(looker)
             if con.destination:
-                exits.append(key)
+                if con.ndb.directional_alias:
+                    exits.append("|m{}|n|w)|n{}".format(con.ndb.directional_alias, key))
+                else:
+                    exits.append(key)
             elif con.has_account:
                 users.append("|c%s|n" % key)
             elif con.db.feature_desc:
@@ -326,3 +330,47 @@ class Zone(DefaultRoom):
             for ex in room.exits:  # iterate exits in the room
                 if not self.ndb.sp_graph.is_edge(room, ex.destination):
                     self.ndb.sp_graph.add_edge(room, ex.destination, 1, ex.name)
+
+                # if destination is already inserted in zone we can proceed, otherwise
+                # aliases are created later.
+                if ex.destination.db.x != - 1 and ex.destination.db.y != - 1:
+                    self._update_exit_alias(ex)
+                    if ex.db.return_exit:
+                        self._update_exit_alias(ex.db.return_exit)
+
+    def _update_exit_alias(self, exit_obj):
+        xl, yl = exit_obj.location.db.x, exit_obj.location.db.y
+        xd, yd = exit_obj.destination.db.x, exit_obj.destination.db.y
+
+        # if aliases aren't already created, create them.
+        if not [ele for ele in ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] if(ele in exit_obj.aliases.all())]:
+            aliases = []
+            dx = xd - xl
+            dy = yd - yl
+            if dx == 0 and dy == -1:
+                aliases.extend(["n", "north"])
+                exit_obj.ndb.directional_alias = "n"
+            elif dx == 0 and dy == 1:
+                aliases.extend(["s", "south"])
+                exit_obj.ndb.directional_alias = "s"
+            elif dx == -1 and dy == 0:
+                aliases.extend(["w", "west"])
+                exit_obj.ndb.directional_alias = "w"
+            elif dx == 1 and dy == 0:
+                aliases.extend(["e", "east"])
+                exit_obj.ndb.directional_alias = "e"
+            elif dx == 1 and dy == -1:
+                aliases.extend(["ne", "northeast"])
+                exit_obj.ndb.directional_alias = "ne"
+            elif dx == -1 and dy == -1:
+                aliases.extend(["nw", "northwest"])
+                exit_obj.ndb.directional_alias = "nw"
+            elif dx == 1 and dy == 1:
+                aliases.extend(["se", "southeast"])
+                exit_obj.ndb.directional_alias = "se"
+            elif dx == -1 and dy == 1:
+                aliases.extend(["sw", "southwest"])
+                exit_obj.ndb.directional_alias = "sw"
+
+            exit_obj.aliases.add(aliases)
+

+ 55 - 22
utils/crafting.py

@@ -121,9 +121,10 @@ a full example of the components for creating a sword from base components.
 
 from copy import copy
 from evennia import logger
-from evennia.utils.utils import callables_from_module, inherits_from, make_iter, iter_to_string
+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 evennia.commands.command import Command
+from commands.command import Command
 from evennia.prototypes.spawner import spawn
 from evennia.utils.create import create_object, create_script
 
@@ -677,7 +678,6 @@ class CraftingRecipe(CraftingRecipeBase):
 
         tools = []
         for itag, tag in enumerate(cls.tool_tags):
-
             tools.append(
                 create_object(
                     key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()),
@@ -690,7 +690,7 @@ class CraftingRecipe(CraftingRecipeBase):
             consumables.append(
                 create_object(
                     key=cons_key
-                    or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
+                        or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
                     tags=[(tag, cls.consumable_tag_category), *cons_tags],
                     **consumable_kwargs,
                 )
@@ -717,14 +717,14 @@ class CraftingRecipe(CraftingRecipeBase):
         """
 
         def _check_completeness(
-            tagmap,
-            taglist,
-            namelist,
-            exact_match,
-            exact_order,
-            error_missing_message,
-            error_order_message,
-            error_excess_message,
+                tagmap,
+                taglist,
+                namelist,
+                exact_match,
+                exact_order,
+                error_missing_message,
+                error_order_message,
+                error_excess_message,
         ):
             """Compare tagmap (inputs) to taglist (required)"""
             valids = []
@@ -771,17 +771,17 @@ class CraftingRecipe(CraftingRecipeBase):
             obj: obj.tags.get(category=self.tool_tag_category, return_list=True)
             for obj in self.inputs
             if obj
-            and hasattr(obj, "tags")
-            and inherits_from(obj, "evennia.objects.models.ObjectDB")
+               and hasattr(obj, "tags")
+               and inherits_from(obj, "evennia.objects.models.ObjectDB")
         }
         tool_map = {obj: tags for obj, tags in tool_map.items() if tags}
         consumable_map = {
             obj: obj.tags.get(category=self.consumable_tag_category, return_list=True)
             for obj in self.inputs
             if obj
-            and hasattr(obj, "tags")
-            and obj not in tool_map
-            and inherits_from(obj, "evennia.objects.models.ObjectDB")
+               and hasattr(obj, "tags")
+               and obj not in tool_map
+               and inherits_from(obj, "evennia.objects.models.ObjectDB")
         }
         consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags}
 
@@ -1008,6 +1008,38 @@ class CraftingCmdSet(CmdSet):
 
     def at_cmdset_creation(self):
         self.add(CmdCraft())
+        self.add(CmdRecipes())
+
+
+class CmdRecipes(Command):
+    """
+        List known recipes.
+
+        Usage:
+          recipes
+        """
+
+    key = "recipes"
+    locks = "cmd:all()"
+    help_category = "General"
+    arg_regex = r"\s|$"
+
+    def func(self):
+        # delayed loading/caching of recipes
+        _load_recipes()
+
+        caller = self.caller
+
+        table = evtable.EvTable("|wName|n", "|wMaterials", "|wTools|n",
+                                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 recipe in _RECIPE_CLASSES.values():
+            if (recipe.name in caller.db.recipes) or caller.is_superuser:
+                table.add_row("|W{}".format(recipe.name), "|M{}".format(list_to_string(recipe.tool_tags)), "|G{}".format(list_to_string(recipe.consumable_tags)))
+
+        caller.msg(table)
 
 
 class CmdCraft(Command):
@@ -1095,9 +1127,9 @@ class CmdCraft(Command):
             if not obj:
                 return
             if (
-                not inherits_from(obj, "evennia.objects.models.ObjectDB")
-                or obj.sessions.all()
-                or not obj.access(caller, "craft", default=True)
+                    not inherits_from(obj, "evennia.objects.models.ObjectDB")
+                    or obj.sessions.all()
+                    or not obj.access(caller, "craft", default=True)
             ):
                 # We don't allow to include puppeted objects nor those with the
                 # 'negative' permission 'nocraft'.
@@ -1139,7 +1171,9 @@ class CmdCraft(Command):
 
         toggle_effect(caller, "is_busy")
         caller.msg("You start crafting {} {}.".format(indefinite_article(recipe_cls.name), recipe_cls.name))
-        action_script = create_script("utils.crafting.CmdCraftComplete", obj=caller, interval=recipe_cls.crafting_time, attributes=[("recipe", recipe_cls.name), ("tools_and_ingredients", tools_and_ingredients)])
+        action_script = create_script("utils.crafting.CmdCraftComplete", obj=caller, interval=recipe_cls.crafting_time,
+                                      attributes=[("recipe", recipe_cls.name),
+                                                  ("tools_and_ingredients", tools_and_ingredients)])
         caller.db.current_action = action_script
 
 
@@ -1171,4 +1205,3 @@ class CmdCraftComplete(CmdActionScript):
 
     def busy_msg(self):
         return "crafting {} {}".format(indefinite_article(self.db.recipe), self.db.recipe)
-

+ 385 - 0
utils/spells.py

@@ -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.
+
+    """

+ 8 - 4
utils/utils.py

@@ -1,26 +1,30 @@
-from evennia.prototypes import spawner
-
 def has_tag(obj, key, category):
-    return obj.tags.get(key=key, category=category) != None;
+    return obj.tags.get(key=key, category=category) is not None;
+
 
 def fmt_light(message):
     return "|r\u2600|y {} |r\u2600|n".format(message)
 
+
 def fmt_dark(message):
     return "|w\u2600|=h {} |w\u2600|n".format(message)
 
+
 def toggle_effect(obj, effect):
     if has_tag(obj, effect, "effect"):
         obj.tags.remove(effect, category="effect")
     else:
         obj.tags.add(effect, category="effect")
 
+
 def has_effect(obj, effect):
     return has_tag(obj, effect, "effect")
 
+
 def has_effect_in(obj, effects):
     return any(True for effect in effects if has_tag(obj, effect, "effect"))
 
+
 def indefinite_article(name):
     """Return the right english indefinite article for given name."""
     the_vowels = ["a","e","i","o","u"]
@@ -30,4 +34,4 @@ def indefinite_article(name):
     else:
         value = "a"
 
-    return value
+    return value

+ 60 - 42
world/spells.py

@@ -1,45 +1,63 @@
-from evennia import utils, create_script, logger
-from evennia.utils import inherits_from
-
+from evennia import create_script
 from typeclasses import effects
-from typeclasses.mobs import Mob
 from utils.utils import has_effect
 
-
-def spell_light(caller, target, **kwargs):
-    if not target:
-        target_obj = caller
-    else:
-        target_obj = caller.search(target, location=[caller, caller.location])
-
-    if not target_obj:
-        return
-
-    if has_effect(target_obj, "emit_magic_light"):
-        caller.msg("{} already has a magical light on itself.".format(target_obj.name))
-        return
-
-    light_script = create_script(effects.EffectMagicalLight, obj=target_obj)
-    caller.msg("You cast |wlight|n on {}.".format(target_obj.name))
-
-
-def spell_charm(caller, target, **kwargs):
-    if not target:
-        caller.msg("You need someone to place your charm on.")
-        return
-
-    target_obj = caller.search(target, location=[caller.location])
-
-    if not target_obj:
-        return
-
-    if not inherits_from(target_obj, Mob):
-        caller.msg("You cannot charm {}".format(target_obj.name))
-        return
-
-    if has_effect(target_obj, "charm"):
-        caller.msg("{} is already charmed.".format(target_obj.name))
-        return
-
-    charm_script = create_script(effects.EffectCharm, obj=target_obj, attributes=[("source", caller.dbref)])
-    caller.msg("You cast |wcharm|n on {}.".format(target_obj.name))
+from utils.spells import Spell
+
+
+class LightSpell(Spell):
+    """Make the target emit magical light"""
+    name = "light"  # name to refer to this recipe as
+    casting_time = 1
+    duration = 60
+    target_type = "SINGLE"
+    success_message = "You cast |wlight|n on {}."
+
+    def do_cast(self, **kwargs):
+        target_obj = self.target_obj
+        if has_effect(target_obj, "emit_magic_light"):
+            self.msg("{} already has a magical light on itself.".format(target_obj.name))
+            return False
+
+        create_script(effects.EffectMagicalLight, obj=target_obj, interval=self.duration)
+        self.success_message = self.success_message.format(target_obj.name)
+
+        return True
+
+# def spell_light(caller, target, **kwargs):
+#     if not target:
+#         target_obj = caller
+#     else:
+#         target_obj = caller.search(target, location=[caller, caller.location])
+#
+#     if not target_obj:
+#         return
+#
+#     if has_effect(target_obj, "emit_magic_light"):
+#         caller.msg("{} already has a magical light on itself.".format(target_obj.name))
+#         return
+#
+#     light_script = create_script(effects.EffectMagicalLight, obj=target_obj)
+#     caller.msg("You cast |wlight|n on {}.".format(target_obj.name))
+
+
+# def spell_charm(caller, target, **kwargs):
+#     if not target:
+#         caller.msg("You need someone to place your charm on.")
+#         return
+#
+#     target_obj = caller.search(target, location=[caller.location])
+#
+#     if not target_obj:
+#         return
+#
+#     if not inherits_from(target_obj, Mob):
+#         caller.msg("You cannot charm {}".format(target_obj.name))
+#         return
+#
+#     if has_effect(target_obj, "charm"):
+#         caller.msg("{} is already charmed.".format(target_obj.name))
+#         return
+#
+#     charm_script = create_script(effects.EffectCharm, obj=target_obj, attributes=[("source", caller.dbref)])
+#     caller.msg("You cast |wcharm|n on {}.".format(target_obj.name))